When figure_caption_candidate blocks lack explicit marker_type or caption_text in the strict inventory, _normalize_bucket now falls back to the structured block's text and infers figure_number/marker_type from the text content. This ensures candidate captions propagate through the reader-figure pipeline to the renderer and health summary.
_index_structured_blocks used block_id as dict key, but PaddleOCR assigns
the same block_id for blocks on different pages. When A8E7SRVS page 6
block_id=5 (Fig.5) overwrites page 5 block_id=5 (Fig.1), reader output
rendered Figure 5 with Fig.1 caption text.
Add _index_blocks_by_page_and_id using (page, block_id) tuples for
page-aware lookup in _normalize_bucket. Keep serialized block_index
clean (plain int|str keys) to preserve JSON compatibility.
Add regression test for cross-page block_id collision.
- Move consumed_caption_block_ids skip to top of block loop (before role
dispatch) so body_paragraph / backmatter_body blocks with consumed
captions are also skipped, not just figure_caption blocks.
- Create _emit_page_objects() helper to eliminate 4 duplicated page-
emission loops. Reader figures are primary: when present, legacy
matched_figures and unresolved_clusters are skipped for that page.
- Add tests: consumed-caption dedupe for body_paragraph, reader-figures-
preferred-over-legacy-matched_figures.
Preserve gate-critical fields in _normalize_bucket():
- marker_type: fall back to source_item['marker_type'] when marker_signature is absent
- strict_reject, linked_legend_block_id, cluster_area_ratio, width_ratio, height_ratio, media_block_count: carry through from source_item
Fixes 6 failing tests that relied on these fields surviving normalization.
- _stable_reader_figure_id: deterministic ID generation for reader figures
(figure_number-based or visual_group fallback with page+asset)
- synthesize_reader_figures: shell that normalizes inventory, materializes
reader figures with separate reader_status (RESOLVED/GROUPED_APPROXIMATE/
DEFERRED/CAPTION_ONLY/ORPHAN_ASSETS) and strict_status (preserved from
source bucket)
- _materialize_reader_figure: maps normalized items to reader figure dicts
- ReaderCoverage dataclass extended with as_dict()
- Tests: status separation, stable ID generation, normalization round-trip
Add figure legend completeness check ensuring every numbered formal
legend lands in an explicit outcome bucket (matched/held/ambiguous/
unresolved_cluster/unmatched). No legend may disappear without inventory
explanation.
Changes:
- Add compute_figure_legend_completeness() to ocr_figures.py
- Integrate completeness into build_figure_inventory() return value
- Add figure_legend_completeness fields to health report (ocr_health.py)
- Gaps degrade overall health status and add degraded reasons
- Add 9 new tests for completeness edge cases
_exclude_tail_nonref_from_body_flow and
_exclude_frontmatter_side_from_body_flow now resolve effective_role
from seed_role when role is 'unassigned' (post-Task-2 state).
Excluded blocks also update seed_role to prevent resolve_final_role
from overriding the exclusion.
Regression from Task 2's seed_role split: exclusion functions ran
before resolve_final_role and saw 'unassigned' instead of
'body_paragraph', so tail non-ref content leaked into body.