Images vanish when a reprocess path never cached its inputs
TLDR: Re-extracting an OCR-backed page dropped its images because the reprocess dispatcher used a cache as a proxy for "which engine produced this page," and the OCR engine never filled that cache. The bug class: routing work by cache membership when multiple producers exist.
The bug class
A worker has two extraction paths. Path A caches per-page inputs when it extracts. Path B extracts through a different engine and caches nothing. A reprocess message arrives and the dispatcher checks: is the page in the cache? If yes, run the cached-input handler. If no, run the vector handler. Every page from path B falls through to the vector handler, which re-reads the document with assumptions that only hold for path A's documents.
Why the architecture produces it
Caches are usually checked for performance, and "miss" is treated as "process from scratch." That is safe when one producer exists: a miss means "not yet processed." With two producers, a miss is ambiguous: it can mean "not yet processed" or "processed by the other engine." The dispatch collapsed both meanings into one branch.
The failure was silent because the wrong handler does not throw. It produces a plausible page with fewer images and worse layout. The result handler applies it, and only a human comparing before and after sees the loss.
The fix
Stop dispatching on cache membership. Dispatch on an explicit ownership flag set during pre-flight:
if (page.scanned && !page.ocrLayer) {
// produced by the OCR backend, local pipeline cannot re-run it
keepPage();
return;
}
if (page.scanned && page.ocrLayer) {
// local OCR bridge, synthetic inputs cached, safe to re-run
worker.post({ type: "reprocess", ... });
return;
}
worker.post({ type: "reprocess", ... }); // vector page
The flag pair distinguishes the three cases the cache could not. Verification: the scanned page kept its 3 inline images and its original html; a vector page reprocessed and kept all 5 of its images.
How to prevent the class
When a new extraction engine joins a pipeline, grep for every dispatch keyed on data the engine might not write. Cache membership, text-item counts, and segment counts are all proxies for "which engine did this," and every proxy breaks the moment a second engine stops writing that field. Replace proxies with an explicit producer or source field at the earliest decision point.
One lesson
An empty cache is evidence, not a decision. The question "is this page mine to re-process" must be answered by the producer's own record, never by the absence of a record.