use serde_json::Value;
use super::citations::{self, CitationOutput};
use super::schema::{self, Outcome};
use crate::core::config::MergeMode;
pub struct FinalizedOutput {
pub structured_output: Value,
pub structured_output_flat: Value,
pub outcome: Outcome,
pub error_message: Option<String>,
pub per_batch_errors: Vec<String>,
}
#[allow(clippy::too_many_arguments)]
pub fn merge_and_cite(
raw_responses: Vec<Value>,
schema_value: &Value,
merge_mode: MergeMode,
ocr_elements: &[Value],
element_metadata: &[Value],
emit_citations: bool,
match_threshold: f64,
fused_confidence: f64,
) -> FinalizedOutput {
let merged = schema::validate_and_merge(raw_responses, schema_value, merge_mode);
let CitationOutput {
structured_output,
structured_output_flat,
} = citations::fuse(
merged.merged,
ocr_elements,
element_metadata,
emit_citations,
match_threshold,
fused_confidence,
);
FinalizedOutput {
structured_output,
structured_output_flat,
outcome: merged.outcome,
error_message: merged.error_message,
per_batch_errors: merged.per_batch_errors,
}
}
#[cfg(test)]
mod tests {
use super::*;
const MATCH_THRESHOLD: f64 = 0.8;
const FUSED_CONFIDENCE: f64 = 0.95;
#[test]
fn should_merge_then_cite_when_all_batches_are_valid_and_citations_are_on() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number" }
}
});
let batch1 = serde_json::json!({"name": "Alice"});
let batch2 = serde_json::json!({"age": 30});
let ocr = serde_json::json!({
"text": "Alice",
"page_number": 1,
"bbox": [10.0, 20.0, 100.0, 30.0]
});
let result = merge_and_cite(
vec![batch1, batch2],
&schema,
MergeMode::ObjectMerge,
&[ocr],
&[],
true,
MATCH_THRESHOLD,
FUSED_CONFIDENCE,
);
assert_eq!(result.outcome, Outcome::Success);
assert!(result.per_batch_errors.is_empty());
let name_field = result.structured_output.get("name").expect("name field present");
let cited: citations::CitedField =
serde_json::from_value(name_field.clone()).expect("citation envelope deserializes");
assert_eq!(cited.source, citations::CitationSource::Fused);
assert_eq!(cited.page, Some(1));
assert_eq!(cited.confidence, Some(FUSED_CONFIDENCE));
assert_eq!(
result.structured_output_flat.get("name").and_then(|v| v.as_str()),
Some("Alice")
);
assert_eq!(
result.structured_output_flat.get("age").and_then(|v| v.as_u64()),
Some(30)
);
}
#[test]
fn should_report_partial_success_and_pass_through_when_citations_are_off() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": { "type": "string" }
}
});
let batch1 = serde_json::json!({"name": "Alice"});
let batch2 = serde_json::json!({"name": 123});
let result = merge_and_cite(
vec![batch1, batch2],
&schema,
MergeMode::ObjectMerge,
&[],
&[],
false,
MATCH_THRESHOLD,
FUSED_CONFIDENCE,
);
assert_eq!(result.outcome, Outcome::PartialSuccess);
assert_eq!(result.per_batch_errors.len(), 1);
assert!(result.per_batch_errors[0].contains("batch 1"));
assert_eq!(
result.structured_output,
serde_json::json!({"name": "Alice"}),
"citations disabled means structured_output is the merged value verbatim"
);
assert_eq!(result.structured_output, result.structured_output_flat);
}
#[test]
fn should_fuse_null_merge_when_every_batch_fails_schema_validation() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"count": { "type": "number" }
},
"required": ["count"]
});
let batch1 = serde_json::json!({"count": "not a number"});
let result = merge_and_cite(
vec![batch1],
&schema,
MergeMode::ObjectMerge,
&[],
&[],
true,
MATCH_THRESHOLD,
FUSED_CONFIDENCE,
);
assert_eq!(result.outcome, Outcome::SchemaInvalid);
assert_eq!(
result.error_message,
Some("all batches failed schema validation".to_string())
);
let cited: citations::CitedField =
serde_json::from_value(result.structured_output.clone()).expect("null leaf still gets an envelope");
assert_eq!(cited.source, citations::CitationSource::None);
assert_eq!(cited.value, Value::Null);
}
}