use crate::emcp::EpistemicTaint;
use crate::ingest_provenance::IngestProvenance;
pub const MAX_PAGES: u32 = 4096;
pub const MAX_MEGAPIXELS: u32 = 256;
pub const MAX_SPANS: usize = 1_000_000;
#[derive(Debug, Clone, Copy)]
pub struct ExtractionBounds {
pub max_pages: u32,
pub max_megapixels: u32,
pub max_spans: usize,
}
impl Default for ExtractionBounds {
fn default() -> Self {
ExtractionBounds { max_pages: MAX_PAGES, max_megapixels: MAX_MEGAPIXELS, max_spans: MAX_SPANS }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BBox {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
impl BBox {
pub fn is_valid(&self) -> bool {
self.x >= 0.0
&& self.y >= 0.0
&& self.w >= 0.0
&& self.h >= 0.0
&& self.x + self.w <= 1.0 + f64::EPSILON
&& self.y + self.h <= 1.0 + f64::EPSILON
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExtractionHint {
pub format: Option<String>,
pub transform: Option<String>,
pub target_field: Option<String>,
pub language: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtractedSpan {
pub text: String,
pub confidence: f64,
pub page: u32,
pub bbox: BBox,
}
impl ExtractedSpan {
pub fn new(text: impl Into<String>, confidence: f64, page: u32, bbox: BBox) -> Self {
let confidence = if confidence.is_nan() { 0.0 } else { confidence.clamp(0.0, 1.0) };
ExtractedSpan { text: text.into(), confidence, page, bbox }
}
pub const fn provenance(&self) -> IngestProvenance {
IngestProvenance::Inferred
}
pub fn epistemic_ceiling(&self) -> &'static str {
IngestProvenance::Inferred.epistemic_ceiling()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExtractionResult {
pub engine: String,
pub engine_version: String,
pub spans: Vec<ExtractedSpan>,
}
impl ExtractionResult {
pub const fn taint(&self) -> EpistemicTaint {
EpistemicTaint::Untrusted
}
pub const fn provenance(&self) -> IngestProvenance {
IngestProvenance::Inferred
}
pub fn mean_confidence(&self) -> f64 {
if self.spans.is_empty() {
return 0.0;
}
self.spans.iter().map(|s| s.confidence).sum::<f64>() / self.spans.len() as f64
}
pub fn page_count(&self) -> u32 {
self.spans.iter().map(|s| s.page).max().map_or(0, |m| m + 1)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtractionError {
NoEngineConfigured,
PageCapExceeded(u32),
PixelCapExceeded(u32),
SpanCapExceeded(usize),
EngineFailed(String),
DecodeFailed(String),
}
impl std::fmt::Display for ExtractionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use ExtractionError::*;
match self {
NoEngineConfigured => write!(
f,
"extraction.no_engine_configured — no extraction engine is wired for this \
transform; refusing rather than inventing content"
),
PageCapExceeded(n) => write!(f, "page_cap_exceeded — {n} pages (max {MAX_PAGES}) — refused"),
PixelCapExceeded(n) => {
write!(f, "pixel_cap_exceeded — {n} megapixels (max {MAX_MEGAPIXELS}) — refused")
}
SpanCapExceeded(n) => write!(f, "span_cap_exceeded — {n} spans (max {MAX_SPANS}) — refused"),
EngineFailed(e) => write!(f, "extraction engine failed: {e} — typed refusal, never fabricated"),
DecodeFailed(e) => write!(f, "decode_failed — {e}"),
}
}
}
impl std::error::Error for ExtractionError {}
impl ExtractionError {
pub fn slug(&self) -> &'static str {
use ExtractionError::*;
match self {
NoEngineConfigured => "extraction.no_engine_configured",
PageCapExceeded(_) => "page_cap_exceeded",
PixelCapExceeded(_) => "pixel_cap_exceeded",
SpanCapExceeded(_) => "span_cap_exceeded",
EngineFailed(_) => "extraction.engine_failed",
DecodeFailed(_) => "decode_failed",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SpanDisposition {
Believed(ExtractedSpan),
Quarantined { span: ExtractedSpan, floor: f64 },
}
impl SpanDisposition {
pub fn is_believed(&self) -> bool {
matches!(self, SpanDisposition::Believed(_))
}
}
pub fn apply_confidence_floor(spans: &[ExtractedSpan], floor: f64) -> Vec<SpanDisposition> {
let floor = if floor.is_nan() { 1.0 } else { floor.clamp(0.0, 1.0) };
spans
.iter()
.cloned()
.map(|span| {
if span.confidence + f64::EPSILON >= floor {
SpanDisposition::Believed(span)
} else {
SpanDisposition::Quarantined { span, floor }
}
})
.collect()
}
pub fn believed_text(dispositions: &[SpanDisposition]) -> String {
dispositions
.iter()
.filter_map(|d| match d {
SpanDisposition::Believed(s) => Some(s.text.as_str()),
SpanDisposition::Quarantined { .. } => None,
})
.collect::<Vec<_>>()
.join(" ")
}
pub trait ExtractionEngine: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn extract(
&self,
bytes: &[u8],
hint: &ExtractionHint,
bounds: &ExtractionBounds,
) -> Result<ExtractionResult, ExtractionError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoEngine;
impl ExtractionEngine for NoEngine {
fn name(&self) -> &str {
"none"
}
fn version(&self) -> &str {
"0"
}
fn extract(
&self,
_bytes: &[u8],
_hint: &ExtractionHint,
_bounds: &ExtractionBounds,
) -> Result<ExtractionResult, ExtractionError> {
Err(ExtractionError::NoEngineConfigured)
}
}
use std::sync::{Arc, OnceLock, RwLock};
fn registry() -> &'static RwLock<Option<Arc<dyn ExtractionEngine>>> {
static REG: OnceLock<RwLock<Option<Arc<dyn ExtractionEngine>>>> = OnceLock::new();
REG.get_or_init(|| RwLock::new(None))
}
pub fn register_engine(engine: Arc<dyn ExtractionEngine>) {
*registry().write().expect("extraction registry poisoned") = Some(engine);
}
pub fn clear_engine() {
*registry().write().expect("extraction registry poisoned") = None;
}
pub fn active_engine() -> Option<Arc<dyn ExtractionEngine>> {
registry().read().expect("extraction registry poisoned").clone()
}
pub fn run_active(
bytes: &[u8],
hint: &ExtractionHint,
bounds: &ExtractionBounds,
) -> Result<ExtractionResult, ExtractionError> {
match active_engine() {
Some(engine) => engine.extract(bytes, hint, bounds),
None => Err(ExtractionError::NoEngineConfigured),
}
}
#[cfg(test)]
mod tests {
use super::*;
static REG_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn bbox() -> BBox {
BBox { x: 0.1, y: 0.1, w: 0.2, h: 0.05 }
}
#[derive(Debug)]
struct MockEngine;
impl ExtractionEngine for MockEngine {
fn name(&self) -> &str {
"mock"
}
fn version(&self) -> &str {
"9"
}
fn extract(
&self,
_bytes: &[u8],
hint: &ExtractionHint,
_bounds: &ExtractionBounds,
) -> Result<ExtractionResult, ExtractionError> {
let t = hint.transform.clone().unwrap_or_default();
Ok(ExtractionResult {
engine: "mock".into(),
engine_version: "9".into(),
spans: vec![ExtractedSpan::new(format!("read:{t}"), 0.9, 0, bbox())],
})
}
}
#[test]
fn every_span_is_born_inferred_never_parsed() {
let s = ExtractedSpan::new("1250.00", 0.94, 0, bbox());
assert_eq!(s.provenance(), IngestProvenance::Inferred);
assert_eq!(s.epistemic_ceiling(), "believe");
}
#[test]
fn result_is_untrusted_and_inferred() {
let r = ExtractionResult {
engine: "idp-e".into(),
engine_version: "1".into(),
spans: vec![ExtractedSpan::new("x", 0.8, 0, bbox())],
};
assert_eq!(r.taint(), EpistemicTaint::Untrusted);
assert_eq!(r.provenance(), IngestProvenance::Inferred);
}
#[test]
fn confidence_is_clamped_and_nan_fails_closed() {
assert_eq!(ExtractedSpan::new("a", 1.5, 0, bbox()).confidence, 1.0);
assert_eq!(ExtractedSpan::new("a", -0.2, 0, bbox()).confidence, 0.0);
assert_eq!(ExtractedSpan::new("a", f64::NAN, 0, bbox()).confidence, 0.0);
}
#[test]
fn floor_gate_quarantines_sub_floor_spans() {
let spans = vec![
ExtractedSpan::new("high", 0.96, 0, bbox()),
ExtractedSpan::new("low", 0.42, 0, bbox()),
];
let disp = apply_confidence_floor(&spans, 0.75);
assert!(disp[0].is_believed());
assert!(matches!(disp[1], SpanDisposition::Quarantined { floor, .. } if floor == 0.75));
assert_eq!(believed_text(&disp), "high");
}
#[test]
fn floor_boundary_is_inclusive() {
let spans = vec![ExtractedSpan::new("edge", 0.75, 0, bbox())];
assert!(apply_confidence_floor(&spans, 0.75)[0].is_believed());
}
#[test]
fn no_engine_typed_refuses_never_empty_string() {
let err = NoEngine.extract(b"whatever", &ExtractionHint::default(), &ExtractionBounds::default())
.unwrap_err();
assert_eq!(err, ExtractionError::NoEngineConfigured);
assert_eq!(err.slug(), "extraction.no_engine_configured");
}
#[test]
fn audit_fields_are_computed_not_reported() {
let r = ExtractionResult {
engine: "idp-e".into(),
engine_version: "1".into(),
spans: vec![
ExtractedSpan::new("a", 0.9, 0, bbox()),
ExtractedSpan::new("b", 0.7, 2, bbox()),
],
};
assert!((r.mean_confidence() - 0.8).abs() < 1e-9);
assert_eq!(r.page_count(), 3); }
#[test]
fn bbox_validity_rejects_out_of_page() {
assert!(bbox().is_valid());
assert!(!BBox { x: 0.9, y: 0.0, w: 0.5, h: 0.1 }.is_valid());
assert!(!BBox { x: -0.1, y: 0.0, w: 0.1, h: 0.1 }.is_valid());
}
#[test]
fn error_slugs_are_stable() {
assert_eq!(ExtractionError::PageCapExceeded(9000).slug(), "page_cap_exceeded");
assert_eq!(ExtractionError::PixelCapExceeded(999).slug(), "pixel_cap_exceeded");
assert_eq!(ExtractionError::EngineFailed("x".into()).slug(), "extraction.engine_failed");
}
#[test]
fn registry_default_refuses_then_serves_a_registered_engine() {
let _guard = REG_LOCK.lock().unwrap();
clear_engine();
let hint = ExtractionHint { transform: Some("image:text".into()), ..Default::default() };
assert_eq!(
run_active(b"x", &hint, &ExtractionBounds::default()).unwrap_err(),
ExtractionError::NoEngineConfigured
);
register_engine(Arc::new(MockEngine));
let out = run_active(b"x", &hint, &ExtractionBounds::default()).unwrap();
assert_eq!(out.engine, "mock");
assert_eq!(out.provenance(), IngestProvenance::Inferred);
assert_eq!(out.taint(), EpistemicTaint::Untrusted);
assert_eq!(out.spans[0].text, "read:image:text");
clear_engine();
assert!(run_active(b"x", &hint, &ExtractionBounds::default()).is_err());
}
}