use std::fmt;
use turnframe_core::case::CaseKey;
use turnframe_core::ids::OperationKey;
use turnframe_core::interaction::InteractionKind;
use turnframe_core::reduce::{CommandRef, PlannedActResult};
use turnframe_core::response::ClaimClass;
use turnframe_core::target::TargetResolution;
use crate::planning::PlannedTurn;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Side {
Shadow,
Authoritative,
}
impl Side {
#[must_use]
pub const fn other(self) -> Self {
match self {
Self::Shadow => Self::Authoritative,
Self::Authoritative => Self::Shadow,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Shadow => "shadow",
Self::Authoritative => "authoritative",
}
}
}
impl fmt::Display for Side {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ActSummary {
pub kind: String,
pub operation: Option<OperationKey>,
pub case: Option<CaseKey>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MutationSummary {
pub operation: OperationKey,
pub case: Option<CaseKey>,
pub command_ref: Option<CommandRef>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClarificationSummary {
pub key: String,
pub kind: InteractionKind,
pub case: CaseKey,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RefusalReason {
AmbiguousTarget,
MissingTarget,
UnauthorizedTarget,
StaleTarget,
ConfirmationRequired,
DomainRejected,
}
impl RefusalReason {
#[must_use]
pub const fn is_unresolved_target(self) -> bool {
matches!(
self,
Self::AmbiguousTarget
| Self::MissingTarget
| Self::UnauthorizedTarget
| Self::StaleTarget
)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::AmbiguousTarget => "ambiguous_target",
Self::MissingTarget => "missing_target",
Self::UnauthorizedTarget => "unauthorized_target",
Self::StaleTarget => "stale_target",
Self::ConfirmationRequired => "confirmation_required",
Self::DomainRejected => "domain_rejected",
}
}
}
impl fmt::Display for RefusalReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefusedMutation {
pub operation: Option<OperationKey>,
pub reason: RefusalReason,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TurnSummary {
pub cases: Vec<CaseKey>,
pub acts: Vec<ActSummary>,
pub mutations: Vec<MutationSummary>,
pub clarifications: Vec<ClarificationSummary>,
pub claims: Vec<ClaimClass>,
pub evidenced_claims: Vec<ClaimClass>,
pub refusals: Vec<RefusedMutation>,
}
impl TurnSummary {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_case(mut self, case: CaseKey) -> Self {
self.cases.push(case);
self
}
#[must_use]
pub fn with_act(mut self, act: ActSummary) -> Self {
self.acts.push(act);
self
}
#[must_use]
pub fn with_mutation(mut self, mutation: MutationSummary) -> Self {
self.mutations.push(mutation);
self
}
#[must_use]
pub fn with_clarification(mut self, clarification: ClarificationSummary) -> Self {
self.clarifications.push(clarification);
self
}
#[must_use]
pub fn with_claim(mut self, class: ClaimClass) -> Self {
self.claims.push(class);
self
}
#[must_use]
pub fn with_evidenced_claim(mut self, class: ClaimClass) -> Self {
self.evidenced_claims.push(class);
self
}
#[must_use]
pub fn with_refusal(mut self, refusal: RefusedMutation) -> Self {
self.refusals.push(refusal);
self
}
#[must_use]
pub fn from_planned(planned: &PlannedTurn) -> Self {
let mut mutations: Vec<MutationSummary> = Vec::new();
let mut acts: Vec<ActSummary> = Vec::new();
let mut refusals: Vec<RefusedMutation> = Vec::new();
for planned_act in &planned.reduction.acts {
let case = planned_act
.target
.as_ref()
.and_then(TargetResolution::exact)
.map(turnframe_core::case::CaseRef::key);
let operation = planned_act.act.operation().cloned();
acts.push(ActSummary {
kind: planned_act.act.kind_name().to_owned(),
operation: operation.clone(),
case: case.clone(),
});
match &planned_act.result {
PlannedActResult::ReadyToExecute { command_refs } => {
if let Some(operation) = operation.clone() {
for command_ref in command_refs {
mutations.push(MutationSummary {
operation: operation.clone(),
case: case.clone(),
command_ref: Some(*command_ref),
});
}
}
}
PlannedActResult::AwaitingConfirmation { .. } => refusals.push(RefusedMutation {
operation: operation.clone(),
reason: RefusalReason::ConfirmationRequired,
}),
PlannedActResult::Rejected { .. } => refusals.push(RefusedMutation {
operation: operation.clone(),
reason: unresolved_reason(planned_act.target.as_ref())
.unwrap_or(RefusalReason::DomainRejected),
}),
PlannedActResult::NeedsClarification { .. } => {
if let Some(reason) = unresolved_reason(planned_act.target.as_ref()) {
refusals.push(RefusedMutation {
operation: operation.clone(),
reason,
});
}
}
_ => {}
}
}
Self {
cases: planned
.views
.iter()
.map(|view| view.case_ref.key())
.collect(),
acts,
mutations,
clarifications: planned
.would_persist
.iter()
.map(|spec| ClarificationSummary {
key: spec.key.clone(),
kind: spec.kind,
case: spec.case_ref.key(),
})
.collect(),
claims: planned.would_claim.clone(),
evidenced_claims: Vec::new(),
refusals,
}
}
}
fn unresolved_reason(resolution: Option<&TargetResolution>) -> Option<RefusalReason> {
match resolution? {
TargetResolution::Ambiguous { .. } => Some(RefusalReason::AmbiguousTarget),
TargetResolution::Missing => Some(RefusalReason::MissingTarget),
TargetResolution::Unauthorized => Some(RefusalReason::UnauthorizedTarget),
TargetResolution::Stale { .. } => Some(RefusalReason::StaleTarget),
TargetResolution::Exact { .. } => None,
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Attribution {
Shadow,
Authoritative,
Undetermined,
}
impl Attribution {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Shadow => "shadow",
Self::Authoritative => "authoritative",
Self::Undetermined => "undetermined",
}
}
}
impl fmt::Display for Attribution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Divergence {
CaseSelection {
shadow: Vec<CaseKey>,
authoritative: Vec<CaseKey>,
},
ActsExtracted {
shadow: Vec<ActSummary>,
authoritative: Vec<ActSummary>,
},
Mutations {
shadow: Vec<MutationSummary>,
authoritative: Vec<MutationSummary>,
},
ClarificationVersusAction {
asked: Side,
clarifications: Vec<ClarificationSummary>,
mutations: Vec<MutationSummary>,
},
ClaimWithoutEvent {
claimant: Side,
class: ClaimClass,
},
RefusedUnresolvedTargetThatRan {
performed: MutationSummary,
reason: RefusalReason,
},
}
impl Divergence {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::CaseSelection { .. } => "case_selection",
Self::ActsExtracted { .. } => "acts_extracted",
Self::Mutations { .. } => "mutations",
Self::ClarificationVersusAction { .. } => "clarification_versus_action",
Self::ClaimWithoutEvent { .. } => "claim_without_event",
Self::RefusedUnresolvedTargetThatRan { .. } => "refused_unresolved_target_that_ran",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
pub divergence: Divergence,
pub attribution: Attribution,
}
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.divergence.kind(), self.attribution)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DivergenceReport {
pub findings: Vec<Finding>,
}
impl DivergenceReport {
#[must_use]
pub fn agreed(&self) -> bool {
self.findings.is_empty()
}
pub fn against(&self, side: Side) -> impl Iterator<Item = &Finding> {
let wanted = match side {
Side::Shadow => Attribution::Shadow,
Side::Authoritative => Attribution::Authoritative,
};
self.findings
.iter()
.filter(move |finding| finding.attribution == wanted)
}
#[must_use]
pub fn any_against_shadow(&self) -> bool {
self.against(Side::Shadow).next().is_some()
}
pub fn of_kind<'a>(&'a self, kind: &'a str) -> impl Iterator<Item = &'a Finding> {
self.findings
.iter()
.filter(move |finding| finding.divergence.kind() == kind)
}
}
impl fmt::Display for DivergenceReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.findings.is_empty() {
return f.write_str("no divergence");
}
for (index, finding) in self.findings.iter().enumerate() {
if index > 0 {
f.write_str("; ")?;
}
write!(f, "{finding}")?;
}
Ok(())
}
}
#[must_use]
pub fn compare(shadow: &TurnSummary, authoritative: &TurnSummary) -> DivergenceReport {
let mut findings = Vec::new();
let mut explained: Vec<&MutationSummary> = Vec::new();
for performed in &authoritative.mutations {
if shadow
.mutations
.iter()
.any(|ours| same_mutation(ours, performed))
{
continue;
}
let Some(refusal) = shadow.refusals.iter().find(|refusal| {
refusal.reason.is_unresolved_target()
&& refusal
.operation
.as_ref()
.is_none_or(|operation| *operation == performed.operation)
}) else {
continue;
};
explained.push(performed);
findings.push(Finding {
divergence: Divergence::RefusedUnresolvedTargetThatRan {
performed: performed.clone(),
reason: refusal.reason,
},
attribution: Attribution::Authoritative,
});
}
if shadow.cases != authoritative.cases {
findings.push(Finding {
divergence: Divergence::CaseSelection {
shadow: shadow.cases.clone(),
authoritative: authoritative.cases.clone(),
},
attribution: Attribution::Undetermined,
});
}
if shadow.acts != authoritative.acts {
findings.push(Finding {
divergence: Divergence::ActsExtracted {
shadow: shadow.acts.clone(),
authoritative: authoritative.acts.clone(),
},
attribution: Attribution::Undetermined,
});
}
let remaining: Vec<MutationSummary> = authoritative
.mutations
.iter()
.filter(|performed| !explained.iter().any(|done| same_mutation(done, performed)))
.cloned()
.collect();
if !same_mutation_set(&shadow.mutations, &remaining) {
findings.push(Finding {
divergence: Divergence::Mutations {
shadow: shadow.mutations.clone(),
authoritative: remaining,
},
attribution: Attribution::Undetermined,
});
}
for asked in [Side::Shadow, Side::Authoritative] {
let (asker, actor) = match asked {
Side::Shadow => (shadow, authoritative),
Side::Authoritative => (authoritative, shadow),
};
if asker.clarifications.is_empty()
|| !asker.mutations.is_empty()
|| actor.mutations.is_empty()
{
continue;
}
let attribution = if asked == Side::Shadow
&& asker
.refusals
.iter()
.any(|refusal| refusal.reason.is_unresolved_target())
{
Attribution::Authoritative
} else {
Attribution::Undetermined
};
findings.push(Finding {
divergence: Divergence::ClarificationVersusAction {
asked,
clarifications: asker.clarifications.clone(),
mutations: actor.mutations.clone(),
},
attribution,
});
}
for (side, summary) in [(Side::Shadow, shadow), (Side::Authoritative, authoritative)] {
for class in &summary.claims {
if !is_event_backable(*class)
|| shadow.evidenced_claims.contains(class)
|| authoritative.evidenced_claims.contains(class)
{
continue;
}
findings.push(Finding {
divergence: Divergence::ClaimWithoutEvent {
claimant: side,
class: *class,
},
attribution: match side {
Side::Shadow => Attribution::Shadow,
Side::Authoritative => Attribution::Authoritative,
},
});
}
}
DivergenceReport { findings }
}
const fn is_event_backable(class: ClaimClass) -> bool {
!matches!(
class,
ClaimClass::InteractionVisibility | ClaimClass::FutureNotification
)
}
fn same_mutation(left: &MutationSummary, right: &MutationSummary) -> bool {
left.operation == right.operation && left.case == right.case
}
fn same_mutation_set(left: &[MutationSummary], right: &[MutationSummary]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right)
.all(|(one, other)| same_mutation(one, other))
}