use std::path::{Path, PathBuf};
use crate::finding::Finding;
use crate::merge::ConflictEntry;
use crate::requirement::EngineRequirement;
use crate::types::{EngineOutput, Provenance};
#[expect(
clippy::module_name_repetitions,
reason = "FileEngine is the canonical trait name; renaming it loses the connection to the file-engines abstraction in plans and call sites."
)]
pub trait FileEngine<Req> {
fn reconcile(current_bytes: Option<&[u8]>, requirement: &Req) -> EngineOutput;
}
pub trait Engine {
fn id(&self) -> &'static str;
fn target_path(&self, workspace_root: &Path) -> PathBuf;
#[expect(
clippy::type_complexity,
reason = "`&[(Provenance, Box<dyn EngineRequirement>)]` is the erased multi-requirement input the registry dispatches; a type alias would hide the contract."
)]
fn reconcile(
&self,
current: Option<&[u8]>,
reqs: &[(Provenance, Box<dyn EngineRequirement>)],
) -> EngineOutput;
}
#[expect(
clippy::type_complexity,
reason = "`Fn(Vec<(Provenance, Req)>) -> (Resolved, Vec<ConflictEntry>)` is the merge phase's signature as data; aliasing it would hide the raw-to-resolved contract."
)]
pub fn merged_reconcile<Req, Resolved, M, F>(
current: Option<&[u8]>,
reqs: &[(Provenance, Box<dyn EngineRequirement>)],
subject: &str,
merge: M,
reconcile_one: F,
) -> EngineOutput
where
Req: EngineRequirement + Clone,
M: Fn(Vec<(Provenance, Req)>) -> (Resolved, Vec<ConflictEntry>),
F: Fn(Option<&[u8]>, &Resolved) -> EngineOutput,
{
let typed: Vec<(Provenance, Req)> = reqs
.iter()
.filter_map(|(prov, r)| {
r.as_any()
.downcast_ref::<Req>()
.map(|req| (prov.clone(), req.clone()))
})
.collect();
if typed.is_empty() {
return EngineOutput {
expected_bytes: current.map(<[u8]>::to_vec).unwrap_or_default(),
findings: Vec::new(),
};
}
let (merged, conflicts) = merge(typed);
let mut out = reconcile_one(current, &merged);
for entry in conflicts {
out.findings.push(Finding::ConflictingRequirements {
subject: subject.to_owned(),
key: entry.key,
contributors: entry
.contributors
.into_iter()
.map(|(prov, value)| (prov.policy, value))
.collect(),
reason: entry.reason,
});
}
out
}