Skip to content

fix(brief): handle publisher-naming variants in stripHeadlineSuffix (DOJ / Bulletin)#3709

Merged
koala73 merged 1 commit into
mainfrom
fix/headline-suffix-naming-variants
May 15, 2026
Merged

fix(brief): handle publisher-naming variants in stripHeadlineSuffix (DOJ / Bulletin)#3709
koala73 merged 1 commit into
mainfrom
fix/headline-suffix-naming-variants

Conversation

@koala73

@koala73 koala73 commented May 15, 2026

Copy link
Copy Markdown
Owner

Summary

Three structural variant classes between the headline-suffix's publisher form and the configured source field were observed live on the May 15 brief — Layer 1's strict word-prefix test (PR #3673's load-bearing asymmetric protection) misses all three:

Class Example Card on May 15
Article insertion tail Bulletin of the Atomic Scientists vs source Bulletin of Atomic Scientists #2
Trailing wire-suffix word tail BBC News vs source BBC (general)
Abbreviation ↔ long-form tail Department of Justice (.gov) vs source DOJ #3

Approach

Layer 2 adds two source-aware paths after Layer 1, all preserving the asymmetric protection:

  • Path 2anormalizePublisher on both sides + the same asymmetric prefix test. Strips trailing domain paren, removes article words globally, removes wire-suffix words only from the trailing position iteratively. Trailing-only is load-bearing: stripping news/press/daily globally would mangle Daily Mail, News Corp, Press TV, The Press Democrat.
  • Path 2b — acronym-shape-gated initials equivalence. Two gates: (a) original publisher matches /^[A-Z]{1,5}$/ — explicit opt-in via ALL-CAPS configured field; (b) tail passes Title-Case-or-connector shape check. Initials use a separate tailForInitials (not normalizePublisher) so wire-suffix words like Press in Associated Press survive to count toward ap.

A speculative source-blind shape fallback ("Layer 3") was scoped and rejected on Codex review — feed-controlled text could force user-visible truncation, an integrity risk not worth the speculative benefit. See plan for the full design rationale.

What this is NOT

  • Per-publisher alias maps. The initials test handles abbreviations generically; an alias map would be maintenance burden without structural benefit.
  • stripHeadlinePrefix pipe-separator boilerplate (e.g. "Office of Public Affairs |") — separate problem, deferred.
  • Schema changes / ingest-time RSS <channel><title> capture — the architecturally pure long-term fix; documented in plan as the next step if the source-aware layer leaves edge cases.

Test plan

  • 26 new test scenarios (S1–S26b) covering happy paths, the four critical-regression classes (wire-suffix in non-trailing positions / Title-Case gate blocks lowercase editorial / ordinary Title-Case publishers don't trigger initials / PR fix(brief): severity/topic-cluster ordering + Video:/Reuters headline cleanup #3673 asymmetric protection holds at every layer), and edge cases.
  • All 77 existing stripHeadlineSuffix tests pass unchanged — Layer 1 is untouched; Layer 2 only fires when Layer 1 returns no match.
  • 350 brief/digest tests pass under tsx --test (broader sweep including brief-llm, brief-magazine-render, digest-orchestration-helpers, brief-edge-route-smoke).
  • typecheck:api clean, biome check clean (no new warnings on touched files).

Codex review history

The plan was reviewed across 3 rounds before this PR (max 5; Codex hit its usage limit before Round 4 could verdict). Key Codex catches that hardened the design:

  • R1: forced dropping the speculative source-blind layer (integrity risk).
  • R2 (P1): caught that reusing normalizePublisher for initials would corrupt Associated Press → AP (Press got stripped). Fix: separate tailForInitials helper. Test S14 locks this.
  • R2 (P2): caught that gating the initials trigger on lowercased normPub would accept ordinary Title-Case publishers like Time/Wired/Axios as acronyms. Fix: gate on the original publisher matching /^[A-Z]{1,5}$/. Tests S18b/S18c/S18d lock this.

Plan: docs/plans/2026-05-15-001-fix-headline-suffix-strip-publisher-naming-variants-plan.md (gitignored, not in this PR).

Three structural variant classes between the headline-suffix's publisher
form and the configured `source` field were observed live on the May 15
brief. Layer 1's strict word-prefix test (the load-bearing PR #3673
asymmetric protection) misses all three:

  1. Article insertion — tail "Bulletin of the Atomic Scientists" vs
     source "Bulletin of Atomic Scientists" (the/no-the).
  2. Trailing wire-suffix word — tail "BBC News" vs source "BBC".
  3. Abbreviation ↔ long-form — tail "Department of Justice (.gov)"
     vs source "DOJ".

Layer 2 adds two source-aware paths after Layer 1, all preserving the
asymmetric protection:

  Path 2a — `normalizePublisher` on both sides + the same asymmetric
            prefix test. Lowercases, strips trailing domain paren,
            removes article words globally, removes wire-suffix words
            ONLY from the trailing position iteratively. Trailing-only
            is load-bearing: stripping `news`/`press`/`daily` globally
            would corrupt "Daily Mail", "News Corp", "Press TV", "The
            Press Democrat".

  Path 2b — acronym-shape-gated initials equivalence. Two gates:
            (a) original publisher matches /^[A-Z]{1,5}$/ — explicit
            opt-in via ALL-CAPS configured field, so DOJ/NPR/AP/BBC
            qualify but Title-Case names like Time/Wired/Axios do not;
            (b) tail passes a Title-Case-or-connector shape check so
            lowercase editorial text like "trump says that" cannot
            initials-match. Initials use a SEPARATE `tailForInitials`
            (not `normalizePublisher`) so wire-suffix words like
            `Press` in "Associated Press" survive to count toward `ap`.

A speculative source-blind shape fallback ("Layer 3") was scoped and
rejected on Codex review — feed-controlled text could force user-visible
truncation, an integrity risk not worth the speculative benefit.

The PR #3673 asymmetric protection (S21 — "AP News analysis" vs "AP News"
must NOT strip) holds at every layer and is locked in a regression test.

26 new test scenarios in tests/brief-from-digest-stories.test.mjs;
77 existing stripHeadlineSuffix tests unchanged. 350 brief/digest tests
pass under `tsx --test`. typecheck:api + biome clean.

Plan: docs/plans/2026-05-15-001-fix-headline-suffix-strip-publisher-naming-variants-plan.md
@vercel

vercel Bot commented May 15, 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 15, 2026 8:32am

Request Review

@greptile-apps

greptile-apps Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds Layer 2 publisher-naming variant matching to stripHeadlineSuffix, closing three classes of mismatch between RSS headline attributions and configured source names observed on the May 15 brief: article insertion, trailing wire-suffix words, and abbreviation to long-form. The load-bearing PR #3673 asymmetric protection is explicitly preserved at every new path.

  • Path 2a (normalizePublisher on both sides) handles all three classes by lowercasing, stripping articles globally, and iteratively removing trailing wire-suffix tokens before applying the same asymmetric prefix test.
  • Path 2b (acronym-shape-gated initials) is gated on the original publisher matching /^[A-Z]{1,5}$/; tailForInitials is kept separate so wire-suffix words like Press in "Associated Press" survive to contribute their initial.
  • 24 new test functions cover happy paths, critical-regression protection, and documented edge cases.

Confidence Score: 4/5

Safe to merge; the new matching paths preserve the existing asymmetric protection and the test suite is thorough for the targeted cases.

The implementation is carefully layered and well-commented. The only gap is that Path 2a normalizes trailing wire-suffix tokens off both the tail and the publisher before comparing, so two publications sharing the same core name but using different wire-suffix words (e.g., configured Fox News vs a tail of Fox Daily) would be treated as equivalent — a case not documented or tested. This is unlikely to surface in practice since RSS feeds are consistent in their attribution, but it is an undocumented behavior boundary.

The core logic in scripts/lib/brief-compose.mjs around Path 2a publisher-side normalization deserves a second read; the cross-wire-suffix edge case lives there.

Important Files Changed

Filename Overview
scripts/lib/brief-compose.mjs Adds Layer 2 publisher-naming variant matching (Path 2a normalised-prefix + Path 2b acronym-initials) after the existing Layer 1 asymmetric prefix test. The asymmetric protection from PR #3673 is preserved at every new path. One undocumented edge case: both sides of Path 2a have trailing wire-suffix tokens stripped, so publishers sharing the same core name but different suffixes are treated as equivalent.
tests/brief-from-digest-stories.test.mjs Adds a new describe block with 24 test functions covering happy paths, wire-suffix preservation, domain parens, the initials path, and PR #3673 regression protection at all layers. The comment on test S10 incorrectly attributes the strip to normalizePublisher article-stripping when the test actually exercises Layer 1 equality.

Reviews (1): Last reviewed commit: "fix(brief): handle publisher-naming vari..." | Re-trigger Greptile

);
});

it('S10: strips "The Press Democrat" via Layer 1 equality — verifies article stripped + middle "press" preserved', () => {

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 The comment on S10 claims this test "verifies article stripped + middle press preserved", but the test exercises Layer 1 (exact case-insensitive equality between tail and publisher) and never reaches normalizePublisher at all. No article-stripping or wire-suffix logic runs here. A developer reading the test suite could incorrectly conclude that normalizePublisher's handling of leading/middle press is covered by this test, when it isn't.

Suggested change
it('S10: strips "The Press Democrat" via Layer 1 equality — verifies article stripped + middle "press" preserved', () => {
it('S10: strips "The Press Democrat" via Layer 1 equality — middle "press" is preserved but Layer 1 never invokes normalizePublisher', () => {

Comment on lines +452 to +465
// Layer 2 — Path 2a: source-aware fuzzy match via normalised
// asymmetric prefix. normalizePublisher strips articles globally and
// wire-suffix words trailing-only, so "Bulletin of the Atomic
// Scientists" matches "Bulletin of Atomic Scientists" and "BBC News"
// matches "BBC" — without mangling "Daily Mail" or "News Corp".
const normTail = normalizePublisher(tail);
const normPub = normalizePublisher(publisher);
if (
normTail.length > 0
&& normPub.length > 0
&& isPublisherWordPrefix(normTail, normPub)
) {
return stripped;
}

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 Cross-suffix false positive in Path 2a

normalizePublisher strips trailing wire-suffix tokens from both the tail and the publisher before the prefix test. This means two publications that share the same core name but differ only in their trailing wire-suffix word are treated as equivalent. Concretely: if the configured publisher is "Fox News" and the headline tail is "Fox Daily", both normalize to "fox" and the strip fires. The same applies to any {Core} {WireSuffix-A} publisher paired with a {Core} {WireSuffix-B} tail. While RSS feeds are generally consistent in their attribution (making this unlikely in production), the gap is undocumented and unguarded by any test.

@koala73
koala73 merged commit 70c4afc into main May 15, 2026
12 checks passed
@koala73
koala73 deleted the fix/headline-suffix-naming-variants branch May 15, 2026 10:15
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