use std::path::{Path, PathBuf};
use crate::finding::Finding;
use crate::merge::ConflictEntry;
use crate::requirement::EngineRequirement;
use crate::types::{EngineFileState, 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_paths(
&self,
workspace_root: &Path,
reqs: &[(Provenance, Box<dyn EngineRequirement>)],
) -> Vec<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,
target_root: &Path,
current: &[EngineFileState],
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: &[EngineFileState],
target_path: PathBuf,
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();
let current_bytes = current
.iter()
.find(|state| state.path == target_path)
.and_then(|state| state.bytes.as_deref());
if typed.is_empty() {
return EngineOutput::single(
current_bytes.map(<[u8]>::to_vec).unwrap_or_default(),
Vec::new(),
)
.with_single_path(target_path);
}
let (merged, conflicts) = merge(typed);
let mut out = reconcile_one(current_bytes, &merged).with_single_path(target_path);
for entry in conflicts {
let finding = 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.findings.push(finding);
}
out
}