Engineering Journal
Pdf Processor
Pdf Processor

When a Module Grows Past Readability: Finding the Seams in a 1150-Line Pipeline

2026-05-31

TLDR

A monolithic 1,150-line document classifier file containing 11 interleaved detection responsibilities conceals step-ordering bugs behind implicit shared mutable closures. Refactoring the monolithic function into 11 single-responsibility domain modules managed by an explicit pipeline orchestrator makes step execution order auditable, surfacing order-dependent bugs directly in code review.
Refactoring StageArchitecture PatternStep Ordering VisibilityDebuggability
Monolithic Function (1150 lines)Interleaved functions sharing scopeImplicit (Hidden in 1150 lines of code)Low (Step order bugs ship unnoticed)
Domain Pipeline Orchestrator11 Isolated modules + Explicit runner100% Explicit (Visible call sequence)High (Unit-testable domain modules)

Problem statement: the entangled classifier monolith

Our contextClassifier.js module grew to 1,150 lines, handling 11 distinct jobs in a single procedural function:

  1. Column Detection
  2. Underline Filtering
  3. Image Region Bounding
  4. Lattice Table Extraction
  5. Stream Table Extraction
  6. Box Bounding
  7. Header/Footer Filtering
  8. List Item Extraction
  9. Heading Classification
  10. Paragraph Block Grouping
  11. Divider Line Classification
Because all 11 passes mutated shared local arrays in a single scope, step-ordering bugs remained invisible for weeks.


Technical failure mode: implicit order-dependent bugs

Divider detection ran at step 9, executing a containment check: "Is this horizontal line segment already inside a known text region?"

Because paragraph and list classification did not run until step 11, the regions array contained only tables and images at step 9.

Every horizontal rule inside body copy passed the containment check as "unclaimed", causing the classifier to insert spurious <hr> elements mid-paragraph across test documents.


The refactored orchestration architecture

Split the monolith into 11 single-responsibility detector modules and expose an explicit call sequence in a lightweight pipeline orchestrator:

// REFACTORED: Explicit Pipeline Orchestrator
import { detectPageColumns } from './detectors/columnDetector.js';
import { detectLatticeTables } from './detectors/latticeDetector.js';
import { detectImages } from './detectors/imageDetector.js';
import { classifyTextBlocks } from './detectors/textClassifier.js';
import { detectDividers } from './detectors/dividerDetector.js';

export function classifyPageDocument(textItems, lineSegments, viewport, scale) { const regions = [];

// Step 1: Detect page columns const columns = detectPageColumns(textItems, viewport, scale);

// Step 2: Extract lattice & stream tables const latticeTables = detectLatticeTables(lineSegments, viewport); regions.push(...latticeTables);

// Step 3: Extract image regions const images = detectImages(textItems, viewport); regions.push(...images);

// Step 4: Classify text blocks (Paragraphs, Headings, Lists) MUST run before Dividers! const textBlocks = classifyTextBlocks(textItems, columns, viewport, scale); regions.push(...textBlocks);

// Step 5: Detect Dividers (MUST run after textBlocks exist so containment check succeeds) const dividers = detectDividers(lineSegments, regions, viewport); regions.push(...dividers);

return sortRegionsByReadingOrder(regions); }

Rule of thumb: Decompose procedural monoliths into single-responsibility domain modules invoked by an explicit pipeline orchestrator to make step-ordering dependencies visible.
Read this post in the full Engineering Journal →