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");