Skip to main content

federated_settlement/
lib.rs

1//! Typed treaty and settlement surface crate for shared-view artifact families.
2//!
3//! The crate keeps the compatibility name, but it is not a network runtime.
4//! It publishes settlement-facing schemas and bounded evaluators that keep
5//! degraded and dissent states explicit.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use stack_ids::{
10    CrossRuntimeEquivalenceBundleId, LocalDissentId, RuntimeIdentitySetId, SettlementCaseId,
11    SettlementReceiptId, SharedDispositionId, SharedDivergenceReportId, SharedReplaySliceId,
12    SharedViewDowngradeId, TreatyBundleId, TreatySuspensionId,
13};
14
15pub use stack_ids::{SurfaceStatus, V25ConstitutionCitation};
16
17pub const TREATY_BUNDLE_V1_SCHEMA: &str = "treaty_bundle_v1";
18pub const RUNTIME_IDENTITY_SET_V1_SCHEMA: &str = "runtime_identity_set_v1";
19pub const CROSS_RUNTIME_EQUIVALENCE_BUNDLE_V1_SCHEMA: &str = "cross_runtime_equivalence_bundle_v1";
20pub const SETTLEMENT_CASE_V1_SCHEMA: &str = "settlement_case_v1";
21pub const SHARED_DISPOSITION_V1_SCHEMA: &str = "shared_disposition_v1";
22pub const LOCAL_DISSENT_RECORD_V1_SCHEMA: &str = "local_dissent_record_v1";
23pub const SHARED_VIEW_DOWNGRADE_V1_SCHEMA: &str = "shared_view_downgrade_v1";
24pub const SETTLEMENT_RECEIPT_V1_SCHEMA: &str = "settlement_receipt_v1";
25pub const SHARED_REPLAY_SLICE_V1_SCHEMA: &str = "shared_replay_slice_v1";
26pub const SHARED_DIVERGENCE_REPORT_V1_SCHEMA: &str = "shared_divergence_report_v1";
27pub const TREATY_SUSPENSION_V1_SCHEMA: &str = "treaty_suspension_v1";
28
29// Canonical v25 citations embed ApplicabilityContextId, ProfileSetId,
30// CompositionReceiptId, EffectiveConstitutionId, and CompiledObligationSetId.
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33pub struct TreatyPartyV1 {
34    pub runtime_name: String,
35    pub local_authority: String,
36    pub admission_lane: String,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40pub struct TreatyBundleV1 {
41    pub schema_version: String,
42    pub treaty_bundle_id: TreatyBundleId,
43    pub treaty_name: String,
44    #[serde(default)]
45    pub parties: Vec<TreatyPartyV1>,
46    pub canonical_owner: String,
47    pub publication_status: SurfaceStatus,
48    pub advisory_only: bool,
49    pub non_authoritative: bool,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
53pub struct RuntimeIdentityRecordV1 {
54    pub runtime_name: String,
55    pub local_subject_ref: String,
56    pub shared_subject_ref: String,
57    pub evidence_ref: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
61pub struct RuntimeIdentitySetV1 {
62    pub schema_version: String,
63    pub runtime_identity_set_id: RuntimeIdentitySetId,
64    pub treaty_bundle_id: TreatyBundleId,
65    #[serde(default)]
66    pub identities: Vec<RuntimeIdentityRecordV1>,
67    pub canonical_owner: String,
68    pub publication_status: SurfaceStatus,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub struct EquivalenceEvidenceV1 {
73    pub local_claim_ref: String,
74    pub shared_claim_ref: String,
75    #[serde(default)]
76    pub evidence_refs: Vec<String>,
77    #[serde(default, skip_serializing_if = "Option::is_none")]
78    pub expires_at: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82pub struct CrossRuntimeEquivalenceBundleV1 {
83    pub schema_version: String,
84    pub equivalence_bundle_id: CrossRuntimeEquivalenceBundleId,
85    pub treaty_bundle_id: TreatyBundleId,
86    pub runtime_identity_set_id: RuntimeIdentitySetId,
87    pub equivalence_scope: String,
88    #[serde(default)]
89    pub evidence: Vec<EquivalenceEvidenceV1>,
90    pub canonical_owner: String,
91    pub publication_status: SurfaceStatus,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
95pub struct SharedDispositionV1 {
96    pub schema_version: String,
97    pub shared_disposition_id: SharedDispositionId,
98    pub disposition_label: String,
99    pub treatment: String,
100    pub advisory_only: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
104pub struct LocalDissentRecordV1 {
105    pub schema_version: String,
106    pub local_dissent_id: LocalDissentId,
107    pub runtime_name: String,
108    pub reason: String,
109    pub blocks_shared_settlement: bool,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
113pub struct ReplayRequirementV1 {
114    pub runtime_name: String,
115    pub required: bool,
116    pub replay_available: bool,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
120#[serde(rename_all = "snake_case")]
121pub enum SettlementDisposition {
122    SharedDispositionIssued,
123    AdvisoryOnly,
124    DegradedSharedView,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
128#[serde(rename_all = "snake_case")]
129pub enum DowngradeReason {
130    MissingRequiredReplay,
131    NonAdmittedSurface,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
135pub struct SharedViewDowngradeV1 {
136    pub schema_version: String,
137    pub shared_view_downgrade_id: SharedViewDowngradeId,
138    pub reason: DowngradeReason,
139    pub detail: String,
140    pub preserves_local_dissent: bool,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
144pub struct SettlementCaseV1 {
145    pub schema_version: String,
146    pub settlement_case_id: SettlementCaseId,
147    pub treaty_bundle_id: TreatyBundleId,
148    pub equivalence_bundle_id: CrossRuntimeEquivalenceBundleId,
149    #[serde(flatten)]
150    pub citation: V25ConstitutionCitation,
151    pub shared_disposition: SharedDispositionV1,
152    #[serde(default)]
153    pub replay_requirements: Vec<ReplayRequirementV1>,
154    #[serde(default)]
155    pub local_dissent: Vec<LocalDissentRecordV1>,
156    pub advisory_only: bool,
157    pub non_admitted: bool,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
161pub struct SettlementReceiptV1 {
162    pub schema_version: String,
163    pub settlement_receipt_id: SettlementReceiptId,
164    pub settlement_case_id: SettlementCaseId,
165    #[serde(flatten)]
166    pub citation: V25ConstitutionCitation,
167    pub disposition: SettlementDisposition,
168    pub settlement_allowed: bool,
169    pub advisory_only: bool,
170    pub non_admitted: bool,
171    pub degraded: bool,
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub shared_disposition_id: Option<SharedDispositionId>,
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub downgrade: Option<SharedViewDowngradeV1>,
176    #[serde(default)]
177    pub local_dissent: Vec<LocalDissentRecordV1>,
178    pub generated_at: String,
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182pub struct SharedReplaySliceV1 {
183    pub schema_version: String,
184    pub shared_replay_slice_id: SharedReplaySliceId,
185    pub settlement_case_id: SettlementCaseId,
186    pub treaty_bundle_id: TreatyBundleId,
187    #[serde(flatten)]
188    pub citation: V25ConstitutionCitation,
189    #[serde(default)]
190    pub replay_requirements: Vec<ReplayRequirementV1>,
191    #[serde(default)]
192    pub missing_required_runtime_names: Vec<String>,
193    pub all_required_replays_available: bool,
194    pub degraded: bool,
195    pub advisory_only: bool,
196    pub generated_at: String,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200pub struct SharedDivergenceReportV1 {
201    pub schema_version: String,
202    pub shared_divergence_report_id: SharedDivergenceReportId,
203    pub settlement_case_id: SettlementCaseId,
204    pub treaty_bundle_id: TreatyBundleId,
205    pub shared_replay_slice_id: SharedReplaySliceId,
206    #[serde(flatten)]
207    pub citation: V25ConstitutionCitation,
208    #[serde(default)]
209    pub blocking_runtime_names: Vec<String>,
210    pub divergence_summary: String,
211    pub requires_local_dissent_preservation: bool,
212    pub recommends_suspension: bool,
213    pub advisory_only: bool,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
217pub struct TreatySuspensionV1 {
218    pub schema_version: String,
219    pub treaty_suspension_id: TreatySuspensionId,
220    pub treaty_bundle_id: TreatyBundleId,
221    pub shared_divergence_report_id: SharedDivergenceReportId,
222    pub suspension_scope: String,
223    pub suspension_reason: String,
224    #[serde(default)]
225    pub blocked_runtime_names: Vec<String>,
226    #[serde(default)]
227    pub resumption_requirements: Vec<String>,
228    pub advisory_only: bool,
229    pub generated_at: String,
230}
231
232pub fn evaluate_shared_replay(
233    case: &SettlementCaseV1,
234    generated_at: impl Into<String>,
235) -> SharedReplaySliceV1 {
236    let missing_required_runtime_names = case
237        .replay_requirements
238        .iter()
239        .filter(|requirement| requirement.required && !requirement.replay_available)
240        .map(|requirement| requirement.runtime_name.clone())
241        .collect::<Vec<_>>();
242
243    SharedReplaySliceV1 {
244        schema_version: SHARED_REPLAY_SLICE_V1_SCHEMA.into(),
245        shared_replay_slice_id: SharedReplaySliceId::generate(),
246        settlement_case_id: case.settlement_case_id.clone(),
247        treaty_bundle_id: case.treaty_bundle_id.clone(),
248        citation: case.citation.clone(),
249        replay_requirements: case.replay_requirements.clone(),
250        all_required_replays_available: missing_required_runtime_names.is_empty(),
251        degraded: !missing_required_runtime_names.is_empty(),
252        missing_required_runtime_names,
253        advisory_only: true,
254        generated_at: generated_at.into(),
255    }
256}
257
258pub fn evaluate_divergence_or_suspension(
259    case: &SettlementCaseV1,
260    replay: &SharedReplaySliceV1,
261    divergence_summary: impl Into<String>,
262    generated_at: impl Into<String>,
263) -> (SharedDivergenceReportV1, Option<TreatySuspensionV1>) {
264    let generated_at = generated_at.into();
265    let divergence_summary = divergence_summary.into();
266    let mut blocking_runtime_names = case
267        .local_dissent
268        .iter()
269        .filter(|dissent| dissent.blocks_shared_settlement)
270        .map(|dissent| dissent.runtime_name.clone())
271        .collect::<Vec<_>>();
272
273    for runtime_name in &replay.missing_required_runtime_names {
274        if !blocking_runtime_names.contains(runtime_name) {
275            blocking_runtime_names.push(runtime_name.clone());
276        }
277    }
278
279    let recommends_suspension = case.non_admitted
280        || !blocking_runtime_names.is_empty()
281        || (replay.degraded
282            && case
283                .local_dissent
284                .iter()
285                .any(|dissent| dissent.blocks_shared_settlement));
286
287    let report = SharedDivergenceReportV1 {
288        schema_version: SHARED_DIVERGENCE_REPORT_V1_SCHEMA.into(),
289        shared_divergence_report_id: SharedDivergenceReportId::generate(),
290        settlement_case_id: case.settlement_case_id.clone(),
291        treaty_bundle_id: case.treaty_bundle_id.clone(),
292        citation: case.citation.clone(),
293        shared_replay_slice_id: replay.shared_replay_slice_id.clone(),
294        blocking_runtime_names: blocking_runtime_names.clone(),
295        divergence_summary,
296        requires_local_dissent_preservation: !case.local_dissent.is_empty(),
297        recommends_suspension,
298        advisory_only: true,
299    };
300
301    let suspension = recommends_suspension.then(|| TreatySuspensionV1 {
302        schema_version: TREATY_SUSPENSION_V1_SCHEMA.into(),
303        treaty_suspension_id: TreatySuspensionId::generate(),
304        treaty_bundle_id: case.treaty_bundle_id.clone(),
305        shared_divergence_report_id: report.shared_divergence_report_id.clone(),
306        suspension_scope: "bounded-settlement-family".into(),
307        suspension_reason:
308            "shared settlement remains blocked until replay gaps or blocking dissent are resolved"
309                .into(),
310        blocked_runtime_names: blocking_runtime_names,
311        resumption_requirements: vec![
312            "publish a replay-complete shared replay slice".into(),
313            "preserve local dissent visibility in the next settlement review".into(),
314            "re-run settlement evaluation before any admission action".into(),
315        ],
316        advisory_only: true,
317        generated_at,
318    });
319
320    (report, suspension)
321}
322
323pub fn evaluate_settlement(
324    case: &SettlementCaseV1,
325    generated_at: impl Into<String>,
326) -> SettlementReceiptV1 {
327    let generated_at = generated_at.into();
328    let replay = evaluate_shared_replay(case, generated_at.clone());
329
330    if case.non_admitted {
331        return SettlementReceiptV1 {
332            schema_version: SETTLEMENT_RECEIPT_V1_SCHEMA.into(),
333            settlement_receipt_id: SettlementReceiptId::generate(),
334            settlement_case_id: case.settlement_case_id.clone(),
335            citation: case.citation.clone(),
336            disposition: SettlementDisposition::AdvisoryOnly,
337            settlement_allowed: false,
338            advisory_only: true,
339            non_admitted: true,
340            degraded: false,
341            shared_disposition_id: None,
342            downgrade: Some(SharedViewDowngradeV1 {
343                schema_version: SHARED_VIEW_DOWNGRADE_V1_SCHEMA.into(),
344                shared_view_downgrade_id: SharedViewDowngradeId::generate(),
345                reason: DowngradeReason::NonAdmittedSurface,
346                detail: "settlement remains non-admitted and cannot create shared authority".into(),
347                preserves_local_dissent: true,
348            }),
349            local_dissent: case.local_dissent.clone(),
350            generated_at,
351        };
352    }
353
354    if replay.degraded {
355        return SettlementReceiptV1 {
356            schema_version: SETTLEMENT_RECEIPT_V1_SCHEMA.into(),
357            settlement_receipt_id: SettlementReceiptId::generate(),
358            settlement_case_id: case.settlement_case_id.clone(),
359            citation: case.citation.clone(),
360            disposition: SettlementDisposition::DegradedSharedView,
361            settlement_allowed: false,
362            advisory_only: true,
363            non_admitted: false,
364            degraded: true,
365            shared_disposition_id: None,
366            downgrade: Some(SharedViewDowngradeV1 {
367                schema_version: SHARED_VIEW_DOWNGRADE_V1_SCHEMA.into(),
368                shared_view_downgrade_id: SharedViewDowngradeId::generate(),
369                reason: DowngradeReason::MissingRequiredReplay,
370                detail: "missing replay on one side forces explicit downgrade instead of silent settlement".into(),
371                preserves_local_dissent: true,
372            }),
373            local_dissent: case.local_dissent.clone(),
374            generated_at,
375        };
376    }
377
378    SettlementReceiptV1 {
379        schema_version: SETTLEMENT_RECEIPT_V1_SCHEMA.into(),
380        settlement_receipt_id: SettlementReceiptId::generate(),
381        settlement_case_id: case.settlement_case_id.clone(),
382        citation: case.citation.clone(),
383        disposition: if case.advisory_only {
384            SettlementDisposition::AdvisoryOnly
385        } else {
386            SettlementDisposition::SharedDispositionIssued
387        },
388        settlement_allowed: !case.advisory_only,
389        advisory_only: case.advisory_only,
390        non_admitted: false,
391        degraded: false,
392        shared_disposition_id: Some(case.shared_disposition.shared_disposition_id.clone()),
393        downgrade: None,
394        local_dissent: case.local_dissent.clone(),
395        generated_at,
396    }
397}