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(®ion_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(®ion),
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}