1use std::collections::HashSet;
2
3use crate::capability::scope::MonetaryAmount;
4use crate::error::AutonomyContractError;
5use crate::model::*;
6
7pub fn validate_autonomous_pricing_input(
8 input: &AutonomousPricingInputArtifact,
9) -> Result<(), AutonomyContractError> {
10 if input.schema != CHIO_AUTONOMOUS_PRICING_INPUT_SCHEMA {
11 return Err(AutonomyContractError::UnsupportedSchema(
12 input.schema.clone(),
13 ));
14 }
15 ensure_non_empty(&input.input_id, "autonomous_pricing_input.input_id")?;
16 ensure_non_empty(&input.subject_key, "autonomous_pricing_input.subject_key")?;
17 ensure_non_empty(&input.provider_id, "autonomous_pricing_input.provider_id")?;
18 validate_currency_code(&input.currency, "autonomous_pricing_input.currency")?;
19 validate_positive_money(
20 &input.requested_coverage_amount,
21 "autonomous_pricing_input.requested_coverage_amount",
22 )?;
23 if !money_currency_matches_declared(&input.requested_coverage_amount, &input.currency) {
24 return Err(AutonomyContractError::InvalidDecision(
25 "autonomous pricing input coverage amount currency must match currency".to_string(),
26 ));
27 }
28 if input.receipt_history_window_secs == 0 {
29 return Err(AutonomyContractError::InvalidDecision(
30 "autonomous pricing input receipt_history_window_secs must be non-zero".to_string(),
31 ));
32 }
33 if input.reputation_score_bps > 10_000 {
34 return Err(AutonomyContractError::InvalidDecision(
35 "autonomous pricing input reputation_score_bps must be <= 10000".to_string(),
36 ));
37 }
38 if input.available_capital_units == 0 {
39 return Err(AutonomyContractError::InvalidDecision(
40 "autonomous pricing input available_capital_units must be non-zero".to_string(),
41 ));
42 }
43 if input.evidence_refs.is_empty() {
44 return Err(AutonomyContractError::MissingField(
45 "autonomous_pricing_input.evidence_refs",
46 ));
47 }
48
49 let mut ids = HashSet::new();
50 let mut kinds = HashSet::new();
51 for evidence in &input.evidence_refs {
52 ensure_non_empty(
53 &evidence.reference_id,
54 "autonomous_pricing_input.evidence_refs.reference_id",
55 )?;
56 if !ids.insert(evidence.reference_id.as_str()) {
57 return Err(AutonomyContractError::DuplicateValue(
58 evidence.reference_id.clone(),
59 ));
60 }
61 kinds.insert(evidence.kind);
62 }
63 for required in [
64 AutonomousEvidenceKind::UnderwritingDecision,
65 AutonomousEvidenceKind::ExposureLedger,
66 AutonomousEvidenceKind::CreditScorecard,
67 AutonomousEvidenceKind::CapitalBook,
68 ] {
69 if !kinds.contains(&required) {
70 return Err(AutonomyContractError::UnknownReference(format!(
71 "autonomous pricing input missing required evidence {:?}",
72 required
73 )));
74 }
75 }
76 if (input.pending_loss_units > 0 || input.settled_loss_units > 0)
77 && !kinds.contains(&AutonomousEvidenceKind::CreditLossLifecycle)
78 {
79 return Err(AutonomyContractError::UnknownReference(
80 "autonomous pricing input with loss units must include credit_loss_lifecycle evidence"
81 .to_string(),
82 ));
83 }
84 if input.latest_web3_settlement_state.is_some()
85 && !kinds.contains(&AutonomousEvidenceKind::Web3SettlementReceipt)
86 {
87 return Err(AutonomyContractError::UnknownReference(
88 "autonomous pricing input with latest_web3_settlement_state must include web3_settlement_receipt evidence"
89 .to_string(),
90 ));
91 }
92 Ok(())
93}
94
95pub fn validate_autonomous_pricing_authority_envelope(
96 envelope: &AutonomousPricingAuthorityEnvelopeArtifact,
97) -> Result<(), AutonomyContractError> {
98 if envelope.schema != CHIO_AUTONOMOUS_PRICING_AUTHORITY_ENVELOPE_SCHEMA {
99 return Err(AutonomyContractError::UnsupportedSchema(
100 envelope.schema.clone(),
101 ));
102 }
103 ensure_non_empty(
104 &envelope.envelope_id,
105 "autonomous_authority_envelope.envelope_id",
106 )?;
107 ensure_non_empty(
108 &envelope.subject_key,
109 "autonomous_authority_envelope.subject_key",
110 )?;
111 ensure_non_empty(
112 &envelope.provider_id,
113 "autonomous_authority_envelope.provider_id",
114 )?;
115 validate_currency_code(&envelope.currency, "autonomous_authority_envelope.currency")?;
116 if envelope.permitted_actions.is_empty() {
117 return Err(AutonomyContractError::MissingField(
118 "autonomous_authority_envelope.permitted_actions",
119 ));
120 }
121 ensure_unique_copy_values(
122 &envelope.permitted_actions,
123 "autonomous_authority_envelope.permitted_actions",
124 )?;
125 if envelope.authority_chain_refs.is_empty() {
126 return Err(AutonomyContractError::MissingField(
127 "autonomous_authority_envelope.authority_chain_refs",
128 ));
129 }
130 ensure_unique_strings(
131 &envelope.authority_chain_refs,
132 "autonomous_authority_envelope.authority_chain_refs",
133 )?;
134 validate_positive_money(
135 &envelope.max_coverage_amount,
136 "autonomous_authority_envelope.max_coverage_amount",
137 )?;
138 validate_positive_money(
139 &envelope.max_premium_amount,
140 "autonomous_authority_envelope.max_premium_amount",
141 )?;
142 if !money_currency_matches_declared(&envelope.max_coverage_amount, &envelope.currency) {
143 return Err(AutonomyContractError::InvalidEnvelope(
144 "autonomous authority max_coverage_amount currency must match envelope currency"
145 .to_string(),
146 ));
147 }
148 if !money_currency_matches_declared(&envelope.max_premium_amount, &envelope.currency) {
149 return Err(AutonomyContractError::InvalidEnvelope(
150 "autonomous authority max_premium_amount currency must match envelope currency"
151 .to_string(),
152 ));
153 }
154 if envelope.max_rate_change_bps == 0 || envelope.max_rate_change_bps > 10_000 {
155 return Err(AutonomyContractError::InvalidEnvelope(
156 "autonomous authority max_rate_change_bps must be between 1 and 10000".to_string(),
157 ));
158 }
159 if envelope.max_daily_decisions == 0 {
160 return Err(AutonomyContractError::InvalidEnvelope(
161 "autonomous authority max_daily_decisions must be non-zero".to_string(),
162 ));
163 }
164 if let Some(threshold) = envelope.requires_human_review_above_premium.as_ref() {
165 validate_positive_money(
166 threshold,
167 "autonomous_authority_envelope.requires_human_review_above_premium",
168 )?;
169 if !money_currency_matches_declared(threshold, &envelope.currency) {
170 return Err(AutonomyContractError::InvalidEnvelope(
171 "autonomous authority premium review threshold currency must match envelope currency"
172 .to_string(),
173 ));
174 }
175 if threshold.units > envelope.max_premium_amount.units {
176 return Err(AutonomyContractError::InvalidEnvelope(
177 "autonomous authority premium review threshold cannot exceed max_premium_amount"
178 .to_string(),
179 ));
180 }
181 }
182 if envelope.not_before >= envelope.not_after {
183 return Err(AutonomyContractError::InvalidEnvelope(
184 "autonomous authority not_before must be earlier than not_after".to_string(),
185 ));
186 }
187 match envelope.kind {
188 AutonomousAuthorityEnvelopeKind::OperatorPolicy => {}
189 AutonomousAuthorityEnvelopeKind::RegulatedRole => {
190 ensure_non_empty(
191 envelope.regulated_role.as_deref().unwrap_or_default(),
192 "autonomous_authority_envelope.regulated_role",
193 )?;
194 }
195 AutonomousAuthorityEnvelopeKind::DelegatedMarketAuthority => {
196 ensure_non_empty(
197 envelope
198 .delegated_authority_reference
199 .as_deref()
200 .unwrap_or_default(),
201 "autonomous_authority_envelope.delegated_authority_reference",
202 )?;
203 }
204 }
205 if envelope.automation_mode != AutonomousAutomationMode::Active
206 && envelope
207 .permitted_actions
208 .contains(&AutonomousPricingAction::Bind)
209 {
210 return Err(AutonomyContractError::InvalidEnvelope(
211 "only active automation envelopes may permit bind".to_string(),
212 ));
213 }
214 Ok(())
215}
216
217pub fn validate_autonomous_pricing_decision(
218 decision: &AutonomousPricingDecisionArtifact,
219) -> Result<(), AutonomyContractError> {
220 if decision.schema != CHIO_AUTONOMOUS_PRICING_DECISION_SCHEMA {
221 return Err(AutonomyContractError::UnsupportedSchema(
222 decision.schema.clone(),
223 ));
224 }
225 ensure_non_empty(
226 &decision.decision_id,
227 "autonomous_pricing_decision.decision_id",
228 )?;
229 validate_autonomous_pricing_input(&decision.pricing_input)?;
230 validate_autonomous_pricing_authority_envelope(&decision.authority_envelope)?;
231 validate_model_provenance(&decision.model)?;
232 validate_positive_money(
233 &decision.suggested_coverage_amount,
234 "autonomous_pricing_decision.suggested_coverage_amount",
235 )?;
236 validate_positive_money(
237 &decision.suggested_premium_amount,
238 "autonomous_pricing_decision.suggested_premium_amount",
239 )?;
240 if !money_currency_matches_declared(
241 &decision.suggested_coverage_amount,
242 &decision.authority_envelope.currency,
243 ) || !money_currency_matches_declared(
244 &decision.suggested_premium_amount,
245 &decision.authority_envelope.currency,
246 ) {
247 return Err(AutonomyContractError::InvalidDecision(
248 "autonomous pricing decision money fields must match envelope currency".to_string(),
249 ));
250 }
251 if decision.pricing_input.subject_key != decision.authority_envelope.subject_key
252 || decision.pricing_input.provider_id != decision.authority_envelope.provider_id
253 || decision.pricing_input.currency != decision.authority_envelope.currency
254 {
255 return Err(AutonomyContractError::InvalidDecision(
256 "autonomous pricing decision input must match authority envelope subject/provider/currency"
257 .to_string(),
258 ));
259 }
260 if decision.suggested_coverage_amount.units
261 > decision.authority_envelope.max_coverage_amount.units
262 {
263 return Err(AutonomyContractError::InvalidDecision(
264 "autonomous pricing decision coverage exceeds authority envelope".to_string(),
265 ));
266 }
267 if decision.suggested_premium_amount.units
268 > decision.authority_envelope.max_premium_amount.units
269 {
270 return Err(AutonomyContractError::InvalidDecision(
271 "autonomous pricing decision premium exceeds authority envelope".to_string(),
272 ));
273 }
274 if let Some(ceiling_factor_bps) = decision.suggested_ceiling_factor_bps {
275 if ceiling_factor_bps == 0 || ceiling_factor_bps > 10_000 {
276 return Err(AutonomyContractError::InvalidDecision(
277 "autonomous pricing decision suggested_ceiling_factor_bps must be between 1 and 10000"
278 .to_string(),
279 ));
280 }
281 }
282 if decision.confidence_bps == 0 || decision.confidence_bps > 10_000 {
283 return Err(AutonomyContractError::InvalidDecision(
284 "autonomous pricing decision confidence_bps must be between 1 and 10000".to_string(),
285 ));
286 }
287 if decision.explanation_factors.is_empty() {
288 return Err(AutonomyContractError::MissingField(
289 "autonomous_pricing_decision.explanation_factors",
290 ));
291 }
292 let mut factor_codes = HashSet::new();
293 for factor in &decision.explanation_factors {
294 ensure_non_empty(
295 &factor.code,
296 "autonomous_pricing_decision.explanation_factors.code",
297 )?;
298 ensure_non_empty(
299 &factor.description,
300 "autonomous_pricing_decision.explanation_factors.description",
301 )?;
302 if factor.weight_bps == 0 || factor.weight_bps > 10_000 {
303 return Err(AutonomyContractError::InvalidDecision(
304 "autonomous pricing explanation factor weight_bps must be between 1 and 10000"
305 .to_string(),
306 ));
307 }
308 if !factor_codes.insert(factor.code.as_str()) {
309 return Err(AutonomyContractError::DuplicateValue(factor.code.clone()));
310 }
311 }
312 if decision.authority_envelope.automation_mode == AutonomousAutomationMode::Shadow
313 && decision.review_state != AutonomousDecisionReviewState::ShadowOnly
314 {
315 return Err(AutonomyContractError::InvalidDecision(
316 "shadow automation decisions must use review_state shadow_only".to_string(),
317 ));
318 }
319 if decision.review_state == AutonomousDecisionReviewState::ShadowOnly
320 && decision.authority_envelope.automation_mode != AutonomousAutomationMode::Shadow
321 {
322 return Err(AutonomyContractError::InvalidDecision(
323 "review_state shadow_only requires a shadow automation envelope".to_string(),
324 ));
325 }
326 if decision.disposition == AutonomousPricingDisposition::ManualReview
327 && decision.review_state == AutonomousDecisionReviewState::AutoApproved
328 {
329 return Err(AutonomyContractError::InvalidDecision(
330 "manual-review pricing decisions cannot be auto approved".to_string(),
331 ));
332 }
333 if let Some(threshold) = decision
334 .authority_envelope
335 .requires_human_review_above_premium
336 .as_ref()
337 {
338 if decision.review_state == AutonomousDecisionReviewState::AutoApproved
339 && decision.suggested_premium_amount.units > threshold.units
340 {
341 return Err(AutonomyContractError::InvalidDecision(
342 "auto-approved pricing decisions cannot exceed the premium review threshold"
343 .to_string(),
344 ));
345 }
346 }
347 if decision.disposition == AutonomousPricingDisposition::BindWithinEnvelope {
348 if !decision
349 .authority_envelope
350 .permitted_actions
351 .contains(&AutonomousPricingAction::Bind)
352 {
353 return Err(AutonomyContractError::InvalidDecision(
354 "bind-within-envelope decisions require bind permission in the authority envelope"
355 .to_string(),
356 ));
357 }
358 if !decision
359 .authority_envelope
360 .support_boundary
361 .live_bind_supported
362 {
363 return Err(AutonomyContractError::InvalidDecision(
364 "bind-within-envelope decisions require live_bind_supported".to_string(),
365 ));
366 }
367 if decision.authority_envelope.requires_human_review_for_bind
368 && decision.review_state == AutonomousDecisionReviewState::AutoApproved
369 {
370 return Err(AutonomyContractError::InvalidDecision(
371 "bind-within-envelope decisions cannot be auto approved when human review is required for bind"
372 .to_string(),
373 ));
374 }
375 }
376 Ok(())
377}
378
379pub fn validate_capital_pool_optimization(
380 optimization: &CapitalPoolOptimizationArtifact,
381) -> Result<(), AutonomyContractError> {
382 if optimization.schema != CHIO_CAPITAL_POOL_OPTIMIZATION_SCHEMA {
383 return Err(AutonomyContractError::UnsupportedSchema(
384 optimization.schema.clone(),
385 ));
386 }
387 ensure_non_empty(
388 &optimization.optimization_id,
389 "capital_pool_optimization.optimization_id",
390 )?;
391 ensure_non_empty(
392 &optimization.subject_key,
393 "capital_pool_optimization.subject_key",
394 )?;
395 validate_currency_code(&optimization.currency, "capital_pool_optimization.currency")?;
396 ensure_non_empty(
397 &optimization.pricing_decision_ref,
398 "capital_pool_optimization.pricing_decision_ref",
399 )?;
400 ensure_non_empty(
401 &optimization.capital_book_ref,
402 "capital_pool_optimization.capital_book_ref",
403 )?;
404 if optimization.facility_refs.is_empty() {
405 return Err(AutonomyContractError::MissingField(
406 "capital_pool_optimization.facility_refs",
407 ));
408 }
409 ensure_unique_strings(
410 &optimization.facility_refs,
411 "capital_pool_optimization.facility_refs",
412 )?;
413 ensure_unique_strings(
414 &optimization.pending_claim_refs,
415 "capital_pool_optimization.pending_claim_refs",
416 )?;
417 if optimization.target_reserve_ratio_bps == 0 || optimization.target_reserve_ratio_bps > 10_000
418 {
419 return Err(AutonomyContractError::InvalidOptimization(
420 "capital pool optimization target_reserve_ratio_bps must be between 1 and 10000"
421 .to_string(),
422 ));
423 }
424 if optimization.max_facility_utilization_bps == 0
425 || optimization.max_facility_utilization_bps > 10_000
426 {
427 return Err(AutonomyContractError::InvalidOptimization(
428 "capital pool optimization max_facility_utilization_bps must be between 1 and 10000"
429 .to_string(),
430 ));
431 }
432 if optimization.max_bind_capacity_units == 0 {
433 return Err(AutonomyContractError::InvalidOptimization(
434 "capital pool optimization max_bind_capacity_units must be non-zero".to_string(),
435 ));
436 }
437 if optimization.recommendations.is_empty() {
438 return Err(AutonomyContractError::MissingField(
439 "capital_pool_optimization.recommendations",
440 ));
441 }
442 for recommendation in &optimization.recommendations {
443 ensure_non_empty(
444 &recommendation.source_ref,
445 "capital_pool_optimization.recommendations.source_ref",
446 )?;
447 ensure_non_empty(
448 &recommendation.rationale,
449 "capital_pool_optimization.recommendations.rationale",
450 )?;
451 validate_positive_money(
452 &recommendation.amount,
453 "capital_pool_optimization.recommendations.amount",
454 )?;
455 if !money_currency_matches_declared(&recommendation.amount, &optimization.currency) {
456 return Err(AutonomyContractError::InvalidOptimization(
457 "capital pool optimization recommendation amounts must match optimization currency"
458 .to_string(),
459 ));
460 }
461 if recommendation.action == CapitalOptimizationAction::ShiftCapacity
462 && recommendation
463 .destination_ref
464 .as_deref()
465 .is_none_or(|value| value.trim().is_empty())
466 {
467 return Err(AutonomyContractError::InvalidOptimization(
468 "shift-capacity recommendations require destination_ref".to_string(),
469 ));
470 }
471 }
472 Ok(())
473}
474
475pub fn validate_capital_pool_simulation_report(
476 report: &CapitalPoolSimulationReport,
477) -> Result<(), AutonomyContractError> {
478 if report.schema != CHIO_CAPITAL_POOL_SIMULATION_REPORT_SCHEMA {
479 return Err(AutonomyContractError::UnsupportedSchema(
480 report.schema.clone(),
481 ));
482 }
483 ensure_non_empty(
484 &report.simulation_id,
485 "capital_pool_simulation.simulation_id",
486 )?;
487 ensure_non_empty(&report.subject_key, "capital_pool_simulation.subject_key")?;
488 validate_currency_code(&report.currency, "capital_pool_simulation.currency")?;
489 validate_capital_pool_optimization(&report.baseline_optimization)?;
490 validate_capital_pool_optimization(&report.candidate_optimization)?;
491 if report.baseline_optimization.subject_key != report.subject_key
492 || report.candidate_optimization.subject_key != report.subject_key
493 {
494 return Err(AutonomyContractError::InvalidOptimization(
495 "capital pool simulation subject_key must match both baseline and candidate optimizations"
496 .to_string(),
497 ));
498 }
499 if report.baseline_optimization.currency != report.currency
500 || report.candidate_optimization.currency != report.currency
501 {
502 return Err(AutonomyContractError::InvalidOptimization(
503 "capital pool simulation currency must match both baseline and candidate optimizations"
504 .to_string(),
505 ));
506 }
507 if report.baseline_optimization.optimization_id == report.candidate_optimization.optimization_id
508 {
509 return Err(AutonomyContractError::DuplicateValue(
510 report.baseline_optimization.optimization_id.clone(),
511 ));
512 }
513 if !report
514 .baseline_optimization
515 .support_boundary
516 .scenario_comparison_supported
517 || !report
518 .candidate_optimization
519 .support_boundary
520 .scenario_comparison_supported
521 {
522 return Err(AutonomyContractError::InvalidOptimization(
523 "capital pool simulation requires scenario_comparison_supported on both optimizations"
524 .to_string(),
525 ));
526 }
527 if report.deltas.is_empty() {
528 return Err(AutonomyContractError::MissingField(
529 "capital_pool_simulation.deltas",
530 ));
531 }
532 for delta in &report.deltas {
533 ensure_non_empty(
534 &delta.metric_name,
535 "capital_pool_simulation.deltas.metric_name",
536 )?;
537 ensure_non_empty(
538 &delta.description,
539 "capital_pool_simulation.deltas.description",
540 )?;
541 }
542 ensure_non_empty(
543 &report.recommended_operator_action,
544 "capital_pool_simulation.recommended_operator_action",
545 )?;
546 Ok(())
547}
548
549pub fn validate_autonomous_execution_decision(
550 decision: &AutonomousExecutionDecisionArtifact,
551) -> Result<(), AutonomyContractError> {
552 if decision.schema != CHIO_AUTONOMOUS_EXECUTION_DECISION_SCHEMA {
553 return Err(AutonomyContractError::UnsupportedSchema(
554 decision.schema.clone(),
555 ));
556 }
557 for (value, field) in [
558 (&decision.execution_id, "autonomous_execution.execution_id"),
559 (
560 &decision.pricing_decision_ref,
561 "autonomous_execution.pricing_decision_ref",
562 ),
563 (
564 &decision.optimization_ref,
565 "autonomous_execution.optimization_ref",
566 ),
567 (
568 &decision.authority_envelope_ref,
569 "autonomous_execution.authority_envelope_ref",
570 ),
571 (&decision.subject_key, "autonomous_execution.subject_key"),
572 (&decision.provider_id, "autonomous_execution.provider_id"),
573 ] {
574 ensure_non_empty(value, field)?;
575 }
576 validate_currency_code(&decision.currency, "autonomous_execution.currency")?;
577 if decision.safety_gates.is_empty() {
578 return Err(AutonomyContractError::MissingField(
579 "autonomous_execution.safety_gates",
580 ));
581 }
582 let mut gate_names = HashSet::new();
583 for gate in &decision.safety_gates {
584 ensure_non_empty(&gate.name, "autonomous_execution.safety_gates.name")?;
585 ensure_non_empty(
586 &gate.description,
587 "autonomous_execution.safety_gates.description",
588 )?;
589 if !gate_names.insert(gate.name.as_str()) {
590 return Err(AutonomyContractError::DuplicateValue(gate.name.clone()));
591 }
592 }
593 validate_rollback_control(&decision.rollback_control)?;
594
595 let all_gates_passed = decision.safety_gates.iter().all(|gate| gate.passed);
596 if decision.lifecycle_state == AutonomousExecutionLifecycleState::Executed && !all_gates_passed
597 {
598 return Err(AutonomyContractError::InvalidExecution(
599 "executed autonomous actions require all safety gates to pass".to_string(),
600 ));
601 }
602 if decision.lifecycle_state == AutonomousExecutionLifecycleState::Blocked && all_gates_passed {
603 return Err(AutonomyContractError::InvalidExecution(
604 "blocked autonomous actions require at least one failed safety gate".to_string(),
605 ));
606 }
607
608 match decision.action {
609 AutonomousExecutionAction::Reprice | AutonomousExecutionAction::Renew => {
610 ensure_non_empty(
611 decision.quote_response_ref.as_deref().unwrap_or_default(),
612 "autonomous_execution.quote_response_ref",
613 )?;
614 if decision.auto_bind_decision_ref.is_some()
615 || decision.bound_coverage_ref.is_some()
616 || decision.settlement_dispatch_ref.is_some()
617 {
618 return Err(AutonomyContractError::InvalidExecution(
619 "reprice and renew automation cannot embed bind or settlement references"
620 .to_string(),
621 ));
622 }
623 }
624 AutonomousExecutionAction::Decline => {
625 if decision.auto_bind_decision_ref.is_some()
626 || decision.bound_coverage_ref.is_some()
627 || decision.settlement_dispatch_ref.is_some()
628 {
629 return Err(AutonomyContractError::InvalidExecution(
630 "decline automation cannot embed bind or settlement references".to_string(),
631 ));
632 }
633 }
634 AutonomousExecutionAction::Bind => {
635 ensure_non_empty(
636 decision.quote_response_ref.as_deref().unwrap_or_default(),
637 "autonomous_execution.quote_response_ref",
638 )?;
639 ensure_non_empty(
640 decision
641 .auto_bind_decision_ref
642 .as_deref()
643 .unwrap_or_default(),
644 "autonomous_execution.auto_bind_decision_ref",
645 )?;
646 ensure_non_empty(
647 decision.bound_coverage_ref.as_deref().unwrap_or_default(),
648 "autonomous_execution.bound_coverage_ref",
649 )?;
650 if decision.lifecycle_state == AutonomousExecutionLifecycleState::Executed {
651 ensure_non_empty(
652 decision
653 .settlement_dispatch_ref
654 .as_deref()
655 .unwrap_or_default(),
656 "autonomous_execution.settlement_dispatch_ref",
657 )?;
658 }
659 }
660 }
661
662 Ok(())
663}
664
665pub fn validate_autonomous_comparison_report(
666 report: &AutonomousComparisonReport,
667) -> Result<(), AutonomyContractError> {
668 if report.schema != CHIO_AUTONOMOUS_COMPARISON_REPORT_SCHEMA {
669 return Err(AutonomyContractError::UnsupportedSchema(
670 report.schema.clone(),
671 ));
672 }
673 ensure_non_empty(&report.comparison_id, "autonomous_comparison.comparison_id")?;
674 ensure_non_empty(
675 &report.pricing_decision_ref,
676 "autonomous_comparison.pricing_decision_ref",
677 )?;
678 ensure_non_empty(
679 &report.manual_decision_ref,
680 "autonomous_comparison.manual_decision_ref",
681 )?;
682 if report.disposition != AutonomousComparisonDisposition::Match && report.deltas.is_empty() {
683 return Err(AutonomyContractError::MissingField(
684 "autonomous_comparison.deltas",
685 ));
686 }
687 if report.disposition == AutonomousComparisonDisposition::ManualOverride {
688 ensure_non_empty(
689 report.override_reference.as_deref().unwrap_or_default(),
690 "autonomous_comparison.override_reference",
691 )?;
692 }
693 for delta in &report.deltas {
694 for (value, field) in [
695 (&delta.field, "autonomous_comparison.deltas.field"),
696 (
697 &delta.automated_value,
698 "autonomous_comparison.deltas.automated_value",
699 ),
700 (
701 &delta.manual_value,
702 "autonomous_comparison.deltas.manual_value",
703 ),
704 (
705 &delta.description,
706 "autonomous_comparison.deltas.description",
707 ),
708 ] {
709 ensure_non_empty(value, field)?;
710 }
711 }
712 Ok(())
713}
714
715pub fn validate_autonomous_drift_report(
716 report: &AutonomousDriftReport,
717) -> Result<(), AutonomyContractError> {
718 if report.schema != CHIO_AUTONOMOUS_DRIFT_REPORT_SCHEMA {
719 return Err(AutonomyContractError::UnsupportedSchema(
720 report.schema.clone(),
721 ));
722 }
723 ensure_non_empty(&report.drift_report_id, "autonomous_drift.drift_report_id")?;
724 ensure_non_empty(&report.subject_key, "autonomous_drift.subject_key")?;
725 ensure_non_empty(
726 &report.pricing_decision_ref,
727 "autonomous_drift.pricing_decision_ref",
728 )?;
729 ensure_non_empty(
730 &report.optimization_ref,
731 "autonomous_drift.optimization_ref",
732 )?;
733 validate_autonomous_rollback_plan(&report.rollback_plan)?;
734 validate_autonomous_comparison_report(&report.comparison_report)?;
735 if report.drift_signals.is_empty() {
736 return Err(AutonomyContractError::MissingField(
737 "autonomous_drift.drift_signals",
738 ));
739 }
740 let mut critical_kinds = HashSet::new();
741 for signal in &report.drift_signals {
742 ensure_non_empty(&signal.metric_name, "autonomous_drift.metric_name")?;
743 ensure_non_empty(&signal.description, "autonomous_drift.description")?;
744 if signal.threshold_value == 0 {
745 return Err(AutonomyContractError::InvalidDrift(
746 "autonomous drift threshold_value must be non-zero".to_string(),
747 ));
748 }
749 if signal.severity == AutonomousDriftSeverity::Critical {
750 critical_kinds.insert(signal.kind);
751 }
752 }
753 if !critical_kinds.is_empty() && !report.fail_safe_engaged {
754 return Err(AutonomyContractError::InvalidDrift(
755 "critical drift signals require fail_safe_engaged".to_string(),
756 ));
757 }
758 for kind in critical_kinds {
759 if !report.rollback_plan.triggers.contains(&kind) {
760 return Err(AutonomyContractError::InvalidDrift(format!(
761 "rollback plan does not cover critical drift trigger {:?}",
762 kind
763 )));
764 }
765 }
766 Ok(())
767}
768
769pub fn validate_autonomous_rollback_plan(
770 plan: &AutonomousRollbackPlanArtifact,
771) -> Result<(), AutonomyContractError> {
772 if plan.schema != CHIO_AUTONOMOUS_ROLLBACK_PLAN_SCHEMA {
773 return Err(AutonomyContractError::UnsupportedSchema(
774 plan.schema.clone(),
775 ));
776 }
777 ensure_non_empty(&plan.plan_id, "autonomous_rollback.plan_id")?;
778 ensure_non_empty(&plan.subject_key, "autonomous_rollback.subject_key")?;
779 if plan.triggers.is_empty() {
780 return Err(AutonomyContractError::MissingField(
781 "autonomous_rollback.triggers",
782 ));
783 }
784 if plan.actions.is_empty() {
785 return Err(AutonomyContractError::MissingField(
786 "autonomous_rollback.actions",
787 ));
788 }
789 ensure_unique_copy_values(&plan.triggers, "autonomous_rollback.triggers")?;
790 ensure_unique_copy_values(&plan.actions, "autonomous_rollback.actions")?;
791 Ok(())
792}
793
794pub fn validate_autonomous_qualification_matrix(
795 matrix: &AutonomousQualificationMatrix,
796) -> Result<(), AutonomyContractError> {
797 if matrix.schema != CHIO_AUTONOMOUS_QUALIFICATION_MATRIX_SCHEMA {
798 return Err(AutonomyContractError::UnsupportedSchema(
799 matrix.schema.clone(),
800 ));
801 }
802 ensure_non_empty(&matrix.profile_id, "autonomous_qualification.profile_id")?;
803 if matrix.cases.is_empty() {
804 return Err(AutonomyContractError::MissingField(
805 "autonomous_qualification.cases",
806 ));
807 }
808 let mut seen_ids = HashSet::new();
809 for case in &matrix.cases {
810 ensure_non_empty(&case.id, "autonomous_qualification.cases.id")?;
811 ensure_non_empty(&case.name, "autonomous_qualification.cases.name")?;
812 ensure_non_empty(&case.notes, "autonomous_qualification.cases.notes")?;
813 if case.requirement_ids.is_empty() {
814 return Err(AutonomyContractError::InvalidQualificationCase(format!(
815 "qualification case `{}` requires at least one requirement id",
816 case.id
817 )));
818 }
819 ensure_unique_strings(
820 &case.requirement_ids,
821 "autonomous_qualification.cases.requirement_ids",
822 )?;
823 if !seen_ids.insert(case.id.as_str()) {
824 return Err(AutonomyContractError::DuplicateValue(case.id.clone()));
825 }
826 }
827 Ok(())
828}
829
830fn validate_model_provenance(
831 model: &AutonomousModelProvenance,
832) -> Result<(), AutonomyContractError> {
833 for (value, field) in [
834 (&model.model_id, "autonomous_model.model_id"),
835 (&model.model_version, "autonomous_model.model_version"),
836 (&model.engine_family, "autonomous_model.engine_family"),
837 (&model.input_hash, "autonomous_model.input_hash"),
838 (
839 &model.explanation_version,
840 "autonomous_model.explanation_version",
841 ),
842 ] {
843 ensure_non_empty(value, field)?;
844 }
845 if model.training_cutoff > model.published_at {
846 return Err(AutonomyContractError::InvalidDecision(
847 "autonomous model training_cutoff must be <= published_at".to_string(),
848 ));
849 }
850 Ok(())
851}
852
853fn validate_rollback_control(
854 control: &AutonomousExecutionRollbackControl,
855) -> Result<(), AutonomyContractError> {
856 ensure_non_empty(
857 &control.rollback_plan_ref,
858 "autonomous_execution.rollback_control.rollback_plan_ref",
859 )?;
860 ensure_non_empty(
861 &control.human_interrupt_contact,
862 "autonomous_execution.rollback_control.human_interrupt_contact",
863 )?;
864 Ok(())
865}
866
867fn validate_positive_money(
868 amount: &MonetaryAmount,
869 field: &'static str,
870) -> Result<(), AutonomyContractError> {
871 if amount.units == 0 {
872 return Err(AutonomyContractError::InvalidDecision(format!(
873 "{field} must be greater than zero"
874 )));
875 }
876 validate_currency_code(&amount.currency, field)
877}
878
879pub(super) fn money_currency_matches_declared(
880 amount: &MonetaryAmount,
881 declared_currency: &str,
882) -> bool {
883 amount.currency == declared_currency
884}
885
886fn validate_currency_code(
887 currency: &str,
888 field: &'static str,
889) -> Result<(), AutonomyContractError> {
890 if currency.len() != 3
891 || !currency
892 .chars()
893 .all(|character| character.is_ascii_uppercase())
894 {
895 return Err(AutonomyContractError::InvalidDecision(format!(
896 "{field} must be a 3-letter uppercase currency code"
897 )));
898 }
899 Ok(())
900}
901
902fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), AutonomyContractError> {
903 if value.trim().is_empty() {
904 return Err(AutonomyContractError::MissingField(field));
905 }
906 Ok(())
907}
908
909pub(super) fn ensure_unique_strings(
910 values: &[String],
911 field: &'static str,
912) -> Result<(), AutonomyContractError> {
913 let mut seen = HashSet::new();
914 for value in values {
915 ensure_non_empty(value, field)?;
916 if value.trim() != value {
917 return Err(AutonomyContractError::InvalidDecision(format!(
918 "{field} must not contain values with surrounding whitespace"
919 )));
920 }
921 if !seen.insert(value.as_str()) {
922 return Err(AutonomyContractError::DuplicateValue(format!(
923 "{field}:{value}"
924 )));
925 }
926 }
927 Ok(())
928}
929
930fn ensure_unique_copy_values<T>(
931 values: &[T],
932 field: &'static str,
933) -> Result<(), AutonomyContractError>
934where
935 T: Copy + Eq + std::hash::Hash + std::fmt::Debug,
936{
937 let mut seen = HashSet::new();
938 for value in values {
939 if !seen.insert(*value) {
940 return Err(AutonomyContractError::DuplicateValue(format!(
941 "{field}:{value:?}"
942 )));
943 }
944 }
945 Ok(())
946}