Skip to content
Back to Blog
RAGSupabaseNext.jsTypeScriptblog pipelineretrievalcodebase indexing

Coverage Before Creativity: The RAG Gate That Keeps My Blog Pipeline Honest

Daniel Anthony Romitelli Jr. · April 14, 2026

The first failure I had to get rid of in the blog pipeline wasn't a bad paragraph. It was a bad evidence set. The system would find a few nearby chunks, mistake density for coverage, and then draft as if that narrow slice stood in for the whole repository. Text like that sounds confident right up until you put it next to the code. So I stopped treating topic selection as a writing problem and started treating it as a retrieval coverage problem.

That distinction does real work. If the upstream evidence is thin, no amount of prompt polish rescues the result, and the draft will still overfit whichever cluster of files happened to match the query first. I wanted the pipeline disciplined about breadth before it got creative about prose. So the gate moved earlier: query fan-out, file-path-aware dedupe, breadth validation, pinned excerpts, and only then the writing pass.

The gate lives before the writing step

In my pipeline the important work happens before generation starts. The dispatcher is where the topic search fans out through multiple lanes: curated highlights, a fixed RAG query pool, and recent commit-derived queries. That's deliberate. A single semantic search tends to collapse into the same dense corners of the codebase, which is exactly where a system gets persuasive and shallow at once.

The dispatcher doesn't need to know how the post will read yet. Its job is to prove that the candidate topic has enough distinct evidence behind it to deserve a draft. Which means the retrieval layer has to do more than collect relevant chunks. It has to show spread, and it has to show that the match didn't come from one file, one subsystem, or one repetitive cluster of adjacent chunks.

That's why the shared blog utilities matter. The generator path imports checkRagSufficiency and fetchFailureEvidence from supabase/functions/_shared/blog-utils.ts, and that's the right place for it: the part of the system that decides whether retrieval is good enough should sit close to the code that evaluates it.

That flow is the real control surface. By the time the generator runs, a decision has already happened: is the evidence wide enough to trust?

Three query lanes, three different jobs

The fan-out isn't random, and it isn't a single blended prompt pretending to be a strategy. I built it as three separate lanes, because each one catches a different failure mode.

Curated highlight queries keep the system anchored in the kinds of features and systems I already know are worth revisiting. Those posts usually come out of places I've touched repeatedly: workflow orchestration, retrieval, caching, parsing, state management, security boundaries, or data transformation. They help the pipeline remember what's already interesting in the repository family.

The fixed query pool is the broadest lane. It's there to force coverage across architectural themes and implementation patterns instead of letting one topic family take over. This is the lane that goes looking for general system shape: event-driven flows, retrieval logic, prompt construction, orchestration, caching, retry paths, auth boundaries, model inference, ETL, and state machines. A selector living only inside the curated highlights would turn too self-referential, and one living only inside the fixed pool would turn too generic. Running both is what keeps the output grounded and varied.

Recent commit queries add the temporal dimension. They bias the selector toward what actually changed recently rather than letting the system settle into evergreen topics that no longer reflect the repository's current shape. That matters because the most obvious topic is often the wrong one once recent work has moved the architecture. A topic can be semantically relevant and still be stale in practice.

The reason for splitting the lanes is simple. No single lane is trusted to decide the topic alone. They feed the same retrieval pass, but for different reasons: one preserves editorial continuity, one broadens the architectural search, one keeps the system current. Merged, the three give me a candidate set that's much harder to fool with local similarity alone.

Why the dedupe key is repo plus file path

Once retrieval returns a pile of chunks, the next problem is repetition. Similarity search loves repetition. A single file can dominate a result set by surfacing multiple overlapping excerpts, especially when the file is dense or when several queries land in the same section of code. Let that through and the draft starts building itself around one artifact instead of one system.

Which is why file-path-aware dedupe carries so much weight here. I want repeated hits from the same repo and file path to collapse early. Five adjacent chunks all sounding relevant doesn't impress me if they're pointing at the same paragraph of the same file. What I care about is whether the sample spans distinct parts of the codebase.

Repo identity belongs in the key too. In a multi-repo setup, two chunks can look similar for completely unrelated reasons. Both might describe retrieval logic, or orchestration, or prompt shaping, while living in different systems that shouldn't count as interchangeable evidence. Repo plus file path tells me whether the sampling is broad or just rediscovering the same neighborhood under different search terms.

The practical effect is a retrieval layer that's less greedy. The first obvious cluster stops earning extra representation, and repeated evidence stops counting as coverage. Smaller candidate set. Considerably more trustworthy one.

There's a second-order benefit. Dedupe reduces the risk that a single implementation detail becomes the skeleton of the whole post. Without it, a draft can end up over-explaining one helper, one file, one branch of logic, purely because retrieval happened to hit it several times. That bias gets broken before the generator ever sees the prompt.

Measuring breadth instead of eyeballing it

After dedupe I don't ask whether the chunks feel diverse. I measure whether the sample is wide enough to support a post. That's the entire point of the breadth gate: reject candidate sets that are semantically plausible but structurally weak.

This is where checkRagSufficiency fits into the pipeline. The name is exactly what the behavior needs to be, a sufficiency check. If the retrieved set can't prove enough spread across the repository, it should not advance. The system should fail closed rather than guess.

What I like about a threshold is that it changes what retrieval means. Retrieval stops being a convenience layer that gathers whatever sits closest and becomes a gate that has to establish evidence quality before writing begins. The failure mode shifts from "draft written from a narrow slice" to "candidate rejected because the sample is too narrow." I'll take the second one every time.

That rejection path earns its keep. It catches the query pool landing too hard in one subsystem, recent changes dominating the semantic neighborhood, one file throwing off too many overlapping hits. Plenty of retrieval bugs look like success right until the prompt is assembled. The breadth check is the thing that stops them from becoming published text.

I like that the failures can produce something concrete, too. fetchFailureEvidence belongs in the same shared utility layer because it gives me a way to inspect why a candidate was rejected, which is useful during tuning. If a topic keeps failing breadth, I can see whether the cause is query bias, inadequate file-path diversity, or a retrieval window too small for the amount of material I want to cover.

The stage-compose step adds a second guardrail

File-path retrieval and semantic retrieval aren't competing systems. File-path retrieval gives me boundary-aware evidence; semantic retrieval gives me breadth across related concepts. In blog-stage-compose I merge both and dedupe a second time, since the two strategies can land on the same excerpt from different angles, and the evidence set should not regress back into repetition right before drafting.

Pinned excerpts hold the evidence in place

Once a candidate survives the coverage gate, I pin the excerpts that explain why the topic is worth writing about. Not a cosmetic step. It buys stability, because without pinned evidence the drafting stage has too much freedom to wander away from the exact chunks that earned the topic in the first place.

Pinned excerpts act like an anchor for the generation pass. They preserve the evidence trail, and they hold the prompt to the source material the draft was built from. That matters because the strongest failure mode in a retrieval-driven blog system isn't outright hallucination. It's the gradual slide: the draft starts from real evidence and then generalizes past what was actually retrieved.

Pinning the survivors makes that harder. The generation stage has to stay connected to the specific implementation details that passed the gate. Review gets easier too, since I can inspect exactly which chunks were considered important enough to carry forward.

Pinning and breadth checking together are what give the pipeline its shape. Breadth says the sample is wide enough. Pinning says these are the exact pieces that justify the topic. Between them, they stop the generator from inventing confidence the retrieval layer didn't earn.

Why I prefer rejection over a weak draft

I'm completely comfortable with a pipeline that says no. I want it to say no when the evidence is bad. A weak candidate shouldn't get rescued by a polished prompt, and if the retrieval set is narrow, rejection is the honest response.

That discipline keeps the blog output specific. It ties the writing to actual systems instead of generic patterns, and it saves me from editing around a draft that was born from a bad evidence shape. A rejected topic costs less time than a published post that looks right but misses the structure of the thing it claims to describe.

This matters more in a multi-repo environment. Once you have a handful of systems with overlapping concepts, retrieval can get too eager to collapse them into one theme. A good gate has to resist that collapse: respect repository boundaries, file boundaries, evidence density boundaries. If those boundaries aren't visible in the sample, the draft should not happen yet.

That's the philosophical change I stopped treating as optional. The system doesn't owe me a draft. It owes me a trustworthy sample, and that sample has to prove the topic is broad enough, current enough, and distinct enough to justify writing.

The actual win isn't creativity; it's control

What changed here wasn't my ability to generate prose. What changed was the quality of the evidence stack the prose starts from. The dispatcher fans out through curated highlights, a fixed query pool, and recent commit queries. The retrieval layer dedupes by stable keys, the sufficiency gate checks for breadth, and stage-compose merges file-path and semantic evidence before deduping a second time. Pinned excerpts hold the final prompt to what survived.

That sequence turns retrieval into a control system rather than a suggestion engine. It enforces a minimum standard before a draft is allowed to exist, and that's the kind of discipline a blog pipeline needs if it's going to write about real systems with real precision.

The result is fewer bad drafts, but also a pipeline that knows what it doesn't know early enough to stop itself. When the evidence is wide, the writing stage does the part it's good at. When the evidence is narrow, the system does the part I'm more grateful for. It refuses to pretend it knows.