1use 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#[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}