Reprocessing is only valid when the pipeline owns the input
TLDR: Re-processing a page is only safe when the pipeline that runs the re-process is the same pipeline that owns the document. When a scanned document is extracted by an OCR backend and then "re-extracted" by a local vector-geometry pipeline that never read that document, the re-run silently destroys content that the first run got right. The fix is a guard on the reprocess entry point, not a repair of the degraded output.
The problem class
Every extraction tool exposes a "re-extract" affordance: the user changed a parameter (skip this layer, adjust a threshold, move a column split) and wants the page rebuilt. The naive contract is "re-run the extractor with the new parameters."
That contract has a hidden precondition. A re-run only improves a page when the extractor that runs the re-run is the same extractor that produced the page, or at least a pipeline that can read the same substrate. If the first extraction came from a different engine with a different substrate model, the re-run is not a refinement. It is a fresh, worse interpretation of the same document, and it replaces a good result with a bad one.
The naive approach
Model extraction as a pure function: pageHtml = extract(bytes, page, params). Re-extraction is then just calling the function again with new parameters and swapping the output.
function reextract(page) {
worker.post({ type: "reprocess", page, pipeline: params });
}
The result handler applies whatever html comes back. No questions asked about where the current html came from.
Why it fails
Extraction pipelines are not interchangeable implementations of one function. They consume different substrates:
- A vector pipeline reads the PDF's text items, vector segments, and image XObjects. It needs a substrate to classify.
- An OCR pipeline renders the page to a bitmap and reads pixels. It needs no vector substrate at all.
- A backend OCR service (table detection, layout zones) produces rich html with inline image data.
The re-run was not idempotent. It was destructive, because the pipeline that re-ran did not own the input.
The better model
The reprocess entry point must know, per page, which engine produced that page. Reprocessing is only offered when the local pipeline can actually serve it. Three cases fall out:
const pgInfo = analysis.pages?.[currentPage];
// Page has no vector substrate for this pipeline. if (pgInfo?.scanned && !pgInfo?.ocrLayer) { keepCurrentPage(); explainWhy(); return; }
// Page came from the local OCR bridge, which cached its synthetic // inputs in the worker. The local pipeline CAN re-run it. if (pgInfo?.scanned && pgInfo?.ocrLayer) { worker.post({ type: "reprocess", page, pipeline: params }); return; }
// Ordinary vector page. Re-run freely. worker.post({ type: "reprocess", page, pipeline: params });
The discriminator is not "can the pipeline produce something" (it always can, and usually something bad). It is "does the pipeline own the inputs for this page." Ownership is tracked as a per-page flag set during pre-flight classification, before any engine runs.
Implementation evidence
The pipeline parameters (skip layers, thresholds, splits) were meaningless for the OCR-extracted page anyway: the backend service that produced it ignores them. So the honest re-extract on a scanned page is to keep the current page and tell the user why. The toast reads: "Scanned page, images are OCR-extracted and cannot be re-extracted by the geometry pipeline. The page is kept as-is."
The guard had to sit at the entry point, not the result handler, for one reason: the result is already garbage by the time it arrives. Intercepting a bad result to keep the old html is possible, but then the region metadata, the canvas overlay, and the undo stack all diverge. Guarding before the request means nothing downstream needs to be rolled back.
Verified in a real browser on both sides of the guard:
Scanned doc, page 1: before 3 inline images, len 60217
after 3 inline images, len 60217 (kept, toast shown)
Vector doc, page 11: before 5 inline images, len 850943 after 5 inline images, len 852003 (reprocessed, kept)
The vector page reprocessed and produced a fresh result with all five images. The scanned page was untouched. Both behaviors are correct.
Tradeoffs
The guard gives up "re-extraction" as a universal affordance. A scanned page cannot be re-pipelined locally, and that is the correct outcome: the parameters do not apply, and the alternative was silent content loss. The local OCR path (a scanned page whose synthetic inputs are cached in the worker) still re-runs and honors the parameters, so the affordance is not gone, it is routed.
The cost is a new per-page flag contract between pre-flight and the panel. It is one boolean, and it is set where the substrate is already measured, so it adds no second measurement of the document.