Skip to content

fix(brief): severity/topic-cluster ordering + Video:/Reuters headline cleanup#3673

Merged
koala73 merged 4 commits into
mainfrom
fix/brief-headline-cleanup
May 13, 2026
Merged

fix(brief): severity/topic-cluster ordering + Video:/Reuters headline cleanup#3673
koala73 merged 4 commits into
mainfrom
fix/brief-headline-cleanup

Conversation

@koala73

@koala73 koala73 commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

This PR fixes three brief-quality issues observed on the May 12 / May 13 sends:

  1. Editorial prefixes shipping to the magazine. May 12 brief, page 16/18: "Video: Philippine senator flees ICC arrest…". Strip Video:, Watch:, Live:, Photos:, Breaking:, Exclusive:, Opinion:, Analysis:, Update:, and similar editorial-format prefixes (12 variants) at compose time. The trailing colon is REQUIRED so legitimate noun-form headlines ("Video game regulator…", "Watch list updated…", "Live broadcasts paused…") survive intact.

  2. Wire-name suffixes surviving when feed-name is the longer form. May 13 brief, story 5: "Putin says Russia will deploy new Sarmat nuclear missile this year - Reuters"; primarySource was Reuters World. The old strict-equality check ("reuters" !== "reuters world") shipped the suffix to the magazine. Fix: asymmetric word-boundary prefix match — tail must be a SHORTER prefix of publisher (Reuters against Reuters World → strip). The inverse direction is rejected ON PURPOSE so "Story - AP News analysis" with publisher AP News keeps the editorial analysis word.

  3. Severity / topic-cluster ordering breaking down to a chaotic interleaved walk. May 13 brief order was 🔴🟠🟠🟠🔴🔴🔴🔴🔴🟠🟠🟠 with Nigeria stories split by Syria and the Iran arc fragmented across four non-adjacent positions. Root cause: applyRankedOrder let LLM rankedStoryHashes dominate the global sort, overriding the topic blocks from groupTopicsPostDedup and ignoring severity entirely. Replace with orderBriefCandidates — a block-based sort keyed by (topic block's highest eligible severity, count at that severity, eligible block size, max score, then LLM rank as tie-breaker). Critical clusters now stay contiguous; LLM still chooses between similarly severe candidates.

Ordering trade-off

The ordering model intentionally favors concentrated top severity before breadth: a block with two critical stories sorts ahead of a block with one critical plus several high stories. Once top-severity count ties, broader eligible block size wins, then max score, then LLM rank. Each tiebreak rung is individually tested so a future contributor reordering the keys will fail a named test, not a vibes check.

Rows without briefTopicId or clusterRepHash degrade to per-row singleton blocks. That preserves severity ordering but loses topic adjacency for legacy/raw rows — deliberate, to avoid false-grouping unrelated stories under a guessed key.

Ops note

dropped_cap telemetry now counts only otherwise-renderable stories skipped after maxStories. Invalid or sensitivity-excluded tail items keep their root-cause reason (severity, headline, url, etc.). The reconciliation invariant still holds (in === out + Σ dropped_*), but absolute dropped_cap values may be lower post-deploy. Worth recalibrating any alerts keyed on the absolute count.

Test coverage

  • REGRESSION (May 13 brief): critical clusters beat lower-severity or ranked singletons
  • Tiebreak coverage for top-severity count, eligible block size, max score, and final LLM rank — one test per rung in the chain
  • REGRESSION (May 13 brief): Sarmat / Reuters / Reuters World suffix stripping
  • REGRESSION (May 12 brief): Video: prefix stripping
  • Rejects inverse suffix stripping for AP News / AP News analysis
  • Word-boundary stem rejection (reuter / Reuters, iran / iranian press)
  • Topic metadata plumbing from groupTopicsPostDedup through compose
  • Cap-drop reconciliation and root-cause telemetry behavior

Test plan

  • node --test tests/brief-filter.test.mjs
  • node --test tests/brief-from-digest-stories.test.mjs
  • node --test tests/brief-dedup-embedding.test.mjs
  • npm run typecheck
  • biome check clean (verified locally + on CI)
  • After deploy, verify next production brief shows:
    • critical-cluster-first ordering (May 13 🔴🟠🟠🟠🔴🔴🔴🔴🔴🟠🟠🟠 interleave fixed)
    • topic-adjacent stories within each severity tier (Nigeria adjacency, Iran-arc grouped)
    • no Video: / Watch: / Live: prefixes on magazine cards
    • no - Reuters suffix when story source is Reuters World

Notes

Independent of PR #3667 (digest grounding validation). PR #3667 guards synthesis content grounding (catches hallucinated leads); this PR handles headline cleanup and deterministic story ordering before render. Both touch the brief but address orthogonal bug classes.

For the full pipeline context, see docs/internal/brief130526.md — the §7 signal-conflict matrix names every offending signal interaction this PR closes.

…d-name in suffix

Two real headline-noise issues observed in production briefs.

#1 — Editorial-format prefixes shipped to the magazine.

  May 12 brief, page 16/18:
    "Video: Philippine senator flees ICC arrest over role in drug war"

  The "Video: " tells the user nothing the magazine card body
  doesn't already convey (every card has its own source line and
  body block). Same shape applies to Watch / Live / Photos / Photo /
  Gallery / Listen / Podcast / Breaking / Exclusive / Opinion /
  Analysis / Update.

  New stripHeadlinePrefix helper handles all of the above with a
  REQUIRED trailing colon — preserves legitimate headlines that use
  one of these words as a noun ("Video game regulator fines...",
  "Watch list updated by sanctions office...", "Live broadcasts
  paused...").

#2 — Wire-name suffix not stripped when feed-name is the longer form.

  May 13 brief, story 5:
    "Putin says Russia will deploy new Sarmat nuclear missile this
     year - Reuters"
  primarySource: "Reuters World"

  stripHeadlineSuffix used strict equality: "reuters" !== "reuters
  world", so the suffix shipped. Fix is asymmetric word-boundary
  prefix-match: tail must be a SHORTER prefix of publisher (with a
  trailing space delimiter), e.g. tail "Reuters" against publisher
  "Reuters World" → strip. The inverse direction (publisher prefix
  of tail) is rejected ON PURPOSE — "Story - AP News analysis" with
  publisher "AP News" must NOT strip because "analysis" is editorial
  content extending the publisher name, not a desk-name suffix.

Both helpers wired into digestStoryToUpstreamTopStory in two-stage
order: prefix first, then suffix (handles "Video: Story - Al Jazeera"
correctly).

Test coverage:
  - REGRESSION (May 13 brief) for the Sarmat/Reuters/Reuters World case
  - REGRESSION (May 12 brief) for the Video: Philippine senator case
  - REJECTS-the-inverse coverage for AP News / AP News analysis
  - Word-boundary stem rejection ("reuter" / "Reuters", "iran" /
    "iranian press")
  - All 12 prefix variants individually tested

Tests: 69/69 brief-from-digest-stories pass (was 56), 8669/8669 full
unit suite pass, typecheck clean, biome clean.
@vercel

vercel Bot commented May 13, 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 13, 2026 11:25am

Request Review

@greptile-apps

greptile-apps Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two production headline-noise bugs: editorial-format prefixes (e.g., Video: ) reaching the magazine, and wire-name suffixes (e.g., - Reuters) surviving when the feed's primarySource is the longer form Reuters World. Both fixes are clean, well-documented, and covered by targeted regression tests.

  • stripHeadlinePrefix — new exported helper using a regex against 12 editorial-prefix variants, requiring a trailing colon to avoid clobbering legitimate noun-form headlines like Video game regulator….
  • isPublisherWordPrefix + stripHeadlineSuffix update — replaces strict equality with an intentionally asymmetric word-boundary check: the headline tail may be a shorter prefix of the publisher name (catching Reuters / Reuters World), but the inverse is explicitly rejected to preserve editorial suffixes like AP News analysis.
  • digestStoryToUpstreamTopStory — wires the two helpers in two-stage order (prefix first, then suffix) so combined cases like Video: Headline - Al Jazeera are handled correctly.

Confidence Score: 4/5

Safe to merge — both helpers are additive, the asymmetric suffix logic matches the documented and tested intent, and no existing behavior is regressed.

The implementation is correct and well-tested (13 new cases including both production regressions, all 12 prefix variants, and the load-bearing asymmetric inverse rejection). The only issue is one inline comment describing the suffix match as bidirectional when the code and PR description both document the asymmetry as intentional and critical — a future maintainer reading only the comment could misapply or remove the asymmetry.

The inline comment on lines 287-292 of scripts/lib/brief-compose.mjs misrepresents the directionality of isPublisherWordPrefix; everything else looks solid.

Important Files Changed

Filename Overview
scripts/lib/brief-compose.mjs Adds stripHeadlinePrefix (regex-based editorial prefix stripper) and isPublisherWordPrefix (asymmetric word-boundary helper for suffix matching); wires both into digestStoryToUpstreamTopStory. Logic is correct; one inline comment misrepresents the asymmetric direction as bidirectional.
tests/brief-from-digest-stories.test.mjs Adds 13 new test cases covering both regressions, all 12 prefix variants, noun-form preservation, word-boundary stem rejection, and the asymmetric inverse rejection — solid coverage for the new helpers.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["rawTitle (from digest story)"] --> B["stripHeadlinePrefix(rawTitle)"]
    B --> C{"Matches HEADLINE_PREFIX_RE?\n^(video|watch|live|…)\\s*:\\s*"}
    C -- Yes --> D["Remove prefix → prefixStripped"]
    C -- No --> D2["Return unchanged → prefixStripped"]
    D --> E["stripHeadlineSuffix(prefixStripped, primarySource)"]
    D2 --> E
    E --> F{"Matches HEADLINE_SUFFIX_RE_PART?\n\\s+[-–—|]\\s+([^\\s].*)$"}
    F -- No --> G["Return trimmed title → cleanTitle"]
    F -- Yes --> H["Extract tail"]
    H --> I{"isPublisherWordPrefix(tail, publisher)?"}
    I -- "tail === publisher OR tail is word-boundary prefix" --> J["Slice off suffix → cleanTitle"]
    I -- "tail longer than publisher OR not a word prefix" --> K["Return trimmed (keep editorial suffix) → cleanTitle"]
    J --> L["digestStoryToUpstreamTopStory output"]
    G --> L
    K --> L
Loading

Reviews (1): Last reviewed commit: "fix(brief): strip "Video:"/"Watch:" pref..." | Re-trigger Greptile

Comment thread scripts/lib/brief-compose.mjs Outdated
Comment on lines +287 to +292
// Case-insensitive equality OR word-boundary prefix in either
// direction. The prefix relaxation handles wire-name vs feed-name
// mismatch (Reuters / Reuters World, AP / AP News). A tail that
// merely CONTAINS the publisher (e.g. "- AP News analysis") still
// stays — that's editorial content, not a wire suffix.
if (!isPublisherWordPrefix(tail.toLowerCase(), publisher.toLowerCase())) return trimmed;

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 inline comment says "prefix in either direction", but the implementation is intentionally one-directional only: isPublisherWordPrefix(tail, publisher) accepts tail as a shorter prefix of publisher, never the reverse. The PR description explicitly calls this asymmetry the "load-bearing trick" (preventing "AP News analysis" from stripping), so the comment actively contradicts the design intent and would mislead anyone reading the code without the PR context.

Suggested change
// Case-insensitive equality OR word-boundary prefix in either
// direction. The prefix relaxation handles wire-name vs feed-name
// mismatch (Reuters / Reuters World, AP / AP News). A tail that
// merely CONTAINS the publisher (e.g. "- AP News analysis") still
// stays — that's editorial content, not a wire suffix.
if (!isPublisherWordPrefix(tail.toLowerCase(), publisher.toLowerCase())) return trimmed;
// Case-insensitive equality OR asymmetric word-boundary prefix: tail
// must be a SHORTER prefix of publisher (Reuters → Reuters World),
// never the reverse. The asymmetry is intentional — it blocks
// editorial suffixes like "AP News analysis" from being stripped
// when publisher is "AP News". A tail that merely CONTAINS the
// publisher (e.g. "- AP News analysis") still stays.
if (!isPublisherWordPrefix(tail.toLowerCase(), publisher.toLowerCase())) return trimmed;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid catch — fixed in commit 1d91206. The "either direction" wording was a stale leftover from round-1 of this PR's internal iteration. Updated comment now names the asymmetry explicitly:

Case-insensitive equality OR ASYMMETRIC word-boundary prefix: tail must be a SHORTER prefix of publisher (Reuters → Reuters World), never the reverse. The asymmetry is intentional and load-bearing — it blocks editorial suffixes like "AP News analysis" (tail) from stripping when publisher is "AP News".

Behaviour unchanged. Tests still 69/69 pass; biome clean. Thanks for the catch.

koala73 added 2 commits May 13, 2026 12:57
…ic one-directional match (PR #3673 review)

The inline comment in stripHeadlineSuffix said 'word-boundary prefix
in either direction' but the actual implementation (and the
isPublisherWordPrefix docstring) is one-directional only: tail must be
a SHORTER prefix of publisher, never the reverse. The asymmetry is
intentional and load-bearing — it's what blocks editorial suffixes
like 'AP News analysis' from stripping when publisher is 'AP News'.

The 'either direction' wording was left over from round-1 of the same
PR's internal iteration. Updated comment names the asymmetry
explicitly and cross-references the full rationale on
isPublisherWordPrefix.

Comment-only change. Behaviour unchanged: 69/69 brief-from-digest
tests still pass, biome clean.
@koala73 koala73 changed the title fix(brief): strip "Video:"/"Watch:" prefixes + match wire-name vs feed-name in suffix fix(brief): severity/topic-cluster ordering + Video:/Reuters headline cleanup May 13, 2026
@koala73
koala73 merged commit 9a777dc into main May 13, 2026
11 checks passed
@koala73
koala73 deleted the fix/brief-headline-cleanup branch May 13, 2026 15:39
koala73 added a commit that referenced this pull request May 15, 2026
…3709)

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
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