Skip to main content

aqc_file_engine_core/
engine.rs

1//! Engine traits: the typed `FileEngine<ResolvedRequirements>` and the erased `Engine`
2//! contract the runner dispatches over.
3
4use crate::finding::Finding;
5use crate::merge::ConflictEntry;
6use crate::requirement::EngineRequirement;
7use crate::types::{EngineOutput, Provenance};
8
9/// A file engine: reconciles bytes-on-disk against typed declarative
10/// requirements, returning both the bytes `init` would write and the
11/// findings `validate` would report.
12///
13/// Engines are pure functions. They do not perform I/O. They never
14/// return an error: catastrophic failures (parse failures, internal
15/// invariant violations) surface as `Finding`s inside `EngineOutput`.
16#[expect(
17    clippy::module_name_repetitions,
18    reason = "FileEngine is the canonical trait name; renaming it loses the connection to the file-engines abstraction in plans and call sites."
19)]
20pub trait FileEngine<ResolvedRequirements> {
21    /// Apply `resolved_requirements` against `current_bytes`, returning what `init`
22    /// would write and what `validate` would report.
23    fn reconcile(
24        current_bytes: Option<&[u8]>,
25        resolved_requirements: &ResolvedRequirements,
26    ) -> EngineOutput;
27}
28
29/// Erased engine contract the runner dispatches over.
30///
31/// The runner supplies the current bytes and the type-erased requirements for
32/// one target. The engine only reconciles bytes; it does not know file paths.
33pub trait Engine {
34    /// Stable engine id (matches the crate's `ENGINE_ID`).
35    fn id(&self) -> &'static str;
36    /// Reconcile current file state against the requirements routed to this
37    /// engine and target slot.
38    #[allow(
39        clippy::type_complexity,
40        reason = "`&[(Provenance, Box<dyn EngineRequirement>)]` is the erased multi-requirement input the registry dispatches; a type alias would hide the contract."
41    )]
42    fn reconcile(
43        &self,
44        current_bytes: Option<&[u8]>,
45        reqs: &[(Provenance, Box<dyn EngineRequirement>)],
46    ) -> EngineOutput;
47}
48
49/// The shared erased-reconcile body every engine's `Engine::reconcile` is.
50///
51/// Downcast the routed requirements to `Requirements`; with none, echo the
52/// current bytes back unchanged. Otherwise run the engine's merge phase, then
53/// reconcile the merged desired-state against the supplied bytes.
54#[allow(
55    clippy::type_complexity,
56    reason = "The erased multi-requirement input and merge/reconcile closures are the public dispatch shape."
57)]
58pub fn merged_reconcile<Requirements, ResolvedRequirements, Merge, Reconcile>(
59    current_bytes: Option<&[u8]>,
60    reqs: &[(Provenance, Box<dyn EngineRequirement>)],
61    merge: Merge,
62    reconcile_one: Reconcile,
63) -> EngineOutput
64where
65    Requirements: EngineRequirement + Clone,
66    Merge: Fn(Vec<(Provenance, Requirements)>) -> (ResolvedRequirements, Vec<ConflictEntry>),
67    Reconcile: Fn(Option<&[u8]>, &ResolvedRequirements) -> EngineOutput,
68{
69    let typed: Vec<(Provenance, Requirements)> = reqs
70        .iter()
71        .filter_map(|(prov, r)| {
72            r.as_any()
73                .downcast_ref::<Requirements>()
74                .map(|req| (prov.clone(), req.clone()))
75        })
76        .collect();
77    if typed.is_empty() {
78        return EngineOutput {
79            expected_bytes: current_bytes.map(<[u8]>::to_vec).unwrap_or_default(),
80            findings: Vec::new(),
81        };
82    }
83    let (merged, conflicts) = merge(typed);
84    let mut out = reconcile_one(current_bytes, &merged);
85    for entry in conflicts {
86        let finding = Finding::ConflictingRequirements {
87            key: entry.key,
88            contributors: entry
89                .contributors
90                .into_iter()
91                .map(|(prov, value)| (prov.policy, value))
92                .collect(),
93            reason: entry.reason,
94        };
95        out.findings.push(finding);
96    }
97    out
98}