Skip to main content

blazegraph_io_core/analytics/
builder.rs

1// Composite builder that drives the single-pass walk and the dependency-ordered
2// finalization. See
3// `docs/P2/core/design-flows/2026-04-28-document-analytics-and-header-footer-classification.md`.
4
5use serde::{Deserialize, Serialize};
6
7use crate::analytics::font::{FontStats, FontStatsBuilder};
8use crate::analytics::geometry::{GeometryStats, GeometryStatsBuilder};
9use crate::analytics::page_roles::{classify_page_roles, PageRolesConfig};
10use crate::analytics::page_stats::{PageStats, PageStatsBuilder};
11use crate::analytics::region::{RegionStats, RegionStatsBuilder};
12use crate::analytics::statistic::{FinalizationContext, Statistic};
13use crate::types::PdfTextElement;
14
15/// Composite builder that drives a single-pass walk over text elements,
16/// dispatching each element to all enabled stat kinds.
17///
18/// Construct with [`AnalysisBuilder::new`], call [`AnalysisBuilder::observe`]
19/// once per element, then [`AnalysisBuilder::finalize`] to obtain a
20/// [`DocumentAnalysis`].
21#[derive(Debug, Default)]
22pub struct AnalysisBuilder {
23    pub font: FontStatsBuilder,
24    pub geometry: GeometryStatsBuilder,
25    pub page_stats: PageStatsBuilder,
26    pub region: RegionStatsBuilder,
27}
28
29impl AnalysisBuilder {
30    /// Construct an empty builder ready to observe elements.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Dispatch an element to every enabled stat kind. Called once per element
36    /// in document reading order.
37    pub fn observe(&mut self, element: &PdfTextElement) {
38        self.font.observe(element);
39        self.geometry.observe(element);
40        self.page_stats.observe(element);
41        self.region.observe(element);
42    }
43
44    /// Finalize all stat kinds in dependency order and produce a
45    /// [`DocumentAnalysis`]. Order: `font → geometry → region → page_stats`.
46    /// Font has no cross-stat dependencies; geometry reads font; region
47    /// reads geometry; page_stats reads font, geometry, and region (it
48    /// attaches per-leaf `RegionSignature`s to the per-page Region trees).
49    pub fn finalize(self) -> DocumentAnalysis {
50        // Font has no dependencies.
51        let empty_ctx = FinalizationContext::default();
52        let font = self.font.finalize(&empty_ctx);
53
54        // Geometry depends on the finalized FontStats — its per-page footer
55        // walk reads the document-level body size instead of a fragile
56        // per-page median. See `find_per_page_footer_line` in geometry.rs
57        // for the rationale.
58        let geometry_ctx = FinalizationContext {
59            font: Some(&font),
60            geometry: None,
61            region: None,
62        };
63        let geometry = self.geometry.finalize(&geometry_ctx);
64
65        // RegionStats depends on GeometryStats (body box + column dividers).
66        let region_ctx = FinalizationContext {
67            font: Some(&font),
68            geometry: Some(&geometry),
69            region: None,
70        };
71        let region = self.region.finalize(&region_ctx);
72
73        // PageStats depends on font + geometry (heatmap) + region (the
74        // per-page Region trees its per-leaf signatures attach to).
75        let page_ctx = FinalizationContext {
76            font: Some(&font),
77            geometry: Some(&geometry),
78            region: Some(&region),
79        };
80        let mut page_stats = self.page_stats.finalize(&page_ctx);
81
82        // Page-roles classifier (Block 06) — analytics post-pass that
83        // assigns each PageSignature.role and the derived body_pages
84        // extent on PageStats. Runs unconditionally; downstream rules
85        // pull `body_start_page` / `body_end_page` for filtering.
86        classify_page_roles(&mut page_stats, &PageRolesConfig::default());
87
88        DocumentAnalysis {
89            font,
90            geometry,
91            page_stats,
92            region,
93        }
94    }
95}
96
97/// Composite output of the analytics pre-pass. Carries one finalized output
98/// per stat kind. Lives in pipeline memory; not serialized into the public
99/// graph output (a separate sidecar dump may serialize it for development
100/// purposes).
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
102pub struct DocumentAnalysis {
103    pub font: FontStats,
104    pub geometry: GeometryStats,
105    pub page_stats: PageStats,
106    pub region: RegionStats,
107}