From dae6a51db03f0888ce50ca99f6a5b65641eda7bc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Jul 2026 13:41:11 +0000 Subject: [PATCH] Skip the AI rewrite entirely for single-source clusters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cluster of one item still went through synthesizeArticle to be "lightly rewritten" — the only recent real-world example fabricated a fake two-outlet merge out of one genuine article (see the opaque-sourceId attribution fix). There's no actual synthesis to do with one source, so the rewrite step only added risk (hallucinated attribution, subtly altered facts) for no benefit. priorityQueue.ts's runSynthesisCycle now routes a 1-item cluster to publishDirect instead of publishCluster — same verbatim-text path already used for youtube/nitter/telegram items and AI-disabled categories. publishCluster is now only ever called with 2+ items, so its doc comment and synthesis.ts's system prompt no longer reference the single-source case. Verified directly: a 1-item cluster now publishes with the original body untouched and zero calls to the model, while a 2-item cluster still goes through the AI merge path unchanged. --- backend/src/pipeline/publish.ts | 5 +++-- backend/src/pipeline/synthesis.ts | 2 -- backend/src/queue/priorityQueue.ts | 9 ++++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/pipeline/publish.ts b/backend/src/pipeline/publish.ts index d09a7b4..27ac515 100644 --- a/backend/src/pipeline/publish.ts +++ b/backend/src/pipeline/publish.ts @@ -349,8 +349,9 @@ export async function publishDirect( /** * Publishing is always automatic — there's no draft/review state (see schema doc). - * A cluster of size 1 publishes as-is via the same path; synthesizeArticle lightly - * rewrites rather than merges when there's only one source. + * Callers should route a size-1 cluster to publishDirect instead — there's nothing to + * merge, so an LLM rewrite would only add risk (hallucinated attribution, altered + * facts) for no synthesis benefit. See priorityQueue.ts's runSynthesisCycle. */ export async function publishCluster( provider: InferenceProvider, diff --git a/backend/src/pipeline/synthesis.ts b/backend/src/pipeline/synthesis.ts index fd1898f..21bacbf 100644 --- a/backend/src/pipeline/synthesis.ts +++ b/backend/src/pipeline/synthesis.ts @@ -37,8 +37,6 @@ const SYSTEM_PROMPT = `You are a neutral news synthesis assistant. Given summari - Stays neutral and factual, without editorializing - Is 2-4 short paragraphs -If only one source is provided, lightly rewrite it in your own words rather than merging, and do not attribute it to any outlet other than that single given source. - After the article, on a new line, write exactly "${TAG_DELIMITER}" followed by 2-4 short comma-separated topic/entity tags (e.g. proper nouns, named events) that this article is about. If nothing salient qualifies, leave the tag line empty.`; export interface SynthesisResult { diff --git a/backend/src/queue/priorityQueue.ts b/backend/src/queue/priorityQueue.ts index 3894bc8..15323e3 100644 --- a/backend/src/queue/priorityQueue.ts +++ b/backend/src/queue/priorityQueue.ts @@ -186,7 +186,14 @@ export async function runSynthesisCycle(provider: InferenceProvider, settings: G // in practice a cluster's items are all near-duplicate coverage of the same // story, so they'd all match the same event's filter anyway when they match at all. const eventId = cluster.items.map((i) => claimedEventId(i, activeEvents)).find((id) => id !== null) ?? undefined; - const article = await publishCluster(provider, settings, cluster, { eventId }); + // A single-item cluster has nothing to merge — publish the source's own text + // verbatim instead of asking the LLM to "lightly rewrite" it, which only risked + // introducing errors (or fabricated attribution — see synthesis.ts) with no + // actual synthesis to justify the risk. + const article = + cluster.items.length === 1 + ? await publishDirect(cluster.items[0], settings, { eventId }) + : await publishCluster(provider, settings, cluster, { eventId }); contentItemsDb.assignCluster( cluster.items.map((i) => i.id), cluster.id