use crate::contract::schema::{Ecosystem, ReleaseLayout};
use crate::protocol::journal::{PublishReceipt as JournalReceipt, RunState};
use crate::protocol::reconcile::{ReconcileReport, ReconcileSummary, TargetReconcile};
use crate::protocol::release::{PublishReceipt, VerifyOutcome};
use super::adapters::{resolve, EffectCtx, ReleaseAdapter};
#[must_use]
pub fn reconcile(state: &RunState, ctx: &EffectCtx<'_>) -> ReconcileReport {
let mut targets = Vec::with_capacity(state.published.len());
let mut summary = ReconcileSummary::default();
for (target_id, receipt) in &state.published {
let (outcome, detail) = classify(ctx, receipt);
match outcome {
VerifyOutcome::Matches => summary.matches += 1,
VerifyOutcome::Conflicts => summary.conflicts += 1,
VerifyOutcome::Missing => summary.missing += 1,
VerifyOutcome::Unknown => summary.unknown += 1,
}
targets.push(TargetReconcile {
target: target_id.clone(),
ecosystem: receipt.ecosystem.clone(),
package: receipt.package.clone(),
version: receipt.version.clone(),
outcome,
detail,
});
}
summary.reconciled = targets.len();
ReconcileReport {
run_id: state.run_id.clone(),
plan_id: state.plan_id.clone(),
run_status: state.status,
journal_seq: state.applied_seq,
targets,
summary,
}
}
fn classify(ctx: &EffectCtx<'_>, receipt: &JournalReceipt) -> (VerifyOutcome, Option<String>) {
let Some(package) = receipt.package.clone() else {
return (
VerifyOutcome::Unknown,
Some(
"the receipt recorded no package name; the registry cannot be queried".to_string(),
),
);
};
let Some(ecosystem) = Ecosystem::parse(&receipt.ecosystem) else {
return (
VerifyOutcome::Unknown,
Some(format!(
"unrecognized ecosystem '{}'; cannot reconcile it against a registry",
receipt.ecosystem
)),
);
};
let adapter_id = ecosystem.default_adapter(ReleaseLayout::Single);
let release_receipt = PublishReceipt {
adapter: adapter_id,
ecosystem,
package,
version: receipt.version.clone(),
canonical_ref: String::new(),
digest: receipt.digest.clone(),
remote_url: receipt.registry_url.clone(),
timestamp: 0,
};
let outcome = match resolve(adapter_id).verify(ctx, &release_receipt) {
Ok(outcome) => outcome,
Err(_) => VerifyOutcome::Unknown,
};
(outcome, detail_for(outcome, ecosystem))
}
fn detail_for(outcome: VerifyOutcome, ecosystem: Ecosystem) -> Option<String> {
match outcome {
VerifyOutcome::Matches => None,
VerifyOutcome::Missing => {
Some("the registry does not report this version as published".to_string())
}
VerifyOutcome::Conflicts => Some(
"the registry holds this version but its digest differs from the recorded receipt"
.to_string(),
),
VerifyOutcome::Unknown if ecosystem == Ecosystem::Binary => Some(
"this distribution target (GitHub Releases or a homebrew formula) is not \
observable through the registry query"
.to_string(),
),
VerifyOutcome::Unknown => Some(
"the registry lookup could not be performed (registry outage or unresolvable package)"
.to_string(),
),
}
}
#[cfg(test)]
mod tests;