use crate::output;
use camino::{Utf8Path, Utf8PathBuf};
use provenance_macros::rule;
use provenance_store::merge::{
changed_statement_diagnostics, merge_records, read_jsonl_records_for_shard,
validate_merged_records, MergeOutcome,
};
use provenance_store::statement_analysis::violation_error;
pub(super) fn handle(
base: &Utf8PathBuf,
ours: &Utf8PathBuf,
theirs: &Utf8PathBuf,
output_path: Option<Utf8PathBuf>,
shard_path: Option<&Utf8Path>,
format: crate::output::OutputFormat,
) -> anyhow::Result<()> {
let target_path = shard_path.or(output_path.as_deref());
let base_records = target_path.map_or_else(
|| provenance_store::merge::read_jsonl_records(base),
|target| read_jsonl_records_for_shard(base, target),
)?;
let our_records = target_path.map_or_else(
|| provenance_store::merge::read_jsonl_records(ours),
|target| read_jsonl_records_for_shard(ours, target),
)?;
let their_records = target_path.map_or_else(
|| provenance_store::merge::read_jsonl_records(theirs),
|target| read_jsonl_records_for_shard(theirs, target),
)?;
let outcome = merge_records(&base_records, &our_records, &their_records)?;
let records = match &outcome {
MergeOutcome::Clean { records } => records,
MergeOutcome::Conflicted { partial, .. } => partial,
};
if let Some(shard_path) = target_path {
validate_merged_records(shard_path, records)?;
if let Err(error) = ensure_changed_statements_are_clean(shard_path, &base_records, records)
{
if matches!(outcome, MergeOutcome::Conflicted { .. }) {
output::print(format, &outcome)?;
}
return Err(error);
}
}
if let Some(output_path) = output_path {
provenance_store::jsonl::write_jsonl_atomic(&output_path, records)?;
}
output::print(format, &outcome)?;
if let MergeOutcome::Conflicted { conflicts, .. } = &outcome {
anyhow::bail!(
"merge left {} conflicting record(s): {}",
conflicts.len(),
conflicts
.iter()
.map(|conflict| conflict.record_id.clone())
.collect::<Vec<String>>()
.join(", ")
);
}
Ok(())
}
#[rule("rule_ste_merge_changed_statement_gate")]
fn ensure_changed_statements_are_clean(
shard_path: &Utf8Path,
base: &[serde_json::Value],
candidate: &[serde_json::Value],
) -> anyhow::Result<()> {
let diagnostics = changed_statement_diagnostics(shard_path, base, candidate)?;
if diagnostics.is_empty() {
Ok(())
} else {
Err(violation_error(&diagnostics))
}
}