Validate with declarative policy resolution. This is the new core validation method. It attempts to resolve violations via policy before falling back to the DLQ. # Arguments `state` — current CRDT state `peer_id` — source peer ID `change` — proposed change `delta_bytes` — raw delta bytes `hlc_timestamp` — Hybrid Logical Clock timestamp of the incoming write Returns: - `Ok(PolicyResolution::Aut
(
&mut self,
state: &CrdtState,
peer_id: u64,
auth: CrdtAuthContext,
change: &ProposedChange,
delta_bytes: Vec<u8>,
hlc_timestamp: u64,
)
| 32 | /// - `Ok(PolicyResolution::Escalate)` if escalating to DLQ (entry already enqueued) |
| 33 | /// - `Err(_)` if an internal error occurred |
| 34 | pub fn validate_with_policy( |
| 35 | &mut self, |
| 36 | state: &CrdtState, |
| 37 | peer_id: u64, |
| 38 | auth: CrdtAuthContext, |
| 39 | change: &ProposedChange, |
| 40 | delta_bytes: Vec<u8>, |
| 41 | hlc_timestamp: u64, |
| 42 | ) -> Result<PolicyResolution> { |
| 43 | match self.validate(state, change) { |
| 44 | ValidationOutcome::Accepted => { |
| 45 | // No violation; return synthetic "auto-resolved" to maintain API consistency |
| 46 | Ok(PolicyResolution::AutoResolved( |
| 47 | ResolvedAction::OverwriteExisting, |
| 48 | )) |
| 49 | } |
| 50 | ValidationOutcome::Rejected(violations) => { |
| 51 | // Exactly one violation per constraint (current design) |
| 52 | let v = &violations[0]; |
| 53 | let constraint = self |
| 54 | .constraints |
| 55 | .all() |
| 56 | .iter() |
| 57 | .find(|c| c.name == v.constraint_name) |
| 58 | .cloned() |
| 59 | .unwrap_or_else(|| Constraint { |
| 60 | name: v.constraint_name.clone(), |
| 61 | collection: change.collection.clone(), |
| 62 | field: String::new(), |
| 63 | kind: ConstraintKind::NotNull, |
| 64 | }); |
| 65 | |
| 66 | let policy = self.policies.get_owned(&change.collection); |
| 67 | let policy_for_kind = policy.for_kind(&constraint.kind); |
| 68 | |
| 69 | // Attempt policy resolution |
| 70 | match policy_for_kind { |
| 71 | ConflictPolicy::LastWriterWins => { |
| 72 | tracing::info!( |
| 73 | constraint = %v.constraint_name, |
| 74 | collection = %change.collection, |
| 75 | timestamp = hlc_timestamp, |
| 76 | reason = %v.reason, |
| 77 | "resolved via LAST_WRITER_WINS" |
| 78 | ); |
| 79 | Ok(PolicyResolution::AutoResolved( |
| 80 | ResolvedAction::OverwriteExisting, |
| 81 | )) |
| 82 | } |
| 83 | |
| 84 | ConflictPolicy::RenameSuffix => { |
| 85 | let counter_key = (change.collection.clone(), constraint.field.clone()); |
| 86 | let suffix = self.suffix_counter.entry(counter_key).or_insert(0); |
| 87 | *suffix += 1; |
| 88 | let new_value = format!( |
| 89 | "{}_{}", |
| 90 | change |
| 91 | .fields |