Engineering Journal
Pdf Processor
Pdf Processor

Postmortem: The Ordering Bug That Only a Refactor Could Find

2026-05-31

TLDR

A severe layout parsing defect, where horizontal rules inside paragraph text generated spurious <hr> elements, shipped unnoticed for weeks because our 1,150-line classifier monolith concealed execution ordering. The bug was not discovered by algorithm analysis, but by splitting the monolith into 11 domain modules called sequentially by a top-level orchestrator. Exposing the pipeline sequence as an explicit function list made the misplaced execution step immediately visible.
Postmortem MetricMonolithic Procedural FileRefactored Domain Pipeline
File Structure1,150 lines in 1 file11 Domain modules + 1 Orchestrator
Step Ordering VisibilityImplicit (Hidden across 1,150 lines)100% Explicit (Visible call sequence)
Ordering Defect DetectionImpossible via code reviewImmediate visual audit in orchestrator

The incident: spurious <hr> rule injections

Our PDF extraction engine began rendering unexpected horizontal rules (<hr>) in the middle of paragraph blocks across specific test documents.

On documents lacking decorative lines, tests passed cleanly. On documents featuring decorative rules or underlined titles near body copy, paragraph sentences were split into separate blocks with inserted <hr> tags.


Root cause analysis: the implicit ordering fallacy

In the monolithic contextClassifier.js, new classification passes were added over several months by appending functions to the file:

[Line 100] Table Detection
[Line 450] Image Detection
[Line 620] Divider Line Detection  <-- Execution Step 9
[Line 910] Text Classification     <-- Execution Step 11

Divider detection (Line 620) performed a containment check against regions. At Line 620, regions contained only tables and images because paragraph classification (Line 910) had not yet executed.

Every line segment inside body paragraphs evaluated as "unclaimed", causing the classifier to generate false-positive section dividers.


Remediation: modular pipeline architecture

We dismantled the 1,150-line monolith, creating 11 isolated domain modules and an explicit 10-line top-level orchestrator:

// REFACTORED: Top-Level Pipeline Orchestrator
export function runDocumentClassificationPipeline(items, segments, viewport) {
  const regions = [];

// Pass 1: Tables & Images regions.push(...detectLatticeTables(segments, viewport)); regions.push(...detectImages(items, viewport));

// Pass 2: Text Classification (Paragraphs, Headings, Lists) MUST run first! const textRegions = classifyTextBlocks(items, viewport); regions.push(...textRegions);

// Pass 3: Dividers MUST run after textRegions exist const dividerRegions = detectDividers(segments, regions, viewport); regions.push(...dividerRegions);

return regions; }

By inspecting the 10-line orchestrator function, the re-ordering requirement (classifyTextBlocks before detectDividers) became self-evident.

Rule of thumb: Structure sequential data processing systems as explicit top-level orchestrator calls to make step-ordering invariants self-documenting and auditable.
Read this post in the full Engineering Journal →