Skip to content
andrew.dunn.dev

When Retrieval Is Not the Question

The last post retired two tools and made a two-layer argument: artifacts and transcripts are both corpora, and both need indexing, querying, and a model of their own decay. The transcript half became varve. The artifact half had a concrete failure to answer for: confident answers assembled from facts that were no longer true, with nothing in the system positioned to notice. The rebuild had one question to settle: could an index that watches its own decay delete that class of wrong answer, without giving up the retrieval quality the original benchmark work had bought?

The Decay Report

Retrieval was the part I knew how to build. What gapvec never had was any notion of what to phase out, and that turned out to be the part that mattered.

A stale chunk retrieves exactly as well as a fresh one. Embedding similarity has no opinion about whether a fact is still true, so a confident answer built from a passage that was accurate two years ago is indistinguishable from a correct one. The original pipeline found temporal blindness in ranking and fixed it with a date pre-filter. The deeper version is temporal blindness about truth, and no amount of ranking work touches it.

What replaced the old tools is a local index with a maintenance organ: a decay report that runs without being asked a query. It reads six signals, all from dates parsed out of the content itself rather than file timestamps: facts past their declared time-to-live, the raw capture lane disagreeing with the curated one, event horizons falling due in the next sixty days, prose whose declared dependencies have since moved, durable notes gone stale, and parse failures held in quarantine. It emits a worklist, and it never archives anything on its own.

Two behaviors came out of that which I did not anticipate.

1 · INGESTEVERY FILE IS A SOURCEkeeps or re-embedsCORPUScorpus filesread-onlySTEPingestparse·chunk·keyTABLEsource registrypath·sha256·mtimeGATEreuse gatebyte-identical2 · TWO LANES IN ONE SQLITE FILEdepends_onPROSEchunks, each with a decay classprose lanefull-textFTS5·BM25·portervectorssqlite-vec·384-dembedding model pinned in the dbFACTS(entity, field)·TTL·valid-as-offact lanenever embeddedcuratedhand-maintainedcapturedsystem of recordallowed to disagree: that is the signal3 · QUERY TIMENO ROUTER WAS BUILTtwo indexesverbtop 20CALLERthe calling modelfact lane or retrievalFUSERRF fuse · k=60recency breaks tiesRESULTtop-20 + staleness notesthe caller’s pick reranks4 · MAINTENANCE · SIX SIGNALSNO QUERY BEHIND IT · NOTHING AUTO-APPLIEDemitsdivergedecay reportdates from content, not mtimepast-TTL factscapture vs curatedevent horizons, 0-60ddependency movedstale durable prosequarantined parse failuresworklistemitted, not appliedcontentionranked: recency, then corroboration

One SQLite file carries the whole pipeline. Every corpus file becomes a registry row and that row decides reuse, prose is indexed twice while facts are keyed and never embedded, the calling model picks the verb and its pick from the top twenty is the rerank, and the decay report runs with no query behind it and retires nothing on its own.

Some Questions Are Not Retrieval Questions

A question about a volatile business attribute, a current value that changes on a schedule, has exactly one right answer sitting in a structured field. Chunk search will hand you three passages asserting three values from three different years and let the synthesis model choose. Those fields live in a fact lane that is never embedded, exposed to the calling model as its own explicit verb beside retrieval. The intent classifier I expected to need here was never built: the caller is already a language model reading two tool descriptions, and it picks the right one. Reserving retrieval for genuinely discursive questions deleted a whole category of confidently wrong answers. Roughly 220 fields resolve this way now.

Where the Two Lanes Disagree

Every fact key carries two provenance lanes at once: a curated, hand-maintained value and a raw one captured from the system of record. Where the lanes agree, they collapse to one clean answer. Where they disagree, the candidates are ranked by recency and then by corroboration, a count of how many sources assert each value, and shown with their dates; the system never picks. Forty-two pairs are genuinely contended, and each is a case where I would rather be asked than have the system guess. The same forty-two surface independently in the decay report’s divergence signal, which is a small, reassuring property: two detectors built for different jobs agree about where the corpus argues with itself.

What I Measured

The maintenance work cost retrieval nothing, and the measurement that shows it is not the recall figure. Recall@5 is 100% for both the index and a plain lexical sweep, so recall was never the differentiator. The difference is shape: the lexical fallback returns a median of 391 files per query, unranked, while the index puts the answer at rank one to five with staleness annotations attached. The regression gate is 108 questions authored against the artifact corpus, scored after the fact lane and the incremental rebuild landed.

REGRESSION GATE / 108 AUTHORED QUESTIONSSHAPE / THE LEAD MEASUREMENTlexical fallback, files returned per querymedian 391, unrankedindex, where the answer landsrank 1-5, staleness annotatedGATE SCORESrecall@5, index and fallback alike100%MRR, mean reciprocal rank0.979missed where lexical hit0 of 108REBUILD / UNCHANGED CORPUSbefore content-hash reuse96safter, every vector reused1.5ssame corpus, nothing re-embedded

Recall does not separate the two retrievers, since both reach 100%. Shape does: 391 unranked files against an answer at rank one to five with staleness attached, scored after the fact lane and the incremental rebuild landed.

The rebuild measurement started as a performance change and turned into a bug find. Research chunks are parsed out of the document, but only the manifest was registered as a tracked source, so every chunk re-embedded on every build no matter what had changed. The incremental work exposed it because reuse was impossible while the inputs looked new every time. Making each chunk’s own file a source fixed both at once.

The honest limitation carries over from the last post: those 108 questions are still authored, not mined. A better set than fifteen, and the same species of set.

What Comes Next

The decay report’s thresholds are guesses. Calibrating them means a quarter of watching what it flags against what I actually retire.

There is a cheaper signal I am not using yet. The index is queried from inside harness sessions, and those sessions are what varve indexes, so every correction I make to a value the index handed back is already on disk, attributed to a date, a session, and a fact key. That is a real-use error rate for the artifact index at no additional instrumentation cost. The 108 authored questions are the only other measurement I have.

Three things I want out of that query: which fact keys draw the most corrections, which topics the decay signals are missing entirely, and whether the correction rate moves once the report starts driving retirement. I have not written it yet.