Skip to content

fix(clustering): re-title events from their cluster, not their seed article - #75

Merged
flvvius merged 1 commit into
mainfrom
claude/retitle-divergent-events
Aug 27, 2026
Merged

fix(clustering): re-title events from their cluster, not their seed article#75
flvvius merged 1 commit into
mainfrom
claude/retitle-divergent-events

Conversation

@flvvius

@flvvius flvvius commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes events whose title and summary describe different stories.

Root cause

An event's title is the title of whichever article created it, and nothing ever updated it as the cluster grew (clustering.ts sets title at insert("events") and never patches it). The summary, by contrast, is regenerated from every article in the cluster.

So when a cluster is seeded by one story and later dominated by another, the summary follows the majority and the title does not.

Two production examples:

Event Cluster
"Adjuncta șefului cabinetului lui Zelenski a fost arestată" 18 articles — 17 about the CIA director's Moscow visit. The single arrest article (Biziday) seeded the event and supplied the title.
"Oana Țoiu a comentat la CNN vizita șefului CIA…" 7 articles — 6 about Nicușor Dan's Fox News editorial. One Digi24 article gave the title.

Roughly 4–6% of recent events were affected. (A naive title/summary word-overlap scan reports more, but many are false positives — the prompt mandates paraphrase, so low overlap is often correct.)

Fix

lib/eventTitle.ts picks the medoid of the cluster's article titles under token overlap: the majority story's titles score high against one another, the outlier scores near zero. In the Moscow cluster the seventeen Moscow headlines share moscova/ratcliffe/directorul; the lone arrest headline shares almost nothing and loses.

  • Pure string logic — no embeddings, no model call.
  • Runs inside summarization, which already holds every article, so it costs no extra reads. This matters because the attach path deliberately avoids scanning all articles (that was the perf(convex): cut prod bill from ~$41/mo to ~$1-3/mo #63 I/O work).
  • Title and summary are now written in the same patch from the same inputs — the invariant that was missing.
  • A rename must beat the incumbent by RETITLE_MARGIN so near-ties cannot flip a published headline back and forth as articles trickle in.

slug is deliberately not touched

It is the permanent URL and is already shared and indexed. A repaired event keeps its old slug and gains a correct title; changing slugs would break shared links, canonicals and the sitemap.

Backfill

retitleDivergentEvents repairs events already stored, which would otherwise keep a wrong headline until something happened to re-summarize them. Paginated, dryRun: true by default, returns before/after samples.

Testing

338 passed, 4 skipped across 40 files. New tests are built on the real Moscow cluster: the outlier scores lowest, the event gets renamed to a Moscow headline, a coherent cluster is left alone, small clusters never rename, and unrelated titles don't oscillate.

Also fixed two bugs in my own stopword list before shipping: a stray Cyrillic token, and china — which would have stripped a meaningful word from headlines.

🤖 Generated with Claude Code


Generated by Claude Code

…rticle

An event's title was the title of whichever article created it, and
nothing ever updated it as the cluster grew. The summary, regenerated
from every article, follows the majority story — so a cluster seeded by
one story and later dominated by another ends up with a headline its own
summary contradicts.

Production had an 18-article event titled "Adjuncta șefului cabinetului
lui Zelenski a fost arestată" whose other 17 articles, and its summary,
were about the CIA director's visit to Moscow. A 7-article event titled
with an Oana Țoiu CNN quote carried a summary about Nicușor Dan's Fox
News editorial. Roughly 4-6% of recent events were affected.

lib/eventTitle.ts picks the medoid of the cluster's article titles under
token overlap: the majority story's titles score high against each
other, the outlier scores near zero. Pure string logic — no embeddings,
no model call — and it runs inside summarization, which already holds
every article, so it costs no extra reads. A rename needs to beat the
incumbent by a margin so near-ties cannot flip a published headline back
and forth.

Title and summary are now written in the same patch from the same
inputs, which is the invariant that was missing.

`slug` is deliberately left alone: it is the permanent URL and is
already shared and indexed, so a repaired event keeps its old slug.

Adds retitleDivergentEvents to repair events already stored, and tests
built on the real Moscow cluster.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MpkuigW6T4N1exXip6bRp
@flvvius
flvvius merged commit c53882b into main Aug 27, 2026
1 of 3 checks passed
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
news-web Building Building Preview Aug 27, 2026 7:02pm

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ada969d-eae1-43fe-9877-5093e5078325


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

flvvius added a commit that referenced this pull request Aug 27, 2026
The margin-based rule shipped in #75 over-fired. A dry run of the
backfill renamed 52 of 200 events (26%), and the samples showed why:
most were same-story swaps, e.g. "Două explozii la Damasc, lângă hotelul
unde a fost cazat Macron" replaced by "Explozii la Damasc, lângă hotelul
în care este cazat Macron". That is churn on a published headline, not a
repair.

Calibrating against 26 production clusters showed the discriminator is
the incumbent's absolute centrality, not its gap to the winner:

  Zelensky -> Moscow      incumbent 0.014   genuine divergence
  Oana Țoiu -> Nicușor Dan incumbent 0.014  genuine divergence
  Damen Mangalia shipyard  incumbent 0.163  same story
  Air alert                incumbent 0.158  same story
  Helicopter / cable car   incumbent 0.086  same story

A title is now treated as the outlier only when it shares essentially
nothing with its cluster (< 0.05), the replacement is itself
representative (>= 0.05), and it dominates by 3x rather than edging
ahead. Both real divergent events still get renamed; the same-story
swaps no longer do.

Adds the Damascus cluster as a churn regression test.


Claude-Session: https://claude.ai/code/session_012MpkuigW6T4N1exXip6bRp

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants