When a Module Grows Past Readability: Finding the Seams in a 1150-Line Pipeline
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 Stage | Architecture Pattern | Step Ordering Visibility | Debuggability |
|---|---|---|---|
| Monolithic Function (1150 lines) | Interleaved functions sharing scope | Implicit (Hidden in 1150 lines of code) | Low (Step order bugs ship unnoticed) |
| Domain Pipeline Orchestrator | 11 Isolated modules + Explicit runner | 100% 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:
- Column Detection
- Underline Filtering
- Image Region Bounding
- Lattice Table Extraction
- Stream Table Extraction
- Box Bounding
- Header/Footer Filtering
- List Item Extraction
- Heading Classification
- Paragraph Block Grouping
- Divider Line Classification
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.