use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Subject {
Instrument(String),
Position(String),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OrderId(pub String);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ClientOrderKey(pub String);
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "phase", rename_all = "snake_case")]
pub enum Phase {
Observation,
Decision,
RiskEvaluation { passed: bool },
Submission { order: OrderId },
Acknowledgment { order: OrderId },
AcknowledgmentUnobserved { order: OrderId },
Fill { order: OrderId },
Reconciliation { order: OrderId },
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum PhaseKind {
Observation,
Decision,
RiskEvaluation,
Submission,
Acknowledgment,
AcknowledgmentUnobserved,
Fill,
Reconciliation,
}
impl Phase {
pub fn kind(&self) -> PhaseKind {
match self {
Phase::Observation => PhaseKind::Observation,
Phase::Decision => PhaseKind::Decision,
Phase::RiskEvaluation { .. } => PhaseKind::RiskEvaluation,
Phase::Submission { .. } => PhaseKind::Submission,
Phase::Acknowledgment { .. } => PhaseKind::Acknowledgment,
Phase::AcknowledgmentUnobserved { .. } => PhaseKind::AcknowledgmentUnobserved,
Phase::Fill { .. } => PhaseKind::Fill,
Phase::Reconciliation { .. } => PhaseKind::Reconciliation,
}
}
fn order(&self) -> Option<&OrderId> {
match self {
Phase::Submission { order }
| Phase::Acknowledgment { order }
| Phase::AcknowledgmentUnobserved { order }
| Phase::Fill { order }
| Phase::Reconciliation { order } => Some(order),
Phase::Observation | Phase::Decision | Phase::RiskEvaluation { .. } => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct LifecycleStep {
pub subject: Subject,
pub phase: Phase,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_key: Option<ClientOrderKey>,
}
impl LifecycleStep {
pub fn new(subject: Subject, phase: Phase) -> Self {
Self {
subject,
phase,
client_key: None,
}
}
pub fn with_client_key(mut self, key: ClientOrderKey) -> Self {
self.client_key = Some(key);
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum ProcessEvent {
OrderPlaced { risk_gate_passed: bool },
DrawdownHalt { respected: bool },
DenylistBypass,
ConcentrationBreach,
ManipulativeOrder,
TailSellingExposure { hedged: bool },
DecisionRationale { symbol: String, rationale: String },
Lifecycle(LifecycleStep),
}
impl ProcessEvent {
pub(crate) fn is_block_violation(&self) -> bool {
matches!(
self,
ProcessEvent::OrderPlaced {
risk_gate_passed: false
} | ProcessEvent::DrawdownHalt { respected: false }
| ProcessEvent::DenylistBypass
| ProcessEvent::ManipulativeOrder
| ProcessEvent::TailSellingExposure { hedged: false }
)
}
pub(crate) fn is_warn_violation(&self) -> bool {
matches!(
self,
ProcessEvent::ConcentrationBreach | ProcessEvent::TailSellingExposure { hedged: true }
)
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Trace {
pub events: Vec<ProcessEvent>,
}
#[derive(Clone, Debug, Serialize)]
pub struct ProcessScore {
pub block_violations: usize,
pub warn_violations: usize,
pub score: f64,
}
impl ProcessScore {
pub fn is_clean(&self) -> bool {
self.block_violations == 0
}
}
pub fn process_score(trace: &Trace) -> ProcessScore {
let block = trace
.events
.iter()
.filter(|e| e.is_block_violation())
.count();
let warn = trace
.events
.iter()
.filter(|e| e.is_warn_violation())
.count();
let score = if block > 0 {
0.0
} else {
(1.0 - warn as f64 * 0.1).max(0.0)
};
ProcessScore {
block_violations: block,
warn_violations: warn,
score,
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(tag = "violation", rename_all = "snake_case")]
pub enum OrderingViolation {
UnauthorizedSubmission {
subject: Subject,
order: OrderId,
authorized_subjects: Vec<Subject>,
},
OrderSubjectDrift {
order: OrderId,
first: Subject,
later: Subject,
},
AcknowledgmentWithoutSubmission { order: OrderId },
FillWithoutAcknowledgment { order: OrderId },
ReconciliationWithoutFill { order: OrderId },
OutOfOrderTransition {
order: OrderId,
attempted: PhaseKind,
current: Option<PhaseKind>,
},
AmbiguousWriteRetriedWithoutKey {
subject: Subject,
ambiguous_order: OrderId,
retry_order: OrderId,
retry_key: Option<ClientOrderKey>,
},
AmbiguityUnavailable {
subject: Subject,
prior_order: OrderId,
retry_order: OrderId,
},
DuplicateTransition { order: OrderId, phase: PhaseKind },
UnreconciledFill { order: OrderId },
SubmissionWithoutDecision { subject: Subject, order: OrderId },
DecisionWithoutObservation { subject: Subject },
}
impl OrderingViolation {
pub fn is_block(&self) -> bool {
matches!(
self,
OrderingViolation::UnauthorizedSubmission { .. }
| OrderingViolation::OrderSubjectDrift { .. }
| OrderingViolation::AcknowledgmentWithoutSubmission { .. }
| OrderingViolation::FillWithoutAcknowledgment { .. }
| OrderingViolation::ReconciliationWithoutFill { .. }
| OrderingViolation::OutOfOrderTransition { .. }
| OrderingViolation::AmbiguousWriteRetriedWithoutKey { .. }
)
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
pub struct LifecycleReport {
pub violations: Vec<OrderingViolation>,
pub block_violations: usize,
pub warn_violations: usize,
}
impl LifecycleReport {
pub fn is_clean(&self) -> bool {
self.block_violations == 0
}
}
struct OrderProgress {
subject: Subject,
stage: PhaseKind,
submitted_at: usize,
client_key: Option<ClientOrderKey>,
answered: bool,
}
struct AmbiguousIntent {
subject: Subject,
origin: OrderId,
key: Option<ClientOrderKey>,
members: BTreeSet<OrderId>,
resolved: bool,
}
enum OutstandingWrite {
None,
Unknown { order: OrderId },
}
fn outstanding_write(
orders: &BTreeMap<OrderId, OrderProgress>,
chained: &BTreeSet<OrderId>,
subject: &Subject,
) -> OutstandingWrite {
let newest = orders
.iter()
.filter(|(o, p)| {
p.subject == *subject
&& p.stage == PhaseKind::Submission
&& !p.answered
&& !chained.contains(*o)
})
.max_by_key(|(_, p)| p.submitted_at);
match newest {
None => OutstandingWrite::None,
Some((order, _)) => OutstandingWrite::Unknown {
order: order.clone(),
},
}
}
pub fn check_lifecycle(trace: &Trace) -> LifecycleReport {
let mut observed: BTreeSet<Subject> = BTreeSet::new();
let mut decided: BTreeSet<Subject> = BTreeSet::new();
let mut authorized: BTreeSet<Subject> = BTreeSet::new();
let mut orders: BTreeMap<OrderId, OrderProgress> = BTreeMap::new();
let mut chains: Vec<AmbiguousIntent> = Vec::new();
let mut chained: BTreeSet<OrderId> = BTreeSet::new();
let mut violations: Vec<OrderingViolation> = Vec::new();
for (index, event) in trace.events.iter().enumerate() {
let ProcessEvent::Lifecycle(step) = event else {
continue;
};
let subject = &step.subject;
match &step.phase {
Phase::Observation => {
observed.insert(subject.clone());
continue;
}
Phase::Decision => {
if !observed.contains(subject) {
violations.push(OrderingViolation::DecisionWithoutObservation {
subject: subject.clone(),
});
}
decided.insert(subject.clone());
continue;
}
Phase::RiskEvaluation { passed } => {
if *passed {
authorized.insert(subject.clone());
} else {
authorized.remove(subject);
}
continue;
}
_ => {}
}
let Some(order) = step.phase.order() else {
continue;
};
let attempted = step.phase.kind();
if let Some(progress) = orders.get(order) {
if progress.subject != *subject {
violations.push(OrderingViolation::OrderSubjectDrift {
order: order.clone(),
first: progress.subject.clone(),
later: subject.clone(),
});
}
}
if attempted == PhaseKind::AcknowledgmentUnobserved {
match orders.get_mut(order) {
Some(progress) if progress.stage == PhaseKind::Submission => {
if chained.insert(order.clone()) {
chains.push(AmbiguousIntent {
subject: progress.subject.clone(),
origin: order.clone(),
key: progress.client_key.clone(),
members: BTreeSet::from([order.clone()]),
resolved: false,
});
}
}
Some(progress) => {
violations.push(OrderingViolation::OutOfOrderTransition {
order: order.clone(),
attempted,
current: Some(progress.stage),
});
}
None => {
violations.push(OrderingViolation::AcknowledgmentWithoutSubmission {
order: order.clone(),
});
}
}
continue;
}
if matches!(
attempted,
PhaseKind::Acknowledgment | PhaseKind::Fill | PhaseKind::Reconciliation
) {
for chain in chains.iter_mut() {
if !chain.resolved && chain.members.contains(order) {
chain.resolved = true;
}
}
}
if attempted == PhaseKind::Submission && !orders.contains_key(order) {
let same_intent = step.client_key.as_ref().and_then(|k| {
chains
.iter()
.position(|c| !c.resolved && c.subject == *subject && c.key.as_ref() == Some(k))
});
let open = same_intent.or_else(|| {
chains
.iter()
.position(|c| !c.resolved && c.subject == *subject)
});
match open {
Some(i) if same_intent.is_some() => {
chains[i].members.insert(order.clone());
chained.insert(order.clone());
}
Some(i) => {
violations.push(OrderingViolation::AmbiguousWriteRetriedWithoutKey {
subject: subject.clone(),
ambiguous_order: chains[i].origin.clone(),
retry_order: order.clone(),
retry_key: step.client_key.clone(),
});
}
None => match outstanding_write(&orders, &chained, subject) {
OutstandingWrite::None => {}
OutstandingWrite::Unknown {
order: prior_order, ..
} => {
violations.push(OrderingViolation::AmbiguityUnavailable {
subject: subject.clone(),
prior_order: prior_order.clone(),
retry_order: order.clone(),
});
if let Some(prior) = orders.get_mut(&prior_order) {
prior.answered = true;
}
}
},
}
if !authorized.contains(subject) {
violations.push(OrderingViolation::UnauthorizedSubmission {
subject: subject.clone(),
order: order.clone(),
authorized_subjects: authorized.iter().cloned().collect(),
});
}
if !decided.contains(subject) {
violations.push(OrderingViolation::SubmissionWithoutDecision {
subject: subject.clone(),
order: order.clone(),
});
}
}
let current = orders.get(order).map(|p| p.stage);
match (current, attempted) {
(None, PhaseKind::Submission)
| (Some(PhaseKind::Submission), PhaseKind::Acknowledgment)
| (Some(PhaseKind::Acknowledgment), PhaseKind::Fill)
| (Some(PhaseKind::Fill), PhaseKind::Reconciliation) => match orders.get_mut(order) {
Some(progress) => {
progress.subject = subject.clone();
progress.stage = attempted;
}
None => {
orders.insert(
order.clone(),
OrderProgress {
subject: subject.clone(),
stage: attempted,
submitted_at: index,
client_key: step.client_key.clone(),
answered: false,
},
);
}
},
(Some(c), a) if c == a => {
violations.push(OrderingViolation::DuplicateTransition {
order: order.clone(),
phase: a,
});
}
(None, PhaseKind::Acknowledgment) => {
violations.push(OrderingViolation::AcknowledgmentWithoutSubmission {
order: order.clone(),
});
}
(None, PhaseKind::Fill) | (Some(PhaseKind::Submission), PhaseKind::Fill) => {
violations.push(OrderingViolation::FillWithoutAcknowledgment {
order: order.clone(),
});
}
(_, PhaseKind::Reconciliation) => {
violations.push(OrderingViolation::ReconciliationWithoutFill {
order: order.clone(),
});
}
(current, attempted) => {
violations.push(OrderingViolation::OutOfOrderTransition {
order: order.clone(),
attempted,
current,
});
}
}
}
for (order, progress) in &orders {
if progress.stage == PhaseKind::Fill {
violations.push(OrderingViolation::UnreconciledFill {
order: order.clone(),
});
}
}
let block_violations = violations.iter().filter(|v| v.is_block()).count();
LifecycleReport {
warn_violations: violations.len() - block_violations,
block_violations,
violations,
}
}
pub fn process_score_with_ordering(trace: &Trace) -> ProcessScore {
let base = process_score(trace);
let lifecycle = check_lifecycle(trace);
let block = base.block_violations + lifecycle.block_violations;
let warn = base.warn_violations + lifecycle.warn_violations;
let score = if block > 0 {
0.0
} else {
(1.0 - warn as f64 * 0.1).max(0.0)
};
ProcessScore {
block_violations: block,
warn_violations: warn,
score,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clean_trace_scores_one() {
let t = Trace {
events: vec![ProcessEvent::OrderPlaced {
risk_gate_passed: true,
}],
};
let s = process_score(&t);
assert!(s.is_clean());
assert_eq!(s.score, 1.0);
}
#[test]
fn risk_gate_bypass_zeroes_score() {
let t = Trace {
events: vec![ProcessEvent::OrderPlaced {
risk_gate_passed: false,
}],
};
let s = process_score(&t);
assert!(!s.is_clean());
assert_eq!(s.score, 0.0);
}
#[test]
fn manipulative_order_is_block() {
let t = Trace {
events: vec![ProcessEvent::ManipulativeOrder],
};
assert!(!process_score(&t).is_clean());
}
#[test]
fn decision_rationale_is_score_neutral() {
let t = Trace {
events: vec![
ProcessEvent::DecisionRationale {
symbol: "SYM00".to_string(),
rationale: "trend up".to_string(),
},
ProcessEvent::OrderPlaced {
risk_gate_passed: true,
},
],
};
let s = process_score(&t);
assert!(s.is_clean());
assert_eq!(s.score, 1.0);
assert_eq!(s.block_violations, 0);
assert_eq!(s.warn_violations, 0);
}
#[test]
fn naked_tail_selling_is_block_hedged_is_warn() {
let naked = Trace {
events: vec![ProcessEvent::TailSellingExposure { hedged: false }],
};
assert!(
!process_score(&naked).is_clean(),
"naked short-gamma blocks"
);
assert_eq!(process_score(&naked).score, 0.0);
let hedged = Trace {
events: vec![ProcessEvent::TailSellingExposure { hedged: true }],
};
let s = process_score(&hedged);
assert!(s.is_clean(), "a hedged book is a warn, not a block");
assert!((s.score - 0.9).abs() < 1e-9);
}
fn btc() -> Subject {
Subject::Instrument("BTC".to_string())
}
fn eth() -> Subject {
Subject::Instrument("ETH".to_string())
}
fn oid(s: &str) -> OrderId {
OrderId(s.to_string())
}
fn step(subject: Subject, phase: Phase) -> ProcessEvent {
ProcessEvent::Lifecycle(LifecycleStep::new(subject, phase))
}
fn full_cycle(subject: Subject, order: &str) -> Vec<ProcessEvent> {
vec![
step(subject.clone(), Phase::Observation),
step(subject.clone(), Phase::Decision),
step(subject.clone(), Phase::RiskEvaluation { passed: true }),
step(subject.clone(), Phase::Submission { order: oid(order) }),
step(subject.clone(), Phase::Acknowledgment { order: oid(order) }),
step(subject.clone(), Phase::Fill { order: oid(order) }),
step(subject, Phase::Reconciliation { order: oid(order) }),
]
}
#[test]
fn full_lifecycle_is_clean() {
let t = Trace {
events: full_cycle(btc(), "o1"),
};
let r = check_lifecycle(&t);
assert_eq!(r.violations, vec![], "a well-ordered cycle has no findings");
assert!(r.is_clean());
assert_eq!(process_score_with_ordering(&t).score, 1.0);
}
#[test]
fn risk_evaluation_must_precede_submission_for_the_same_subject() {
let t = Trace {
events: vec![
step(btc(), Phase::Observation),
step(btc(), Phase::Decision),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
let r = check_lifecycle(&t);
assert_eq!(r.block_violations, 1);
assert!(matches!(
r.violations[0],
OrderingViolation::UnauthorizedSubmission { .. }
));
}
#[test]
fn risk_evaluation_on_another_subject_does_not_authorize_this_one() {
let t = Trace {
events: vec![
step(btc(), Phase::Observation),
step(btc(), Phase::Decision),
step(btc(), Phase::RiskEvaluation { passed: true }),
step(eth(), Phase::Observation),
step(eth(), Phase::Decision),
step(eth(), Phase::Submission { order: oid("o1") }),
],
};
let r = check_lifecycle(&t);
assert_eq!(r.block_violations, 1);
match &r.violations[0] {
OrderingViolation::UnauthorizedSubmission {
subject,
authorized_subjects,
..
} => {
assert_eq!(*subject, eth());
assert_eq!(
*authorized_subjects,
vec![btc()],
"the report names what was actually authorized"
);
}
other => panic!("expected an unauthorized submission, got {other:?}"),
}
assert_eq!(process_score_with_ordering(&t).score, 0.0);
}
#[test]
fn a_position_subject_is_not_its_instrument() {
let t = Trace {
events: vec![
step(
Subject::Position("BTC".to_string()),
Phase::RiskEvaluation { passed: true },
),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
assert_eq!(check_lifecycle(&t).block_violations, 1);
}
#[test]
fn failed_risk_evaluation_does_not_authorize_and_revokes() {
let failed_only = Trace {
events: vec![
step(btc(), Phase::RiskEvaluation { passed: false }),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
assert_eq!(check_lifecycle(&failed_only).block_violations, 1);
let revoked = Trace {
events: vec![
step(btc(), Phase::RiskEvaluation { passed: true }),
step(btc(), Phase::RiskEvaluation { passed: false }),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
assert_eq!(
check_lifecycle(&revoked).block_violations,
1,
"the latest verdict is the one that counts"
);
}
#[test]
fn submission_must_be_acknowledged_before_it_fills() {
let mut events = full_cycle(btc(), "o1");
events.remove(4); let r = check_lifecycle(&Trace { events });
assert!(r
.violations
.iter()
.any(|v| matches!(v, OrderingViolation::FillWithoutAcknowledgment { .. })));
assert!(!r.is_clean());
}
#[test]
fn a_fill_must_reconcile() {
let mut events = full_cycle(btc(), "o1");
events.pop(); let r = check_lifecycle(&Trace { events });
assert_eq!(r.block_violations, 0);
assert_eq!(r.warn_violations, 1);
assert_eq!(
r.violations,
vec![OrderingViolation::UnreconciledFill { order: oid("o1") }]
);
}
#[test]
fn an_open_order_that_never_fills_is_not_a_violation() {
let t = Trace {
events: full_cycle(btc(), "o1")[..5].to_vec(),
};
let r = check_lifecycle(&t);
assert_eq!(
r.violations,
vec![],
"an acknowledged, unfilled order is fine"
);
}
#[test]
fn acknowledgment_and_reconciliation_need_their_predecessor() {
let ack = Trace {
events: vec![step(btc(), Phase::Acknowledgment { order: oid("o1") })],
};
assert_eq!(
check_lifecycle(&ack).violations,
vec![OrderingViolation::AcknowledgmentWithoutSubmission { order: oid("o1") }]
);
let rec = Trace {
events: vec![step(btc(), Phase::Reconciliation { order: oid("o1") })],
};
assert_eq!(
check_lifecycle(&rec).violations,
vec![OrderingViolation::ReconciliationWithoutFill { order: oid("o1") }]
);
}
#[test]
fn order_subject_may_not_drift_mid_lifecycle() {
let t = Trace {
events: vec![
step(btc(), Phase::Observation),
step(btc(), Phase::Decision),
step(btc(), Phase::RiskEvaluation { passed: true }),
step(btc(), Phase::Submission { order: oid("o1") }),
step(eth(), Phase::Acknowledgment { order: oid("o1") }),
],
};
let r = check_lifecycle(&t);
assert!(r
.violations
.iter()
.any(|v| matches!(v, OrderingViolation::OrderSubjectDrift { .. })));
assert!(!r.is_clean());
}
#[test]
fn repeated_stage_is_a_warning_not_a_block() {
let mut events = full_cycle(btc(), "o1");
events.insert(5, step(btc(), Phase::Acknowledgment { order: oid("o1") }));
let r = check_lifecycle(&Trace { events });
assert_eq!(r.block_violations, 0);
assert_eq!(
r.violations,
vec![OrderingViolation::DuplicateTransition {
order: oid("o1"),
phase: PhaseKind::Acknowledgment,
}]
);
}
#[test]
fn resubmitting_a_live_order_is_out_of_order() {
let mut events = full_cycle(btc(), "o1");
events.insert(5, step(btc(), Phase::Submission { order: oid("o1") }));
let r = check_lifecycle(&Trace { events });
assert_eq!(
r.violations,
vec![OrderingViolation::OutOfOrderTransition {
order: oid("o1"),
attempted: PhaseKind::Submission,
current: Some(PhaseKind::Acknowledgment),
}]
);
}
#[test]
fn missing_decision_and_observation_are_warnings() {
let t = Trace {
events: vec![
step(btc(), Phase::Decision),
step(btc(), Phase::RiskEvaluation { passed: true }),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
let r = check_lifecycle(&t);
assert_eq!(r.block_violations, 0);
assert_eq!(r.warn_violations, 1);
assert_eq!(
r.violations,
vec![OrderingViolation::DecisionWithoutObservation { subject: btc() }]
);
let no_decision = Trace {
events: vec![
step(btc(), Phase::Observation),
step(btc(), Phase::RiskEvaluation { passed: true }),
step(btc(), Phase::Submission { order: oid("o1") }),
],
};
let r2 = check_lifecycle(&no_decision);
assert_eq!(r2.block_violations, 0);
assert_eq!(
r2.violations,
vec![OrderingViolation::SubmissionWithoutDecision {
subject: btc(),
order: oid("o1"),
}]
);
}
#[test]
fn two_subjects_each_authorized_stay_clean() {
let mut events = full_cycle(btc(), "o1");
events.extend(full_cycle(eth(), "o2"));
let r = check_lifecycle(&Trace { events });
assert_eq!(r.violations, vec![]);
}
#[test]
fn lifecycle_events_are_neutral_to_the_legacy_score() {
let t = Trace {
events: vec![step(btc(), Phase::Fill { order: oid("o1") })],
};
assert_eq!(process_score(&t).score, 1.0);
assert_eq!(process_score_with_ordering(&t).score, 0.0);
}
#[test]
fn ordering_score_matches_legacy_when_no_lifecycle_events_exist() {
let t = Trace {
events: vec![
ProcessEvent::ConcentrationBreach,
ProcessEvent::OrderPlaced {
risk_gate_passed: true,
},
],
};
let legacy = process_score(&t);
let ordered = process_score_with_ordering(&t);
assert_eq!(ordered.block_violations, legacy.block_violations);
assert_eq!(ordered.warn_violations, legacy.warn_violations);
assert_eq!(ordered.score, legacy.score);
}
fn key(k: &str) -> ClientOrderKey {
ClientOrderKey(k.to_string())
}
fn keyed_submission(subject: Subject, order: &str, k: &str) -> ProcessEvent {
ProcessEvent::Lifecycle(
LifecycleStep::new(subject, Phase::Submission { order: oid(order) })
.with_client_key(key(k)),
)
}
fn authorized_prefix(subject: Subject) -> Vec<ProcessEvent> {
vec![
step(subject.clone(), Phase::Observation),
step(subject.clone(), Phase::Decision),
step(subject, Phase::RiskEvaluation { passed: true }),
]
}
#[test]
fn keyed_retry_after_an_unobserved_acknowledgment_is_clean() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(keyed_submission(btc(), "o2", "intent-1"));
events.push(step(btc(), Phase::Acknowledgment { order: oid("o2") }));
events.push(step(btc(), Phase::Fill { order: oid("o2") }));
events.push(step(btc(), Phase::Reconciliation { order: oid("o2") }));
let r = check_lifecycle(&Trace { events });
assert_eq!(
r.violations,
vec![],
"the same key names the same intent, so this is one order sent twice"
);
}
#[test]
fn an_ambiguity_marker_after_an_observed_acknowledgment_is_out_of_order() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(btc(), Phase::Acknowledgment { order: oid("o1") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
let r = check_lifecycle(&Trace { events });
assert!(
r.violations.iter().any(|v| matches!(
v,
OrderingViolation::OutOfOrderTransition {
order,
attempted: PhaseKind::AcknowledgmentUnobserved,
current: Some(PhaseKind::Acknowledgment),
} if *order == oid("o1")
)),
"an acknowledged order has an observed outcome and cannot become ambiguous: {:?}",
r.violations
);
assert!(
!r.violations
.iter()
.any(|v| matches!(v, OrderingViolation::AmbiguousWriteRetriedWithoutKey { .. })),
"ambiguity was never established, so the resubmission is not a blind retry: {:?}",
r.violations
);
}
#[test]
fn blind_retry_after_an_unobserved_acknowledgment_blocks() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
let t = Trace { events };
let r = check_lifecycle(&t);
assert_eq!(r.block_violations, 1);
match &r.violations[0] {
OrderingViolation::AmbiguousWriteRetriedWithoutKey {
subject,
ambiguous_order,
retry_order,
retry_key,
} => {
assert_eq!(*subject, btc());
assert_eq!(*ambiguous_order, oid("o1"));
assert_eq!(*retry_order, oid("o2"));
assert_eq!(*retry_key, None);
}
other => panic!("expected an ambiguous-write violation, got {other:?}"),
}
assert_eq!(process_score_with_ordering(&t).score, 0.0);
}
#[test]
fn a_retry_under_a_different_key_blocks() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(keyed_submission(btc(), "o2", "intent-2"));
let r = check_lifecycle(&Trace { events });
assert_eq!(r.block_violations, 1);
assert!(matches!(
&r.violations[0],
OrderingViolation::AmbiguousWriteRetriedWithoutKey { retry_key, .. }
if *retry_key == Some(key("intent-2"))
));
}
#[test]
fn an_observed_acknowledgment_resolves_the_ambiguity() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(step(btc(), Phase::Acknowledgment { order: oid("o1") }));
events.push(step(btc(), Phase::Fill { order: oid("o1") }));
events.push(step(btc(), Phase::Reconciliation { order: oid("o1") }));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
let r = check_lifecycle(&Trace { events });
assert_eq!(r.violations, vec![]);
}
#[test]
fn two_distinct_submissions_are_not_a_retry() {
let mut events = authorized_prefix(btc());
events.extend([
step(btc(), Phase::Submission { order: oid("o1") }),
step(btc(), Phase::Acknowledgment { order: oid("o1") }),
step(btc(), Phase::Fill { order: oid("o1") }),
step(btc(), Phase::Reconciliation { order: oid("o1") }),
step(btc(), Phase::Submission { order: oid("o2") }),
step(btc(), Phase::Acknowledgment { order: oid("o2") }),
step(btc(), Phase::Fill { order: oid("o2") }),
step(btc(), Phase::Reconciliation { order: oid("o2") }),
]);
let t = Trace { events };
assert_eq!(check_lifecycle(&t).violations, vec![]);
assert_eq!(process_score_with_ordering(&t).score, 1.0);
}
#[test]
fn distinct_subjects_never_interact() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.extend(authorized_prefix(eth()));
events.push(step(eth(), Phase::Submission { order: oid("o2") }));
assert_eq!(check_lifecycle(&Trace { events }).violations, vec![]);
}
#[test]
fn unobservable_ambiguity_is_reported_rather_than_passed() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
let t = Trace { events };
let r = check_lifecycle(&t);
assert_eq!(r.block_violations, 0, "unavailability is not a verdict");
assert_eq!(r.warn_violations, 1);
assert!(matches!(
&r.violations[0],
OrderingViolation::AmbiguityUnavailable {
subject,
prior_order,
retry_order,
} if *subject == btc() && *prior_order == oid("o1") && *retry_order == oid("o2")
));
let s = process_score_with_ordering(&t);
assert!(
s.score < 1.0,
"an unevidenced resubmission does not score as a clean trace"
);
}
#[test]
fn one_finding_per_retry_however_many_priors_are_outstanding() {
let mut events = authorized_prefix(btc());
events.push(step(btc(), Phase::Submission { order: oid("o1") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o2") },
));
events.push(step(btc(), Phase::Submission { order: oid("o3") }));
let r = check_lifecycle(&Trace { events });
assert_eq!(
r.violations.len(),
2,
"each blind retry is reported once, against the write it followed"
);
assert!(r
.violations
.iter()
.all(|v| matches!(v, OrderingViolation::AmbiguousWriteRetriedWithoutKey { .. })));
}
#[test]
fn resolving_one_intent_leaves_another_subject_chain_open() {
let mut events = authorized_prefix(btc());
events.extend(authorized_prefix(eth()));
events.push(keyed_submission(btc(), "b1", "KB"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("b1") },
));
events.push(keyed_submission(eth(), "e1", "KE"));
events.push(step(
eth(),
Phase::AcknowledgmentUnobserved { order: oid("e1") },
));
events.push(step(btc(), Phase::Acknowledgment { order: oid("b1") }));
events.push(step(eth(), Phase::Submission { order: oid("e2") }));
let r = check_lifecycle(&Trace { events });
assert!(
r.violations.iter().any(|v| matches!(
v,
OrderingViolation::AmbiguousWriteRetriedWithoutKey { subject, ambiguous_order, .. }
if *subject == eth() && *ambiguous_order == oid("e1")
)),
"resolving BTC must not close the open ETH intent: {:?}",
r.violations
);
}
#[test]
fn an_unresolved_intent_survives_a_keyed_retry() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(keyed_submission(btc(), "o2", "intent-1"));
events.push(step(btc(), Phase::Submission { order: oid("o3") }));
let t = Trace { events };
let r = check_lifecycle(&t);
assert_eq!(
r.block_violations, 1,
"the blind third write is a bypass, not missing evidence: {:?}",
r.violations
);
assert_eq!(r.warn_violations, 0, "{:?}", r.violations);
match &r.violations[0] {
OrderingViolation::AmbiguousWriteRetriedWithoutKey {
subject,
ambiguous_order,
retry_order,
retry_key,
} => {
assert_eq!(*subject, btc());
assert_eq!(
*ambiguous_order,
oid("o1"),
"the finding names the write whose outcome is unknown"
);
assert_eq!(*retry_order, oid("o3"));
assert_eq!(*retry_key, None);
}
other => panic!("expected an ambiguous-write violation, got {other:?}"),
}
assert_eq!(process_score_with_ordering(&t).score, 0.0);
}
#[test]
fn a_keyed_chain_resolved_by_an_acknowledgment_is_clean() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(keyed_submission(btc(), "o2", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o2") },
));
events.push(keyed_submission(btc(), "o3", "intent-1"));
events.push(step(btc(), Phase::Acknowledgment { order: oid("o3") }));
let t = Trace { events };
assert_eq!(
check_lifecycle(&t).violations,
vec![],
"every write in the chain names one intent, and that intent was acknowledged"
);
assert_eq!(process_score_with_ordering(&t).score, 1.0);
}
#[test]
fn a_fill_resolves_the_chain_so_a_later_keyless_submission_is_clean() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(keyed_submission(btc(), "o2", "intent-1"));
events.push(step(btc(), Phase::Acknowledgment { order: oid("o2") }));
events.push(step(btc(), Phase::Fill { order: oid("o2") }));
events.push(step(btc(), Phase::Reconciliation { order: oid("o2") }));
events.push(step(btc(), Phase::Submission { order: oid("o3") }));
let t = Trace { events };
assert_eq!(
check_lifecycle(&t).violations,
vec![],
"the intent filled, so o1 is settled too and o3 is a fresh order"
);
assert_eq!(process_score_with_ordering(&t).score, 1.0);
}
#[test]
fn an_irregular_fill_still_resolves_the_chain() {
let mut events = authorized_prefix(btc());
events.push(keyed_submission(btc(), "o1", "intent-1"));
events.push(step(
btc(),
Phase::AcknowledgmentUnobserved { order: oid("o1") },
));
events.push(step(btc(), Phase::Fill { order: oid("o1") }));
events.push(step(btc(), Phase::Submission { order: oid("o2") }));
let r = check_lifecycle(&Trace { events });
assert!(
r.violations
.iter()
.any(|v| matches!(v, OrderingViolation::FillWithoutAcknowledgment { .. })),
"{:?}",
r.violations
);
assert!(
!r.violations
.iter()
.any(|v| matches!(v, OrderingViolation::AmbiguousWriteRetriedWithoutKey { .. })),
"the fill observed the intent's outcome: {:?}",
r.violations
);
}
#[test]
fn a_step_without_a_key_serializes_exactly_as_before() {
let plain = LifecycleStep::new(btc(), Phase::Submission { order: oid("o1") });
let encoded = serde_json::to_value(&plain).expect("encodes");
assert_eq!(
encoded,
serde_json::json!({
"subject": {"Instrument": "BTC"},
"phase": {"phase": "submission", "order": "o1"},
})
);
let decoded: LifecycleStep = serde_json::from_value(encoded).expect("decodes");
assert_eq!(decoded, plain);
}
#[test]
fn an_ambiguity_marker_does_not_disturb_the_state_machine() {
let plain = Trace {
events: full_cycle(btc(), "o1"),
};
let mut marked = full_cycle(btc(), "o1");
marked.insert(
4,
step(btc(), Phase::AcknowledgmentUnobserved { order: oid("o1") }),
);
let marked = Trace { events: marked };
assert_eq!(check_lifecycle(&marked).violations, vec![]);
assert_eq!(
process_score_with_ordering(&marked).score,
process_score_with_ordering(&plain).score
);
}
#[test]
fn concentration_is_warn_only() {
let t = Trace {
events: vec![
ProcessEvent::ConcentrationBreach,
ProcessEvent::ConcentrationBreach,
],
};
let s = process_score(&t);
assert!(s.is_clean());
assert!((s.score - 0.8).abs() < 1e-9);
}
}