Skip to main content

chio_credit/
lib.rs

1pub use chio_appraisal as appraisal;
2pub use chio_core_types::{capability, crypto, receipt};
3pub use chio_underwriting as underwriting;
4
5use serde::{Deserialize, Serialize};
6
7use crate::appraisal::AttestationVerifierFamily;
8use crate::capability::{GovernedAutonomyTier, MonetaryAmount, RuntimeAssuranceTier};
9use crate::receipt::{Decision, SettlementStatus, SignedExportEnvelope};
10use crate::underwriting::{
11    UnderwritingCertificationState, UnderwritingComplianceEvidence,
12    UnderwritingDecisionLifecycleState, UnderwritingDecisionOutcome, UnderwritingReviewState,
13    UnderwritingRiskClass,
14};
15
16pub const EXPOSURE_LEDGER_SCHEMA: &str = "chio.credit.exposure-ledger.v1";
17pub const CREDIT_SCORECARD_SCHEMA: &str = "chio.credit.scorecard.v1";
18pub const CREDIT_FACILITY_REPORT_SCHEMA: &str = "chio.credit.facility-report.v1";
19pub const CREDIT_FACILITY_ARTIFACT_SCHEMA: &str = "chio.credit.facility.v1";
20pub const CREDIT_FACILITY_LIST_REPORT_SCHEMA: &str = "chio.credit.facility-list.v1";
21pub const CREDIT_BOND_REPORT_SCHEMA: &str = "chio.credit.bond-report.v1";
22pub const CREDIT_BOND_ARTIFACT_SCHEMA: &str = "chio.credit.bond.v1";
23pub const CREDIT_BOND_LIST_REPORT_SCHEMA: &str = "chio.credit.bond-list.v1";
24pub const CREDIT_LOSS_LIFECYCLE_REPORT_SCHEMA: &str = "chio.credit.loss-lifecycle-report.v1";
25pub const CREDIT_LOSS_LIFECYCLE_ARTIFACT_SCHEMA: &str = "chio.credit.loss-lifecycle.v1";
26pub const CREDIT_LOSS_LIFECYCLE_LIST_REPORT_SCHEMA: &str = "chio.credit.loss-lifecycle-list.v1";
27pub const CREDIT_BACKTEST_REPORT_SCHEMA: &str = "chio.credit.backtest-report.v1";
28pub const CREDIT_PROVIDER_RISK_PACKAGE_SCHEMA: &str = "chio.credit.provider-risk-package.v1";
29pub const CAPITAL_BOOK_REPORT_SCHEMA: &str = "chio.credit.capital-book.v1";
30pub const CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA: &str =
31    "chio.credit.capital-instruction.v1";
32pub const CAPITAL_ALLOCATION_DECISION_ARTIFACT_SCHEMA: &str = "chio.credit.capital-allocation.v1";
33pub const CREDIT_BONDED_EXECUTION_SIMULATION_REPORT_SCHEMA: &str =
34    "chio.credit.bonded-execution-simulation-report.v1";
35pub const MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT: usize = 200;
36pub const MAX_EXPOSURE_LEDGER_DECISION_LIMIT: usize = 200;
37pub const MAX_CREDIT_FACILITY_LIST_LIMIT: usize = 100;
38pub const MAX_CREDIT_BOND_LIST_LIMIT: usize = 100;
39pub const MAX_CREDIT_LOSS_LIFECYCLE_LIST_LIMIT: usize = 100;
40pub const MAX_CREDIT_BACKTEST_WINDOW_LIMIT: usize = 24;
41pub const MAX_CREDIT_PROVIDER_LOSS_LIMIT: usize = 25;
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "camelCase")]
45pub struct ExposureLedgerQuery {
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub capability_id: Option<String>,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub agent_subject: Option<String>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub tool_server: Option<String>,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub tool_name: Option<String>,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub since: Option<u64>,
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub until: Option<u64>,
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub receipt_limit: Option<usize>,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub decision_limit: Option<usize>,
62}
63
64impl Default for ExposureLedgerQuery {
65    fn default() -> Self {
66        Self {
67            capability_id: None,
68            agent_subject: None,
69            tool_server: None,
70            tool_name: None,
71            since: None,
72            until: None,
73            receipt_limit: Some(100),
74            decision_limit: Some(50),
75        }
76    }
77}
78
79impl ExposureLedgerQuery {
80    #[must_use]
81    pub fn receipt_limit_or_default(&self) -> usize {
82        self.receipt_limit
83            .unwrap_or(100)
84            .clamp(1, MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT)
85    }
86
87    #[must_use]
88    pub fn decision_limit_or_default(&self) -> usize {
89        self.decision_limit
90            .unwrap_or(50)
91            .clamp(1, MAX_EXPOSURE_LEDGER_DECISION_LIMIT)
92    }
93
94    #[must_use]
95    pub fn normalized(&self) -> Self {
96        let mut normalized = self.clone();
97        normalized.receipt_limit = Some(self.receipt_limit_or_default());
98        normalized.decision_limit = Some(self.decision_limit_or_default());
99        normalized
100    }
101
102    pub fn validate(&self) -> Result<(), String> {
103        if self.capability_id.is_none()
104            && self.agent_subject.is_none()
105            && self.tool_server.is_none()
106        {
107            return Err(
108                "exposure ledger queries require at least one anchor: --capability, --agent-subject, or --tool-server".to_string(),
109            );
110        }
111        if self.tool_name.is_some() && self.tool_server.is_none() {
112            return Err(
113                "exposure ledger queries that specify --tool-name must also specify --tool-server"
114                    .to_string(),
115            );
116        }
117        if let (Some(since), Some(until)) = (self.since, self.until) {
118            if since > until {
119                return Err(
120                    "exposure ledger queries require --since to be less than or equal to --until"
121                        .to_string(),
122                );
123            }
124        }
125        Ok(())
126    }
127}
128
129#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
130#[serde(rename_all = "snake_case")]
131pub enum ExposureLedgerEvidenceKind {
132    Receipt,
133    SettlementReconciliation,
134    MeteredBillingReconciliation,
135    UnderwritingDecision,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139#[serde(rename_all = "camelCase")]
140pub struct ExposureLedgerEvidenceReference {
141    pub kind: ExposureLedgerEvidenceKind,
142    pub reference_id: String,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub observed_at: Option<u64>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub locator: Option<String>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
150#[serde(rename_all = "camelCase")]
151pub struct ExposureLedgerSupportBoundary {
152    pub governed_receipts_authoritative: bool,
153    pub underwriting_decisions_authoritative: bool,
154    pub settlement_reconciliation_authoritative: bool,
155    pub cross_currency_netting_supported: bool,
156    pub claim_adjudication_supported: bool,
157    pub recovery_lifecycle_supported: bool,
158}
159
160impl Default for ExposureLedgerSupportBoundary {
161    fn default() -> Self {
162        Self {
163            governed_receipts_authoritative: true,
164            underwriting_decisions_authoritative: true,
165            settlement_reconciliation_authoritative: true,
166            cross_currency_netting_supported: false,
167            claim_adjudication_supported: false,
168            recovery_lifecycle_supported: false,
169        }
170    }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
174#[serde(rename_all = "camelCase")]
175pub struct ExposureLedgerCurrencyPosition {
176    pub currency: String,
177    pub governed_max_exposure_units: u64,
178    pub reserved_units: u64,
179    pub settled_units: u64,
180    pub pending_units: u64,
181    pub failed_units: u64,
182    pub provisional_loss_units: u64,
183    pub recovered_units: u64,
184    pub quoted_premium_units: u64,
185    pub active_quoted_premium_units: u64,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189#[serde(rename_all = "camelCase")]
190pub struct ExposureLedgerReceiptEntry {
191    pub receipt_id: String,
192    pub timestamp: u64,
193    pub capability_id: String,
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub subject_key: Option<String>,
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub issuer_key: Option<String>,
198    pub tool_server: String,
199    pub tool_name: String,
200    pub decision: Decision,
201    pub settlement_status: SettlementStatus,
202    pub action_required: bool,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub governed_max_amount: Option<MonetaryAmount>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub financial_amount: Option<MonetaryAmount>,
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub reserve_required_amount: Option<MonetaryAmount>,
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub provisional_loss_amount: Option<MonetaryAmount>,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub recovered_amount: Option<MonetaryAmount>,
213    pub metered_action_required: bool,
214    #[serde(default, skip_serializing_if = "Vec::is_empty")]
215    pub evidence_refs: Vec<ExposureLedgerEvidenceReference>,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219#[serde(rename_all = "camelCase")]
220pub struct ExposureLedgerDecisionEntry {
221    pub decision_id: String,
222    pub issued_at: u64,
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub capability_id: Option<String>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub agent_subject: Option<String>,
227    #[serde(default, skip_serializing_if = "Option::is_none")]
228    pub tool_server: Option<String>,
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    pub tool_name: Option<String>,
231    pub outcome: UnderwritingDecisionOutcome,
232    pub lifecycle_state: UnderwritingDecisionLifecycleState,
233    pub review_state: UnderwritingReviewState,
234    pub risk_class: UnderwritingRiskClass,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub supersedes_decision_id: Option<String>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub quoted_premium_amount: Option<MonetaryAmount>,
239    #[serde(default, skip_serializing_if = "Vec::is_empty")]
240    pub evidence_refs: Vec<ExposureLedgerEvidenceReference>,
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
244#[serde(rename_all = "camelCase")]
245pub struct ExposureLedgerSummary {
246    pub matching_receipts: u64,
247    pub returned_receipts: u64,
248    pub matching_decisions: u64,
249    pub returned_decisions: u64,
250    pub active_decisions: u64,
251    pub superseded_decisions: u64,
252    pub actionable_receipts: u64,
253    pub pending_settlement_receipts: u64,
254    pub failed_settlement_receipts: u64,
255    pub currencies: Vec<String>,
256    pub mixed_currency_book: bool,
257    pub truncated_receipts: bool,
258    pub truncated_decisions: bool,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262#[serde(rename_all = "camelCase")]
263pub struct ExposureLedgerReport {
264    pub schema: String,
265    pub generated_at: u64,
266    pub filters: ExposureLedgerQuery,
267    pub support_boundary: ExposureLedgerSupportBoundary,
268    pub summary: ExposureLedgerSummary,
269    pub positions: Vec<ExposureLedgerCurrencyPosition>,
270    pub receipts: Vec<ExposureLedgerReceiptEntry>,
271    pub decisions: Vec<ExposureLedgerDecisionEntry>,
272}
273
274pub type SignedExposureLedgerReport = SignedExportEnvelope<ExposureLedgerReport>;
275
276#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
277#[serde(rename_all = "snake_case")]
278pub enum CreditScorecardConfidence {
279    Low,
280    Medium,
281    High,
282}
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
285#[serde(rename_all = "snake_case")]
286pub enum CreditScorecardBand {
287    Prime,
288    Standard,
289    Guarded,
290    Probationary,
291    Restricted,
292}
293
294#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
295#[serde(rename_all = "snake_case")]
296pub enum CreditScorecardDimensionKind {
297    ReputationSupport,
298    SettlementDiscipline,
299    LossPressure,
300    ExposureStewardship,
301}
302
303#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "snake_case")]
305pub enum CreditScorecardReasonCode {
306    SparseReceiptHistory,
307    SparseDayHistory,
308    LowConfidence,
309    PendingSettlementBacklog,
310    FailedSettlementBacklog,
311    ProvisionalLossPressure,
312    MixedCurrencyBook,
313    LowReputation,
314    ImportedTrustDependency,
315    MissingDecisionCoverage,
316}
317
318#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
319#[serde(rename_all = "snake_case")]
320pub enum CreditScorecardAnomalySeverity {
321    Info,
322    Warning,
323    Critical,
324}
325
326#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
327#[serde(rename_all = "snake_case")]
328pub enum CreditScorecardEvidenceKind {
329    Receipt,
330    SettlementReconciliation,
331    UnderwritingDecision,
332    ReputationInspection,
333    ComplianceScore,
334    ExposureLedger,
335    CreditBond,
336    CreditLossLifecycle,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
340#[serde(rename_all = "camelCase")]
341pub struct CreditScorecardEvidenceReference {
342    pub kind: CreditScorecardEvidenceKind,
343    pub reference_id: String,
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub observed_at: Option<u64>,
346    #[serde(default, skip_serializing_if = "Option::is_none")]
347    pub locator: Option<String>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
351#[serde(rename_all = "camelCase")]
352pub struct CreditScorecardSupportBoundary {
353    pub subject_scoped_only: bool,
354    pub cross_currency_netting_supported: bool,
355    pub capital_allocation_supported: bool,
356    pub facility_policy_supported: bool,
357}
358
359impl Default for CreditScorecardSupportBoundary {
360    fn default() -> Self {
361        Self {
362            subject_scoped_only: true,
363            cross_currency_netting_supported: false,
364            capital_allocation_supported: false,
365            facility_policy_supported: false,
366        }
367    }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371#[serde(rename_all = "camelCase")]
372pub struct CreditScorecardDimension {
373    pub kind: CreditScorecardDimensionKind,
374    #[serde(default, skip_serializing_if = "Option::is_none")]
375    pub score: Option<f64>,
376    pub weight: f64,
377    pub description: String,
378    #[serde(default, skip_serializing_if = "Vec::is_empty")]
379    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
383#[serde(rename_all = "camelCase")]
384pub struct CreditScorecardProbationStatus {
385    pub probationary: bool,
386    #[serde(default, skip_serializing_if = "Vec::is_empty")]
387    pub reasons: Vec<CreditScorecardReasonCode>,
388    pub receipt_count: u64,
389    pub span_days: u64,
390    pub target_receipt_count: u64,
391    pub target_span_days: u64,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
395#[serde(rename_all = "camelCase")]
396pub struct CreditScorecardAnomaly {
397    pub code: CreditScorecardReasonCode,
398    pub severity: CreditScorecardAnomalySeverity,
399    pub description: String,
400    #[serde(default, skip_serializing_if = "Vec::is_empty")]
401    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
405#[serde(rename_all = "camelCase")]
406pub struct CreditScorecardReputationContext {
407    pub effective_score: f64,
408    pub probationary: bool,
409    #[serde(default, skip_serializing_if = "Option::is_none")]
410    pub resolved_tier: Option<String>,
411    pub imported_signal_count: usize,
412    pub accepted_imported_signal_count: usize,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
416#[serde(rename_all = "camelCase")]
417pub struct CreditScorecardSummary {
418    pub matching_receipts: u64,
419    pub returned_receipts: u64,
420    pub matching_decisions: u64,
421    pub returned_decisions: u64,
422    pub currencies: Vec<String>,
423    pub mixed_currency_book: bool,
424    pub confidence: CreditScorecardConfidence,
425    pub band: CreditScorecardBand,
426    pub overall_score: f64,
427    pub anomaly_count: u64,
428    pub probationary: bool,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
432#[serde(rename_all = "camelCase")]
433pub struct CreditScorecardReport {
434    pub schema: String,
435    pub generated_at: u64,
436    pub filters: ExposureLedgerQuery,
437    pub support_boundary: CreditScorecardSupportBoundary,
438    pub summary: CreditScorecardSummary,
439    pub reputation: CreditScorecardReputationContext,
440    pub positions: Vec<ExposureLedgerCurrencyPosition>,
441    pub probation: CreditScorecardProbationStatus,
442    pub dimensions: Vec<CreditScorecardDimension>,
443    #[serde(default, skip_serializing_if = "Vec::is_empty")]
444    pub anomalies: Vec<CreditScorecardAnomaly>,
445}
446
447pub type SignedCreditScorecardReport = SignedExportEnvelope<CreditScorecardReport>;
448
449#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
450#[serde(rename_all = "snake_case")]
451pub enum CreditFacilityDisposition {
452    Grant,
453    ManualReview,
454    Deny,
455}
456
457#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
458#[serde(rename_all = "snake_case")]
459pub enum CreditFacilityLifecycleState {
460    Active,
461    Superseded,
462    Denied,
463    Expired,
464}
465
466#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
467#[serde(rename_all = "snake_case")]
468pub enum CreditFacilityCapitalSource {
469    OperatorInternal,
470    ManualProviderReview,
471}
472
473#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
474#[serde(rename_all = "snake_case")]
475pub enum CreditFacilityReasonCode {
476    ScoreRestricted,
477    ProbationaryScore,
478    LowConfidence,
479    MixedCurrencyBook,
480    MixedRuntimeAssuranceProvenance,
481    MissingRuntimeAssurance,
482    CertificationNotActive,
483    FailedSettlementBacklog,
484    PendingSettlementBacklog,
485    FacilityGranted,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
489#[serde(rename_all = "camelCase")]
490pub struct CreditFacilityTerms {
491    pub credit_limit: MonetaryAmount,
492    pub utilization_ceiling_bps: u16,
493    pub reserve_ratio_bps: u16,
494    pub concentration_cap_bps: u16,
495    pub ttl_seconds: u64,
496    pub capital_source: CreditFacilityCapitalSource,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
500#[serde(rename_all = "camelCase")]
501pub struct CreditFacilityPrerequisites {
502    pub minimum_runtime_assurance_tier: RuntimeAssuranceTier,
503    pub runtime_assurance_met: bool,
504    pub certification_required: bool,
505    pub certification_met: bool,
506    pub manual_review_required: bool,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
510#[serde(rename_all = "camelCase")]
511pub struct CreditFacilityFinding {
512    pub code: CreditFacilityReasonCode,
513    pub description: String,
514    #[serde(default, skip_serializing_if = "Vec::is_empty")]
515    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
519#[serde(rename_all = "camelCase")]
520pub struct CreditFacilitySupportBoundary {
521    pub provider_neutral_policy: bool,
522    pub cross_currency_allocation_supported: bool,
523    pub bond_execution_supported: bool,
524}
525
526impl Default for CreditFacilitySupportBoundary {
527    fn default() -> Self {
528        Self {
529            provider_neutral_policy: true,
530            cross_currency_allocation_supported: false,
531            bond_execution_supported: false,
532        }
533    }
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
537#[serde(rename_all = "camelCase")]
538pub struct CreditFacilityReport {
539    pub schema: String,
540    pub generated_at: u64,
541    pub filters: ExposureLedgerQuery,
542    pub scorecard: CreditScorecardSummary,
543    pub disposition: CreditFacilityDisposition,
544    pub prerequisites: CreditFacilityPrerequisites,
545    pub support_boundary: CreditFacilitySupportBoundary,
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub terms: Option<CreditFacilityTerms>,
548    #[serde(default, skip_serializing_if = "Vec::is_empty")]
549    pub findings: Vec<CreditFacilityFinding>,
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
553#[serde(rename_all = "camelCase")]
554pub struct CreditFacilityArtifact {
555    pub schema: String,
556    pub facility_id: String,
557    pub issued_at: u64,
558    pub expires_at: u64,
559    pub lifecycle_state: CreditFacilityLifecycleState,
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    pub supersedes_facility_id: Option<String>,
562    pub report: CreditFacilityReport,
563}
564
565pub type SignedCreditFacility = SignedExportEnvelope<CreditFacilityArtifact>;
566
567#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
568#[serde(rename_all = "camelCase")]
569pub struct CreditFacilityListQuery {
570    #[serde(default, skip_serializing_if = "Option::is_none")]
571    pub facility_id: Option<String>,
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub capability_id: Option<String>,
574    #[serde(default, skip_serializing_if = "Option::is_none")]
575    pub agent_subject: Option<String>,
576    #[serde(default, skip_serializing_if = "Option::is_none")]
577    pub tool_server: Option<String>,
578    #[serde(default, skip_serializing_if = "Option::is_none")]
579    pub tool_name: Option<String>,
580    #[serde(default, skip_serializing_if = "Option::is_none")]
581    pub disposition: Option<CreditFacilityDisposition>,
582    #[serde(default, skip_serializing_if = "Option::is_none")]
583    pub lifecycle_state: Option<CreditFacilityLifecycleState>,
584    #[serde(default, skip_serializing_if = "Option::is_none")]
585    pub limit: Option<usize>,
586}
587
588impl Default for CreditFacilityListQuery {
589    fn default() -> Self {
590        Self {
591            facility_id: None,
592            capability_id: None,
593            agent_subject: None,
594            tool_server: None,
595            tool_name: None,
596            disposition: None,
597            lifecycle_state: None,
598            limit: Some(50),
599        }
600    }
601}
602
603impl CreditFacilityListQuery {
604    #[must_use]
605    pub fn limit_or_default(&self) -> usize {
606        self.limit
607            .unwrap_or(50)
608            .clamp(1, MAX_CREDIT_FACILITY_LIST_LIMIT)
609    }
610
611    #[must_use]
612    pub fn normalized(&self) -> Self {
613        let mut normalized = self.clone();
614        normalized.limit = Some(self.limit_or_default());
615        normalized
616    }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
620#[serde(rename_all = "camelCase")]
621pub struct CreditFacilityRow {
622    pub facility: SignedCreditFacility,
623    pub lifecycle_state: CreditFacilityLifecycleState,
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub superseded_by_facility_id: Option<String>,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
629#[serde(rename_all = "camelCase")]
630pub struct CreditFacilityListSummary {
631    pub matching_facilities: u64,
632    pub returned_facilities: u64,
633    pub active_facilities: u64,
634    pub superseded_facilities: u64,
635    pub denied_facilities: u64,
636    pub expired_facilities: u64,
637    pub granted_facilities: u64,
638    pub manual_review_facilities: u64,
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
642#[serde(rename_all = "camelCase")]
643pub struct CreditFacilityListReport {
644    pub schema: String,
645    pub generated_at: u64,
646    pub query: CreditFacilityListQuery,
647    pub summary: CreditFacilityListSummary,
648    pub facilities: Vec<CreditFacilityRow>,
649}
650
651#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
652#[serde(rename_all = "snake_case")]
653pub enum CreditBondDisposition {
654    Lock,
655    Hold,
656    Release,
657    Impair,
658}
659
660#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
661#[serde(rename_all = "snake_case")]
662pub enum CreditBondLifecycleState {
663    Active,
664    Superseded,
665    Released,
666    Impaired,
667    Expired,
668}
669
670#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
671#[serde(rename_all = "snake_case")]
672pub enum CreditBondReasonCode {
673    ActiveFacilityMissing,
674    MixedCurrencyBook,
675    PendingSettlementBacklog,
676    FailedSettlementBacklog,
677    ProvisionalLossOutstanding,
678    ReserveLocked,
679    ReserveHeld,
680    ReserveReleased,
681    UnderCollateralized,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685#[serde(rename_all = "camelCase")]
686pub struct CreditBondTerms {
687    pub facility_id: String,
688    pub credit_limit: MonetaryAmount,
689    pub collateral_amount: MonetaryAmount,
690    pub reserve_requirement_amount: MonetaryAmount,
691    pub outstanding_exposure_amount: MonetaryAmount,
692    pub reserve_ratio_bps: u16,
693    pub coverage_ratio_bps: u16,
694    pub capital_source: CreditFacilityCapitalSource,
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
698#[serde(rename_all = "camelCase")]
699pub struct CreditBondPrerequisites {
700    pub active_facility_required: bool,
701    pub active_facility_met: bool,
702    pub runtime_assurance_met: bool,
703    pub certification_required: bool,
704    pub certification_met: bool,
705    pub currency_coherent: bool,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
709#[serde(rename_all = "camelCase")]
710pub struct CreditBondFinding {
711    pub code: CreditBondReasonCode,
712    pub description: String,
713    #[serde(default, skip_serializing_if = "Vec::is_empty")]
714    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
718#[serde(rename_all = "camelCase")]
719pub struct CreditBondSupportBoundary {
720    pub reserve_accounting_authoritative: bool,
721    pub external_escrow_execution_supported: bool,
722    pub autonomy_gating_supported: bool,
723}
724
725impl Default for CreditBondSupportBoundary {
726    fn default() -> Self {
727        Self {
728            reserve_accounting_authoritative: true,
729            external_escrow_execution_supported: false,
730            autonomy_gating_supported: false,
731        }
732    }
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
736#[serde(rename_all = "camelCase")]
737pub struct CreditBondReport {
738    pub schema: String,
739    pub generated_at: u64,
740    pub filters: ExposureLedgerQuery,
741    pub exposure: ExposureLedgerSummary,
742    pub scorecard: CreditScorecardSummary,
743    pub disposition: CreditBondDisposition,
744    pub prerequisites: CreditBondPrerequisites,
745    pub support_boundary: CreditBondSupportBoundary,
746    #[serde(default, skip_serializing_if = "Option::is_none")]
747    pub latest_facility_id: Option<String>,
748    #[serde(default, skip_serializing_if = "Option::is_none")]
749    pub terms: Option<CreditBondTerms>,
750    #[serde(default, skip_serializing_if = "Vec::is_empty")]
751    pub findings: Vec<CreditBondFinding>,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
755#[serde(rename_all = "camelCase")]
756pub struct CreditBondArtifact {
757    pub schema: String,
758    pub bond_id: String,
759    pub issued_at: u64,
760    pub expires_at: u64,
761    pub lifecycle_state: CreditBondLifecycleState,
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub supersedes_bond_id: Option<String>,
764    pub report: CreditBondReport,
765}
766
767pub type SignedCreditBond = SignedExportEnvelope<CreditBondArtifact>;
768
769#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
770#[serde(rename_all = "camelCase")]
771pub struct CreditBondListQuery {
772    #[serde(default, skip_serializing_if = "Option::is_none")]
773    pub bond_id: Option<String>,
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub facility_id: Option<String>,
776    #[serde(default, skip_serializing_if = "Option::is_none")]
777    pub capability_id: Option<String>,
778    #[serde(default, skip_serializing_if = "Option::is_none")]
779    pub agent_subject: Option<String>,
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub tool_server: Option<String>,
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    pub tool_name: Option<String>,
784    #[serde(default, skip_serializing_if = "Option::is_none")]
785    pub disposition: Option<CreditBondDisposition>,
786    #[serde(default, skip_serializing_if = "Option::is_none")]
787    pub lifecycle_state: Option<CreditBondLifecycleState>,
788    #[serde(default, skip_serializing_if = "Option::is_none")]
789    pub limit: Option<usize>,
790}
791
792impl Default for CreditBondListQuery {
793    fn default() -> Self {
794        Self {
795            bond_id: None,
796            facility_id: None,
797            capability_id: None,
798            agent_subject: None,
799            tool_server: None,
800            tool_name: None,
801            disposition: None,
802            lifecycle_state: None,
803            limit: Some(50),
804        }
805    }
806}
807
808impl CreditBondListQuery {
809    #[must_use]
810    pub fn limit_or_default(&self) -> usize {
811        self.limit
812            .unwrap_or(50)
813            .clamp(1, MAX_CREDIT_BOND_LIST_LIMIT)
814    }
815
816    #[must_use]
817    pub fn normalized(&self) -> Self {
818        let mut normalized = self.clone();
819        normalized.limit = Some(self.limit_or_default());
820        normalized
821    }
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
825#[serde(rename_all = "camelCase")]
826pub struct CreditBondRow {
827    pub bond: SignedCreditBond,
828    pub lifecycle_state: CreditBondLifecycleState,
829    #[serde(default, skip_serializing_if = "Option::is_none")]
830    pub superseded_by_bond_id: Option<String>,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
834#[serde(rename_all = "camelCase")]
835pub struct CreditBondListSummary {
836    pub matching_bonds: u64,
837    pub returned_bonds: u64,
838    pub active_bonds: u64,
839    pub superseded_bonds: u64,
840    pub released_bonds: u64,
841    pub impaired_bonds: u64,
842    pub expired_bonds: u64,
843    pub locked_bonds: u64,
844    pub held_bonds: u64,
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
848#[serde(rename_all = "camelCase")]
849pub struct CreditBondListReport {
850    pub schema: String,
851    pub generated_at: u64,
852    pub query: CreditBondListQuery,
853    pub summary: CreditBondListSummary,
854    pub bonds: Vec<CreditBondRow>,
855}
856
857#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
858#[serde(rename_all = "snake_case")]
859pub enum CreditLossLifecycleEventKind {
860    Delinquency,
861    Recovery,
862    ReserveRelease,
863    ReserveSlash,
864    WriteOff,
865}
866
867#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
868#[serde(rename_all = "snake_case")]
869pub enum CreditLossLifecycleReasonCode {
870    ActiveBondRequired,
871    BondNotActive,
872    DelinquencyEvidenceMissing,
873    OutstandingDelinquencyRequired,
874    AmountRequired,
875    AmountCurrencyMismatch,
876    AmountExceedsOutstandingDelinquency,
877    OutstandingExposureBlocksReserveRelease,
878    ReserveAlreadyReleased,
879    DelinquencyRecorded,
880    RecoveryRecorded,
881    ReserveReleased,
882    ReserveSlashed,
883    WriteOffRecorded,
884}
885
886#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
887#[serde(rename_all = "snake_case")]
888pub enum CreditReserveControlExecutionState {
889    PendingExecution,
890    Executed,
891}
892
893#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
894#[serde(rename_all = "snake_case")]
895pub enum CreditReserveControlAppealState {
896    Unsupported,
897    Open,
898    Closed,
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
902#[serde(rename_all = "camelCase")]
903pub struct CreditLossLifecycleQuery {
904    pub bond_id: String,
905    pub event_kind: CreditLossLifecycleEventKind,
906    #[serde(default, skip_serializing_if = "Option::is_none")]
907    pub amount: Option<MonetaryAmount>,
908}
909
910impl CreditLossLifecycleQuery {
911    pub fn validate(&self) -> Result<(), String> {
912        if self.bond_id.trim().is_empty() {
913            return Err("credit loss lifecycle requests require --bond-id".to_string());
914        }
915        if self.amount.as_ref().is_some_and(|amount| amount.units == 0) {
916            return Err("credit loss lifecycle amounts must be greater than zero".to_string());
917        }
918        Ok(())
919    }
920}
921
922#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
923#[serde(rename_all = "camelCase")]
924pub struct CreditLossLifecycleSummary {
925    pub bond_id: String,
926    #[serde(default, skip_serializing_if = "Option::is_none")]
927    pub facility_id: Option<String>,
928    #[serde(default, skip_serializing_if = "Option::is_none")]
929    pub capability_id: Option<String>,
930    #[serde(default, skip_serializing_if = "Option::is_none")]
931    pub agent_subject: Option<String>,
932    #[serde(default, skip_serializing_if = "Option::is_none")]
933    pub tool_server: Option<String>,
934    #[serde(default, skip_serializing_if = "Option::is_none")]
935    pub tool_name: Option<String>,
936    pub current_bond_lifecycle_state: CreditBondLifecycleState,
937    pub projected_bond_lifecycle_state: CreditBondLifecycleState,
938    #[serde(default, skip_serializing_if = "Option::is_none")]
939    pub current_delinquent_amount: Option<MonetaryAmount>,
940    #[serde(default, skip_serializing_if = "Option::is_none")]
941    pub current_recovered_amount: Option<MonetaryAmount>,
942    #[serde(default, skip_serializing_if = "Option::is_none")]
943    pub current_written_off_amount: Option<MonetaryAmount>,
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub current_released_reserve_amount: Option<MonetaryAmount>,
946    #[serde(default, skip_serializing_if = "Option::is_none")]
947    pub current_slashed_reserve_amount: Option<MonetaryAmount>,
948    #[serde(default, skip_serializing_if = "Option::is_none")]
949    pub outstanding_delinquent_amount: Option<MonetaryAmount>,
950    #[serde(default, skip_serializing_if = "Option::is_none")]
951    pub releaseable_reserve_amount: Option<MonetaryAmount>,
952    #[serde(default, skip_serializing_if = "Option::is_none")]
953    pub reserve_control_source_id: Option<String>,
954    #[serde(default, skip_serializing_if = "Option::is_none")]
955    pub execution_state: Option<CreditReserveControlExecutionState>,
956    #[serde(default, skip_serializing_if = "Option::is_none")]
957    pub appeal_state: Option<CreditReserveControlAppealState>,
958    #[serde(default, skip_serializing_if = "Option::is_none")]
959    pub appeal_window_ends_at: Option<u64>,
960    #[serde(default, skip_serializing_if = "Option::is_none")]
961    pub event_amount: Option<MonetaryAmount>,
962}
963
964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
965#[serde(rename_all = "camelCase")]
966pub struct CreditLossLifecycleFinding {
967    pub code: CreditLossLifecycleReasonCode,
968    pub description: String,
969    #[serde(default, skip_serializing_if = "Vec::is_empty")]
970    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
974#[serde(rename_all = "camelCase")]
975pub struct CreditLossLifecycleSupportBoundary {
976    pub immutable_lifecycle_authoritative: bool,
977    pub bond_lifecycle_projection_authoritative: bool,
978    pub external_claim_adjudication_supported: bool,
979    pub automatic_capital_execution_supported: bool,
980    pub reserve_control_execution_supported: bool,
981    pub appeal_window_supported: bool,
982}
983
984impl Default for CreditLossLifecycleSupportBoundary {
985    fn default() -> Self {
986        Self {
987            immutable_lifecycle_authoritative: true,
988            bond_lifecycle_projection_authoritative: true,
989            external_claim_adjudication_supported: false,
990            automatic_capital_execution_supported: false,
991            reserve_control_execution_supported: true,
992            appeal_window_supported: true,
993        }
994    }
995}
996
997#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
998#[serde(rename_all = "camelCase")]
999pub struct CreditLossLifecycleReport {
1000    pub schema: String,
1001    pub generated_at: u64,
1002    pub query: CreditLossLifecycleQuery,
1003    pub summary: CreditLossLifecycleSummary,
1004    pub support_boundary: CreditLossLifecycleSupportBoundary,
1005    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1006    pub findings: Vec<CreditLossLifecycleFinding>,
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1010#[serde(rename_all = "camelCase")]
1011pub struct CreditLossLifecycleArtifact {
1012    pub schema: String,
1013    pub event_id: String,
1014    pub issued_at: u64,
1015    pub bond_id: String,
1016    pub event_kind: CreditLossLifecycleEventKind,
1017    pub projected_bond_lifecycle_state: CreditBondLifecycleState,
1018    #[serde(default, skip_serializing_if = "Option::is_none")]
1019    pub reserve_control_source_id: Option<String>,
1020    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1021    pub authority_chain: Vec<CapitalExecutionAuthorityStep>,
1022    #[serde(default, skip_serializing_if = "Option::is_none")]
1023    pub execution_window: Option<CapitalExecutionWindow>,
1024    #[serde(default, skip_serializing_if = "Option::is_none")]
1025    pub rail: Option<CapitalExecutionRail>,
1026    #[serde(default, skip_serializing_if = "Option::is_none")]
1027    pub observed_execution: Option<CapitalExecutionObservation>,
1028    #[serde(default, skip_serializing_if = "Option::is_none")]
1029    pub reconciled_state: Option<CapitalExecutionReconciledState>,
1030    #[serde(default, skip_serializing_if = "Option::is_none")]
1031    pub execution_state: Option<CreditReserveControlExecutionState>,
1032    #[serde(default, skip_serializing_if = "Option::is_none")]
1033    pub appeal_state: Option<CreditReserveControlAppealState>,
1034    #[serde(default, skip_serializing_if = "Option::is_none")]
1035    pub appeal_window_ends_at: Option<u64>,
1036    #[serde(default, skip_serializing_if = "Option::is_none")]
1037    pub description: Option<String>,
1038    pub report: CreditLossLifecycleReport,
1039}
1040
1041pub type SignedCreditLossLifecycle = SignedExportEnvelope<CreditLossLifecycleArtifact>;
1042
1043#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1044#[serde(rename_all = "camelCase")]
1045pub struct CreditLossLifecycleListQuery {
1046    #[serde(default, skip_serializing_if = "Option::is_none")]
1047    pub event_id: Option<String>,
1048    #[serde(default, skip_serializing_if = "Option::is_none")]
1049    pub bond_id: Option<String>,
1050    #[serde(default, skip_serializing_if = "Option::is_none")]
1051    pub facility_id: Option<String>,
1052    #[serde(default, skip_serializing_if = "Option::is_none")]
1053    pub capability_id: Option<String>,
1054    #[serde(default, skip_serializing_if = "Option::is_none")]
1055    pub agent_subject: Option<String>,
1056    #[serde(default, skip_serializing_if = "Option::is_none")]
1057    pub tool_server: Option<String>,
1058    #[serde(default, skip_serializing_if = "Option::is_none")]
1059    pub tool_name: Option<String>,
1060    #[serde(default, skip_serializing_if = "Option::is_none")]
1061    pub event_kind: Option<CreditLossLifecycleEventKind>,
1062    #[serde(default, skip_serializing_if = "Option::is_none")]
1063    pub limit: Option<usize>,
1064}
1065
1066impl Default for CreditLossLifecycleListQuery {
1067    fn default() -> Self {
1068        Self {
1069            event_id: None,
1070            bond_id: None,
1071            facility_id: None,
1072            capability_id: None,
1073            agent_subject: None,
1074            tool_server: None,
1075            tool_name: None,
1076            event_kind: None,
1077            limit: Some(50),
1078        }
1079    }
1080}
1081
1082impl CreditLossLifecycleListQuery {
1083    #[must_use]
1084    pub fn limit_or_default(&self) -> usize {
1085        self.limit
1086            .unwrap_or(50)
1087            .clamp(1, MAX_CREDIT_LOSS_LIFECYCLE_LIST_LIMIT)
1088    }
1089
1090    #[must_use]
1091    pub fn normalized(&self) -> Self {
1092        let mut normalized = self.clone();
1093        normalized.limit = Some(self.limit_or_default());
1094        normalized
1095    }
1096}
1097
1098#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1099#[serde(rename_all = "camelCase")]
1100pub struct CreditLossLifecycleRow {
1101    pub event: SignedCreditLossLifecycle,
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1105#[serde(rename_all = "camelCase")]
1106pub struct CreditLossLifecycleListSummary {
1107    pub matching_events: u64,
1108    pub returned_events: u64,
1109    pub delinquency_events: u64,
1110    pub recovery_events: u64,
1111    pub reserve_release_events: u64,
1112    pub reserve_slash_events: u64,
1113    pub write_off_events: u64,
1114}
1115
1116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1117#[serde(rename_all = "camelCase")]
1118pub struct CreditLossLifecycleListReport {
1119    pub schema: String,
1120    pub generated_at: u64,
1121    pub query: CreditLossLifecycleListQuery,
1122    pub summary: CreditLossLifecycleListSummary,
1123    pub events: Vec<CreditLossLifecycleRow>,
1124}
1125
1126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1127#[serde(rename_all = "camelCase")]
1128pub struct CreditBacktestQuery {
1129    #[serde(default, skip_serializing_if = "Option::is_none")]
1130    pub capability_id: Option<String>,
1131    #[serde(default, skip_serializing_if = "Option::is_none")]
1132    pub agent_subject: Option<String>,
1133    #[serde(default, skip_serializing_if = "Option::is_none")]
1134    pub tool_server: Option<String>,
1135    #[serde(default, skip_serializing_if = "Option::is_none")]
1136    pub tool_name: Option<String>,
1137    #[serde(default, skip_serializing_if = "Option::is_none")]
1138    pub since: Option<u64>,
1139    #[serde(default, skip_serializing_if = "Option::is_none")]
1140    pub until: Option<u64>,
1141    #[serde(default, skip_serializing_if = "Option::is_none")]
1142    pub receipt_limit: Option<usize>,
1143    #[serde(default, skip_serializing_if = "Option::is_none")]
1144    pub decision_limit: Option<usize>,
1145    #[serde(default, skip_serializing_if = "Option::is_none")]
1146    pub window_seconds: Option<u64>,
1147    #[serde(default, skip_serializing_if = "Option::is_none")]
1148    pub window_count: Option<usize>,
1149    #[serde(default, skip_serializing_if = "Option::is_none")]
1150    pub stale_after_seconds: Option<u64>,
1151}
1152
1153impl Default for CreditBacktestQuery {
1154    fn default() -> Self {
1155        Self {
1156            capability_id: None,
1157            agent_subject: None,
1158            tool_server: None,
1159            tool_name: None,
1160            since: None,
1161            until: None,
1162            receipt_limit: Some(100),
1163            decision_limit: Some(50),
1164            window_seconds: Some(7 * 86_400),
1165            window_count: Some(4),
1166            stale_after_seconds: Some(30 * 86_400),
1167        }
1168    }
1169}
1170
1171impl CreditBacktestQuery {
1172    #[must_use]
1173    pub fn receipt_limit_or_default(&self) -> usize {
1174        self.receipt_limit
1175            .unwrap_or(100)
1176            .clamp(1, MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT)
1177    }
1178
1179    #[must_use]
1180    pub fn decision_limit_or_default(&self) -> usize {
1181        self.decision_limit
1182            .unwrap_or(50)
1183            .clamp(1, MAX_EXPOSURE_LEDGER_DECISION_LIMIT)
1184    }
1185
1186    #[must_use]
1187    pub fn window_seconds_or_default(&self) -> u64 {
1188        self.window_seconds.unwrap_or(7 * 86_400).max(1)
1189    }
1190
1191    #[must_use]
1192    pub fn window_count_or_default(&self) -> usize {
1193        self.window_count
1194            .unwrap_or(4)
1195            .clamp(1, MAX_CREDIT_BACKTEST_WINDOW_LIMIT)
1196    }
1197
1198    #[must_use]
1199    pub fn stale_after_seconds_or_default(&self) -> u64 {
1200        self.stale_after_seconds.unwrap_or(30 * 86_400).max(1)
1201    }
1202
1203    #[must_use]
1204    pub fn normalized(&self) -> Self {
1205        let mut normalized = self.clone();
1206        normalized.receipt_limit = Some(self.receipt_limit_or_default());
1207        normalized.decision_limit = Some(self.decision_limit_or_default());
1208        normalized.window_seconds = Some(self.window_seconds_or_default());
1209        normalized.window_count = Some(self.window_count_or_default());
1210        normalized.stale_after_seconds = Some(self.stale_after_seconds_or_default());
1211        normalized
1212    }
1213
1214    #[must_use]
1215    pub fn exposure_query(&self) -> ExposureLedgerQuery {
1216        ExposureLedgerQuery {
1217            capability_id: self.capability_id.clone(),
1218            agent_subject: self.agent_subject.clone(),
1219            tool_server: self.tool_server.clone(),
1220            tool_name: self.tool_name.clone(),
1221            since: self.since,
1222            until: self.until,
1223            receipt_limit: self.receipt_limit,
1224            decision_limit: self.decision_limit,
1225        }
1226    }
1227
1228    pub fn validate(&self) -> Result<(), String> {
1229        self.exposure_query().validate()?;
1230        if self.agent_subject.is_none() {
1231            return Err(
1232                "credit backtests require --agent-subject because scorecards and facilities are subject-scoped"
1233                    .to_string(),
1234            );
1235        }
1236        Ok(())
1237    }
1238}
1239
1240#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1241#[serde(rename_all = "snake_case")]
1242pub enum CreditBacktestReasonCode {
1243    ScoreBandShift,
1244    FacilityDispositionShift,
1245    MixedCurrencyBook,
1246    StaleEvidence,
1247    FacilityOverUtilization,
1248    PendingSettlementBacklog,
1249    FailedSettlementBacklog,
1250    MissingRuntimeAssurance,
1251    CertificationNotActive,
1252}
1253
1254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1255#[serde(rename_all = "camelCase")]
1256pub struct CreditBacktestWindow {
1257    pub index: u64,
1258    pub window_started_at: u64,
1259    pub window_ended_at: u64,
1260    #[serde(default, skip_serializing_if = "Option::is_none")]
1261    pub newest_receipt_at: Option<u64>,
1262    #[serde(default, skip_serializing_if = "Option::is_none")]
1263    pub expected_band: Option<CreditScorecardBand>,
1264    #[serde(default, skip_serializing_if = "Option::is_none")]
1265    pub expected_disposition: Option<CreditFacilityDisposition>,
1266    pub simulated_scorecard: CreditScorecardSummary,
1267    pub simulated_disposition: CreditFacilityDisposition,
1268    #[serde(default, skip_serializing_if = "Option::is_none")]
1269    pub simulated_terms: Option<CreditFacilityTerms>,
1270    pub stale_evidence: bool,
1271    #[serde(default, skip_serializing_if = "Option::is_none")]
1272    pub utilization_bps: Option<u32>,
1273    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1274    pub reason_codes: Vec<CreditBacktestReasonCode>,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1278#[serde(rename_all = "camelCase")]
1279pub struct CreditBacktestSummary {
1280    pub windows_evaluated: u64,
1281    pub drift_windows: u64,
1282    pub score_band_changes: u64,
1283    pub facility_disposition_changes: u64,
1284    pub manual_review_windows: u64,
1285    pub denied_windows: u64,
1286    pub stale_evidence_windows: u64,
1287    pub mixed_currency_windows: u64,
1288    pub over_utilized_windows: u64,
1289}
1290
1291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1292#[serde(rename_all = "camelCase")]
1293pub struct CreditBacktestReport {
1294    pub schema: String,
1295    pub generated_at: u64,
1296    pub query: CreditBacktestQuery,
1297    pub summary: CreditBacktestSummary,
1298    pub windows: Vec<CreditBacktestWindow>,
1299}
1300
1301#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1302#[serde(rename_all = "camelCase")]
1303pub struct CreditProviderRiskPackageQuery {
1304    #[serde(default, skip_serializing_if = "Option::is_none")]
1305    pub capability_id: Option<String>,
1306    #[serde(default, skip_serializing_if = "Option::is_none")]
1307    pub agent_subject: Option<String>,
1308    #[serde(default, skip_serializing_if = "Option::is_none")]
1309    pub tool_server: Option<String>,
1310    #[serde(default, skip_serializing_if = "Option::is_none")]
1311    pub tool_name: Option<String>,
1312    #[serde(default, skip_serializing_if = "Option::is_none")]
1313    pub since: Option<u64>,
1314    #[serde(default, skip_serializing_if = "Option::is_none")]
1315    pub until: Option<u64>,
1316    #[serde(default, skip_serializing_if = "Option::is_none")]
1317    pub receipt_limit: Option<usize>,
1318    #[serde(default, skip_serializing_if = "Option::is_none")]
1319    pub decision_limit: Option<usize>,
1320    #[serde(default, skip_serializing_if = "Option::is_none")]
1321    pub recent_loss_limit: Option<usize>,
1322}
1323
1324impl Default for CreditProviderRiskPackageQuery {
1325    fn default() -> Self {
1326        Self {
1327            capability_id: None,
1328            agent_subject: None,
1329            tool_server: None,
1330            tool_name: None,
1331            since: None,
1332            until: None,
1333            receipt_limit: Some(100),
1334            decision_limit: Some(50),
1335            recent_loss_limit: Some(10),
1336        }
1337    }
1338}
1339
1340impl CreditProviderRiskPackageQuery {
1341    #[must_use]
1342    pub fn recent_loss_limit_or_default(&self) -> usize {
1343        self.recent_loss_limit
1344            .unwrap_or(10)
1345            .clamp(1, MAX_CREDIT_PROVIDER_LOSS_LIMIT)
1346    }
1347
1348    #[must_use]
1349    pub fn normalized(&self) -> Self {
1350        let mut normalized = self.clone();
1351        normalized.receipt_limit = Some(
1352            self.receipt_limit
1353                .unwrap_or(100)
1354                .clamp(1, MAX_EXPOSURE_LEDGER_RECEIPT_LIMIT),
1355        );
1356        normalized.decision_limit = Some(
1357            self.decision_limit
1358                .unwrap_or(50)
1359                .clamp(1, MAX_EXPOSURE_LEDGER_DECISION_LIMIT),
1360        );
1361        normalized.recent_loss_limit = Some(self.recent_loss_limit_or_default());
1362        normalized
1363    }
1364
1365    #[must_use]
1366    pub fn exposure_query(&self) -> ExposureLedgerQuery {
1367        ExposureLedgerQuery {
1368            capability_id: self.capability_id.clone(),
1369            agent_subject: self.agent_subject.clone(),
1370            tool_server: self.tool_server.clone(),
1371            tool_name: self.tool_name.clone(),
1372            since: self.since,
1373            until: self.until,
1374            receipt_limit: self.receipt_limit,
1375            decision_limit: self.decision_limit,
1376        }
1377    }
1378
1379    pub fn validate(&self) -> Result<(), String> {
1380        self.exposure_query().validate()?;
1381        if self.agent_subject.is_none() {
1382            return Err(
1383                "provider risk packages require --agent-subject because the package is subject-scoped"
1384                    .to_string(),
1385            );
1386        }
1387        Ok(())
1388    }
1389}
1390
1391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1392#[serde(rename_all = "camelCase")]
1393pub struct CreditRecentLossEntry {
1394    pub receipt_id: String,
1395    pub observed_at: u64,
1396    pub settlement_status: SettlementStatus,
1397    #[serde(default, skip_serializing_if = "Option::is_none")]
1398    pub financial_amount: Option<MonetaryAmount>,
1399    #[serde(default, skip_serializing_if = "Option::is_none")]
1400    pub provisional_loss_amount: Option<MonetaryAmount>,
1401    #[serde(default, skip_serializing_if = "Option::is_none")]
1402    pub recovered_amount: Option<MonetaryAmount>,
1403    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1404    pub evidence_refs: Vec<ExposureLedgerEvidenceReference>,
1405}
1406
1407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1408#[serde(rename_all = "camelCase")]
1409pub struct CreditRecentLossSummary {
1410    pub matching_loss_events: u64,
1411    pub returned_loss_events: u64,
1412    pub failed_settlement_events: u64,
1413    pub provisional_loss_events: u64,
1414    pub recovered_events: u64,
1415}
1416
1417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1418#[serde(rename_all = "camelCase")]
1419pub struct CreditRecentLossHistory {
1420    pub summary: CreditRecentLossSummary,
1421    pub entries: Vec<CreditRecentLossEntry>,
1422}
1423
1424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1425#[serde(rename_all = "camelCase")]
1426pub struct CreditRuntimeAssuranceState {
1427    pub governed_receipts: u64,
1428    pub runtime_assurance_receipts: u64,
1429    #[serde(default, skip_serializing_if = "Option::is_none")]
1430    pub highest_tier: Option<RuntimeAssuranceTier>,
1431    #[serde(default, skip_serializing_if = "Option::is_none")]
1432    pub latest_schema: Option<String>,
1433    #[serde(default, skip_serializing_if = "Option::is_none")]
1434    pub latest_verifier_family: Option<AttestationVerifierFamily>,
1435    #[serde(default, skip_serializing_if = "Option::is_none")]
1436    pub latest_verifier: Option<String>,
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub latest_evidence_sha256: Option<String>,
1439    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1440    pub observed_verifier_families: Vec<AttestationVerifierFamily>,
1441    pub stale: bool,
1442}
1443
1444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1445#[serde(rename_all = "camelCase")]
1446pub struct CreditCertificationState {
1447    pub required: bool,
1448    #[serde(default, skip_serializing_if = "Option::is_none")]
1449    pub state: Option<UnderwritingCertificationState>,
1450    #[serde(default, skip_serializing_if = "Option::is_none")]
1451    pub artifact_id: Option<String>,
1452    #[serde(default, skip_serializing_if = "Option::is_none")]
1453    pub checked_at: Option<u64>,
1454    #[serde(default, skip_serializing_if = "Option::is_none")]
1455    pub published_at: Option<u64>,
1456}
1457
1458#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1459#[serde(rename_all = "camelCase")]
1460pub struct CreditProviderRiskPackageSupportBoundary {
1461    pub signed_exposure_authoritative: bool,
1462    pub signed_scorecard_authoritative: bool,
1463    pub facility_policy_authoritative: bool,
1464    pub compliance_score_reference_supported: bool,
1465    pub external_capital_review_supported: bool,
1466    pub autonomous_pricing_supported: bool,
1467    pub liability_market_supported: bool,
1468}
1469
1470impl Default for CreditProviderRiskPackageSupportBoundary {
1471    fn default() -> Self {
1472        Self {
1473            signed_exposure_authoritative: true,
1474            signed_scorecard_authoritative: true,
1475            facility_policy_authoritative: true,
1476            compliance_score_reference_supported: true,
1477            external_capital_review_supported: true,
1478            autonomous_pricing_supported: false,
1479            liability_market_supported: false,
1480        }
1481    }
1482}
1483
1484#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1485#[serde(rename_all = "camelCase")]
1486pub struct CreditProviderFacilitySnapshot {
1487    pub facility_id: String,
1488    pub issued_at: u64,
1489    pub expires_at: u64,
1490    pub disposition: CreditFacilityDisposition,
1491    pub lifecycle_state: CreditFacilityLifecycleState,
1492    #[serde(default, skip_serializing_if = "Option::is_none")]
1493    pub credit_limit: Option<MonetaryAmount>,
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub supersedes_facility_id: Option<String>,
1496    pub signer_key: String,
1497}
1498
1499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1500#[serde(rename_all = "camelCase")]
1501pub struct CreditProviderRiskPackage {
1502    pub schema: String,
1503    pub generated_at: u64,
1504    pub subject_key: String,
1505    pub filters: CreditProviderRiskPackageQuery,
1506    pub support_boundary: CreditProviderRiskPackageSupportBoundary,
1507    pub exposure: SignedExposureLedgerReport,
1508    pub scorecard: SignedCreditScorecardReport,
1509    pub facility_report: CreditFacilityReport,
1510    #[serde(default, skip_serializing_if = "Option::is_none")]
1511    pub compliance_score: Option<UnderwritingComplianceEvidence>,
1512    #[serde(default, skip_serializing_if = "Option::is_none")]
1513    pub latest_facility: Option<CreditProviderFacilitySnapshot>,
1514    #[serde(default, skip_serializing_if = "Option::is_none")]
1515    pub runtime_assurance: Option<CreditRuntimeAssuranceState>,
1516    pub certification: CreditCertificationState,
1517    pub recent_loss_history: CreditRecentLossHistory,
1518    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1519    pub evidence_refs: Vec<CreditScorecardEvidenceReference>,
1520}
1521
1522pub type SignedCreditProviderRiskPackage = SignedExportEnvelope<CreditProviderRiskPackage>;
1523
1524include!("credit/capital_and_execution.rs");