Skip to content

Commit ce4328c

Browse files
dgunningclaude
andcommitted
fix(documents): log TOC analyzer failures instead of swallowing them silently (edgartools-hk9w)
The analyzer degraded silently on internal errors — blanket 'except Exception: pass' in _analyze_generic_toc, _extract_preceding_item_label, _infer_part_from_row_context, _anchor_matches_heading, and the module-level _find_toc_table_start, plus an unlogged agent-parser fallthrough. When an agent parser threw or found nothing, the analyzer dropped to the generic fallback (or returned {}) with no signal — exactly the blindness that let the GS/Citi section bugs hide. Every catch now emits logger.debug(..., exc_info=True) with context, matching the already-logged agent parsers (Workiva/DFIN/Novaworks/Toppan, body-header scan). The agent fallthrough logs which parser returned nothing before degrading to the generic scan. The one remaining narrow catch (except (ValueError, AttributeError): continue in the per-anchor loop) stays as-is — already typed, and per-anchor logging is noise. Tests: test_agent_fallthrough_logs (caplog asserts the fallthrough log names the parser) and test_no_blanket_silent_except (guard against any 'except Exception: pass' regressing). test_toc_agents 44 passed; broader TOC/section/corpus run 85 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4b247a4 commit ce4328c

2 files changed

Lines changed: 44 additions & 5 deletions

File tree

edgar/documents/utils/toc_analyzer.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ def analyze_toc_structure(self, html_content: str, agent: Optional[str] = None,
9494

9595
# Generic fallback for unknown agents or when agent-specific parser returns empty
9696
if not result:
97+
if agent:
98+
# The agent parser was tried and found nothing — make the
99+
# degradation to the generic scan observable (edgartools-hk9w).
100+
logger.debug("Agent parser %r returned no sections; "
101+
"falling back to generic TOC scan", agent)
97102
result = self._analyze_generic_toc(html_content, tree=tree)
98103

99104
# Body-header fallback: some filers (Goldman Sachs, Citi — large bank
@@ -212,8 +217,9 @@ def _analyze_generic_toc(self, html_content: str, tree=None) -> Dict[str, str]:
212217
section_mapping = self._build_section_mapping(toc_sections, tree=tree)
213218

214219
except Exception:
215-
# Return empty mapping on error - fallback to other methods
216-
pass
220+
# Degrade to other strategies, but record why the generic scan failed
221+
# so the silent-fallback path stays diagnosable (edgartools-hk9w).
222+
logger.debug("Generic TOC parser failed", exc_info=True)
217223

218224
return section_mapping
219225

@@ -951,7 +957,7 @@ def _extract_preceding_item_label(self, link_element) -> str:
951957
return part_match.group(1)
952958

953959
except Exception:
954-
pass
960+
logger.debug("Preceding-item-label extraction failed", exc_info=True)
955961

956962
return ''
957963

@@ -1015,6 +1021,7 @@ def _infer_part_from_row_context(self, link_element) -> Optional[str]:
10151021
prev = prev.getprevious()
10161022

10171023
except Exception:
1024+
logger.debug("Part inference from row context failed", exc_info=True)
10181025
return None
10191026

10201027
return None
@@ -1332,7 +1339,7 @@ def _anchor_matches_heading(self, tree, anchor_id: str, expected_name: str) -> b
13321339
if item_pattern in el_text:
13331340
return True
13341341
except Exception:
1335-
pass
1342+
logger.debug("Anchor/heading match check failed", exc_info=True)
13361343

13371344
return False
13381345

@@ -1475,6 +1482,6 @@ def _find_toc_table_start(html_content: str) -> int:
14751482
return pos
14761483

14771484
except Exception:
1478-
pass
1485+
logger.debug("TOC table-start scan failed", exc_info=True)
14791486

14801487
return -1

tests/test_toc_agents.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,35 @@ def test_cat_item_1d_is_canonical_and_correct(self):
678678
canon = TOCAnalyzer._CANONICAL_ITEM_KEY
679679
assert all(canon.match(k) for k in secs.keys()), \
680680
f"non-canonical: {[k for k in secs if not canon.match(k)]}"
681+
682+
683+
class TestSilentFailureObservability:
684+
"""The TOC analyzer must not swallow failures silently (edgartools-hk9w):
685+
the agent-parser fallthrough and internal errors emit debug logs so the
686+
degradation path is diagnosable."""
687+
688+
def test_agent_fallthrough_logs(self, caplog):
689+
"""When a named agent parser finds nothing and we degrade to the generic
690+
scan, a debug log identifies which parser fell through."""
691+
import logging
692+
analyzer = TOCAnalyzer(form='10-K')
693+
# HTML with no Workiva TOC structure -> _analyze_workiva_toc returns {} ->
694+
# fallthrough to generic.
695+
html = "<html><body><p>No table of contents here.</p></body></html>"
696+
with caplog.at_level(logging.DEBUG, logger='edgar.documents.utils.toc_analyzer'):
697+
analyzer.analyze_toc_structure(html, agent='Workiva')
698+
assert any('Workiva' in r.message and 'generic' in r.message.lower()
699+
for r in caplog.records), \
700+
f"no fallthrough log; records={[r.message for r in caplog.records]}"
701+
702+
def test_no_blanket_silent_except(self):
703+
"""Guard: no 'except Exception:' in the analyzer is immediately followed by
704+
a bare 'pass' — every catch logs or is a narrowed typed catch."""
705+
import re
706+
from pathlib import Path
707+
import edgar.documents.utils.toc_analyzer as mod
708+
src = Path(mod.__file__).read_text().splitlines()
709+
offenders = [i + 1 for i, line in enumerate(src)
710+
if line.strip() == 'except Exception:'
711+
and src[i + 1].strip() == 'pass']
712+
assert not offenders, f"silent 'except Exception: pass' at lines {offenders}"

0 commit comments

Comments
 (0)