@@ -605,34 +605,158 @@ func countLeafSections(sections []Section) int {
605605 return n
606606}
607607
608- // capLeafSections enforces a ceiling on the total leaf-section count.
609- // While the count exceeds maxLeaves it repeatedly merges the two
610- // smallest ADJACENT leaf siblings under whichever parent currently has
611- // the most leaf children — so the runaway byte-split sections collapse
612- // back first, while genuinely distinct top-level sections are left
613- // alone. maxLeaves <= 0 disables the cap.
608+ // capLeafSections enforces a ceiling on the total leaf-section count so a
609+ // pathological PDF can't shatter into thousands of tiny leaves (each of
610+ // which costs a summarize + HyDE + multi-axis LLM call at ingest, which
611+ // is what throttles/stalls the full pipeline). maxLeaves <= 0 disables
612+ // the cap; a tree already at or under the cap is returned untouched.
614613//
615- // Merged leaves concatenate their content (blank-line separated), keep
616- // the first sibling's title, and union their page ranges. Table
617- // sections are attached AFTER this pass (attachTableSections), so their
618- // numeric content is never merged away.
614+ // The reduction is robust to ANY tree shape, which is the fix for the
615+ // real 10-K explosion: that document is not a flat list of adjacent
616+ // leaves but hundreds of SINGLE-LEAF PARENTS (heading -> one body leaf),
617+ // which have no adjacent leaf-sibling pairs at all — so the old
618+ // adjacent-only merge silently did nothing and a 92-page filing sailed
619+ // past the cap at 463-1465 leaves. We fix it in two phases:
620+ //
621+ // 1. collapseSingleLeafParents flattens every heading -> lone-leaf chain
622+ // so the formerly-only-children become adjacent leaf SIBLINGS. This
623+ // restructures the tree without changing the leaf count (the parent
624+ // absorbs the child and itself becomes the leaf).
625+ //
626+ // 2. The merge loop then repeatedly merges the smallest adjacent leaf
627+ // pair until the count is back under the cap (a defensive collapse
628+ // step covers any pair a table leaf blocked).
629+ //
630+ // The invariant: for any tree with > maxLeaves MERGEABLE leaves,
631+ // capLeafSections drives countLeafSections to <= maxLeaves. The only
632+ // leaves it will not merge are table sections (Metadata["table"]=="true")
633+ // — those carry distinct numeric content and must survive verbatim. In
634+ // the normal parse flow table sections are attached AFTER this pass
635+ // (attachTableSections), so the cap never sees them; the guard is
636+ // defensive for any caller that pre-attaches tables.
637+ //
638+ // Merged/collapsed leaves concatenate their content (blank-line
639+ // separated), keep the parent/first-sibling title, and union their page
640+ // ranges, so no body text is ever dropped.
619641func capLeafSections (sections []Section , maxLeaves int ) []Section {
620642 if maxLeaves <= 0 {
621643 return sections
622644 }
623- // Guard against pathological loops: at most one merge per excess leaf.
624- for guard := 0 ; countLeafSections (sections ) > maxLeaves && guard < 100000 ; guard ++ {
625- if ! mergeOneSmallestAdjacentLeafPair (sections ) {
626- break // no mergeable adjacent leaf pair anywhere
645+ if countLeafSections (sections ) <= maxLeaves {
646+ return sections // already under the cap — leave structure untouched
647+ }
648+
649+ // Wrap the top-level sections under a synthetic root. The merge step
650+ // shrinks a sibling list in place (it rewrites parent.Children), which
651+ // only propagates back to the caller when the mutated list is a struct
652+ // FIELD, not a bare slice parameter. The runaway shape we're fixing —
653+ // hundreds of single-leaf PARENTS — needs the TOP-level list to shrink,
654+ // so we make the top level a nested list (root.Children) and operate on
655+ // that; the wrapper itself (length 1) never changes. We return
656+ // root.Children at the end.
657+ root := []Section {{Children : sections }}
658+
659+ // Phase 1: flatten single-leaf-parent chains so only-children become
660+ // mergeable siblings. Count is unchanged; structure is normalised.
661+ root [0 ].Children = collapseSingleLeafParents (root [0 ].Children )
662+
663+ // Phase 2: merge adjacent leaf pairs (smallest first) until under cap.
664+ // Each merge removes one leaf and each fallback collapse removes one
665+ // internal node — both strictly monotonic — so the loop is bounded;
666+ // the guard is belt-and-braces against a logic regression.
667+ for guard := 0 ; countLeafSections (root ) > maxLeaves && guard < 1000000 ; guard ++ {
668+ if mergeOneSmallestAdjacentLeafPair (root ) {
669+ continue
670+ }
671+ // No adjacent mergeable pair left. Try to free one up by
672+ // collapsing a remaining single-leaf parent; if that's impossible
673+ // too, every remaining leaf is unmergeable (e.g. table sections)
674+ // and we stop — there is nothing left we're permitted to merge.
675+ if ! collapseOneSingleLeafParent (& root ) {
676+ break
677+ }
678+ }
679+ return root [0 ].Children
680+ }
681+
682+ // isTableLeaf reports whether s is a table section that must never be
683+ // merged or collapsed (its cell content is distinct numeric data).
684+ func isTableLeaf (s * Section ) bool {
685+ return s .Metadata != nil && s .Metadata ["table" ] == "true"
686+ }
687+
688+ // collapseSingleLeafParents recursively flattens every parent that has
689+ // exactly one child which is a (non-table) leaf: the parent absorbs the
690+ // child's content + page range and itself becomes the leaf. Chains
691+ // (heading -> subheading -> body) collapse fully in one bottom-up pass.
692+ //
693+ // Leaf count is preserved (one internal node + one leaf become one leaf);
694+ // the point is purely to turn only-children into adjacent siblings so the
695+ // adjacent-pair merge can then reduce the count. Returns the rewritten
696+ // slice.
697+ func collapseSingleLeafParents (sections []Section ) []Section {
698+ for i := range sections {
699+ s := & sections [i ]
700+ if len (s .Children ) == 0 {
701+ continue
702+ }
703+ // Bottom-up: collapse within the children first so a chain folds
704+ // from the leaf upward.
705+ s .Children = collapseSingleLeafParents (s .Children )
706+ if len (s .Children ) == 1 && len (s .Children [0 ].Children ) == 0 && ! isTableLeaf (& s .Children [0 ]) {
707+ absorbChildIntoParent (s , s .Children [0 ])
708+ s .Children = nil
627709 }
628710 }
629711 return sections
630712}
631713
714+ // collapseOneSingleLeafParent finds the first parent in the tree with
715+ // exactly one (non-table) leaf child and collapses it (parent absorbs the
716+ // leaf, becomes a leaf). Returns false when no such parent exists. Used
717+ // as a defensive fallback inside the merge loop when an adjacent pair was
718+ // blocked by a table leaf; the bulk flattening is done up front by
719+ // collapseSingleLeafParents.
720+ func collapseOneSingleLeafParent (sections * []Section ) bool {
721+ s := * sections
722+ for i := range s {
723+ n := & s [i ]
724+ if len (n .Children ) == 1 && len (n .Children [0 ].Children ) == 0 && ! isTableLeaf (& n .Children [0 ]) {
725+ absorbChildIntoParent (n , n .Children [0 ])
726+ n .Children = nil
727+ return true
728+ }
729+ if len (n .Children ) > 0 {
730+ if collapseOneSingleLeafParent (& n .Children ) {
731+ return true
732+ }
733+ }
734+ }
735+ return false
736+ }
737+
738+ // absorbChildIntoParent folds a leaf child's content and page range up
739+ // into its parent. The parent keeps its own title (the heading) and
740+ // gains the child's body; an empty parent body is replaced outright so we
741+ // don't prefix a stray separator. Page ranges union (min start, max end).
742+ func absorbChildIntoParent (parent * Section , child Section ) {
743+ switch {
744+ case strings .TrimSpace (parent .Content ) == "" :
745+ parent .Content = child .Content
746+ case strings .TrimSpace (child .Content ) != "" :
747+ parent .Content = parent .Content + "\n \n " + child .Content
748+ }
749+ parent .PageStart = minNonZero (parent .PageStart , child .PageStart )
750+ if child .PageEnd > parent .PageEnd {
751+ parent .PageEnd = child .PageEnd
752+ }
753+ }
754+
632755// mergeOneSmallestAdjacentLeafPair finds the adjacent leaf-sibling pair
633756// with the smallest combined content length anywhere in the tree and
634- // merges it in place. Returns false when no sibling list has two
635- // adjacent leaves to merge.
757+ // merges it in place. Only pairs where BOTH siblings are NON-table leaves
758+ // are eligible — table sections are never merged. Returns false when no
759+ // sibling list has two adjacent mergeable leaves.
636760func mergeOneSmallestAdjacentLeafPair (sections []Section ) bool {
637761 bestList := (* []Section )(nil )
638762 bestIdx := - 1
@@ -642,7 +766,8 @@ func mergeOneSmallestAdjacentLeafPair(sections []Section) bool {
642766 walk = func (list * []Section ) {
643767 s := * list
644768 for i := 0 ; i + 1 < len (s ); i ++ {
645- if len (s [i ].Children ) == 0 && len (s [i + 1 ].Children ) == 0 {
769+ if len (s [i ].Children ) == 0 && len (s [i + 1 ].Children ) == 0 &&
770+ ! isTableLeaf (& s [i ]) && ! isTableLeaf (& s [i + 1 ]) {
646771 size := len (s [i ].Content ) + len (s [i + 1 ].Content )
647772 if bestSize < 0 || size < bestSize {
648773 bestSize , bestList , bestIdx = size , list , i
0 commit comments