1pub mod error;
34pub use error::*;
35
36use schemars::JsonSchema;
37use serde::{Deserialize, Serialize};
38use stack_ids::{
39 AmendmentDecisionId, AmendmentProposalId, ArchiveManifestId, CharterBundleId,
40 CompactionReceiptId, DeprecationBundleId, DoctrineSnapshotId, HistoricalQueryGuaranteeId,
41 RetirementBundleId, SemanticDiffId, SurfaceStatus,
42};
43
44pub const CHARTER_BUNDLE_V1_SCHEMA: &str = "charter_bundle_v1";
45pub const DOCTRINE_SNAPSHOT_V1_SCHEMA: &str = "doctrine_snapshot_v1";
46pub const AMENDMENT_PROPOSAL_V1_SCHEMA: &str = "amendment_proposal_v1";
47pub const AMENDMENT_DECISION_V1_SCHEMA: &str = "amendment_decision_v1";
48pub const ARCHIVE_MANIFEST_V1_SCHEMA: &str = "archive_manifest_v1";
49pub const COMPACTION_RECEIPT_V1_SCHEMA: &str = "compaction_receipt_v1";
50pub const HISTORICAL_QUERY_GUARANTEE_V1_SCHEMA: &str = "historical_query_guarantee_v1";
51pub const DEPRECATION_BUNDLE_V1_SCHEMA: &str = "deprecation_bundle_v1";
52pub const RETIREMENT_BUNDLE_V1_SCHEMA: &str = "retirement_bundle_v1";
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
55pub struct CharterBundleV1 {
56 pub schema_version: String,
57 pub charter_bundle_id: CharterBundleId,
58 pub charter_name: String,
59 pub canonical_owner: String,
60 pub publication_status: SurfaceStatus,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
64pub struct DoctrineSnapshotV1 {
65 pub schema_version: String,
66 pub doctrine_snapshot_id: DoctrineSnapshotId,
67 pub charter_bundle_id: CharterBundleId,
68 pub doctrine_version: String,
69 pub advisory_only: bool,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
73pub struct AmendmentProposalV1 {
74 pub schema_version: String,
75 pub amendment_proposal_id: AmendmentProposalId,
76 pub charter_bundle_id: CharterBundleId,
77 pub doctrine_snapshot_id: DoctrineSnapshotId,
78 pub proposal_title: String,
79 #[serde(default)]
80 pub migration_obligations: Vec<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub semantic_diff_id: Option<SemanticDiffId>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub expected_archive_action: Option<String>,
85 #[serde(default)]
86 pub required_historical_query_modes: Vec<String>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub rollback_handle: Option<String>,
89 pub advisory_only: bool,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum AmendmentDisposition {
95 ApprovedWithRollback,
96 BlockedMissingRollback,
97 BlockedMissingSemanticDiff,
98 BlockedMissingArchiveGuarantee,
99 AdvisoryOnly,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
103pub struct AmendmentDecisionV1 {
104 pub schema_version: String,
105 pub amendment_decision_id: AmendmentDecisionId,
106 pub amendment_proposal_id: AmendmentProposalId,
107 pub disposition: AmendmentDisposition,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub semantic_diff_id: Option<SemanticDiffId>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub archive_manifest_id: Option<ArchiveManifestId>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub historical_query_guarantee_id: Option<HistoricalQueryGuaranteeId>,
114 pub archive_consequence_summary: String,
115 #[serde(default)]
116 pub unresolved_obligations: Vec<String>,
117 pub advisory_only: bool,
118 pub rollback_ready: bool,
119 pub decided_at: String,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
123pub struct ArchiveManifestV1 {
124 pub schema_version: String,
125 pub archive_manifest_id: ArchiveManifestId,
126 pub charter_bundle_id: CharterBundleId,
127 #[serde(default)]
128 pub preserved_refs: Vec<String>,
129 pub replay_guarantee: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133pub struct CompactionReceiptV1 {
134 pub schema_version: String,
135 pub compaction_receipt_id: CompactionReceiptId,
136 pub archive_manifest_id: ArchiveManifestId,
137 #[serde(default)]
138 pub dropped_detail_refs: Vec<String>,
139 pub queryable_degradation_note: String,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
143pub struct HistoricalQueryGuaranteeV1 {
144 pub schema_version: String,
145 pub historical_query_guarantee_id: HistoricalQueryGuaranteeId,
146 pub archive_manifest_id: ArchiveManifestId,
147 pub guarantee_text: String,
148 pub horizon_only: bool,
149 #[serde(default)]
150 pub guaranteed_query_modes: Vec<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
154pub struct DeprecationBundleV1 {
155 pub schema_version: String,
156 pub deprecation_bundle_id: DeprecationBundleId,
157 pub charter_bundle_id: CharterBundleId,
158 pub target_surface: String,
159 pub replacement_surface: String,
160 pub advisory_only: bool,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
164pub struct RetirementBundleV1 {
165 pub schema_version: String,
166 pub retirement_bundle_id: RetirementBundleId,
167 pub deprecation_bundle_id: DeprecationBundleId,
168 pub horizon_only: bool,
169}
170
171pub fn evaluate_amendment(
172 proposal: &AmendmentProposalV1,
173 migration_obligations_satisfied: bool,
174 archive_manifest: Option<&ArchiveManifestV1>,
175 historical_query_guarantee: Option<&HistoricalQueryGuaranteeV1>,
176 decided_at: impl Into<String>,
177) -> AmendmentDecisionV1 {
178 let rollback_ready = proposal.rollback_handle.is_some();
179 let semantic_diff_ready = proposal.semantic_diff_id.is_some();
180 let archive_ready = archive_manifest.is_some() && historical_query_guarantee.is_some();
181 let disposition = if proposal.advisory_only {
182 AmendmentDisposition::AdvisoryOnly
183 } else if !rollback_ready {
184 AmendmentDisposition::BlockedMissingRollback
185 } else if !semantic_diff_ready {
186 AmendmentDisposition::BlockedMissingSemanticDiff
187 } else if !archive_ready {
188 AmendmentDisposition::BlockedMissingArchiveGuarantee
189 } else if migration_obligations_satisfied {
190 AmendmentDisposition::ApprovedWithRollback
191 } else {
192 AmendmentDisposition::BlockedMissingRollback
193 };
194
195 let mut unresolved_obligations =
196 if matches!(disposition, AmendmentDisposition::ApprovedWithRollback) {
197 vec![]
198 } else {
199 proposal.migration_obligations.clone()
200 };
201 if !rollback_ready {
202 unresolved_obligations.push("rollback handle must be present".into());
203 }
204 if !semantic_diff_ready {
205 unresolved_obligations.push("semantic diff linkage must remain explicit".into());
206 }
207 if !archive_ready {
208 unresolved_obligations
209 .push("archive manifest and historical query guarantee must be emitted".into());
210 }
211
212 AmendmentDecisionV1 {
213 schema_version: AMENDMENT_DECISION_V1_SCHEMA.into(),
214 amendment_decision_id: AmendmentDecisionId::generate(),
215 amendment_proposal_id: proposal.amendment_proposal_id.clone(),
216 disposition,
217 semantic_diff_id: proposal.semantic_diff_id.clone(),
218 archive_manifest_id: archive_manifest.map(|manifest| manifest.archive_manifest_id.clone()),
219 historical_query_guarantee_id: historical_query_guarantee
220 .map(|guarantee| guarantee.historical_query_guarantee_id.clone()),
221 archive_consequence_summary: proposal
222 .expected_archive_action
223 .clone()
224 .unwrap_or_else(|| "archive consequences were not described".into()),
225 unresolved_obligations,
226 advisory_only: !matches!(disposition, AmendmentDisposition::ApprovedWithRollback),
227 rollback_ready,
228 decided_at: decided_at.into(),
229 }
230}
231
232pub fn evaluate_archive_compaction(
233 charter_bundle_id: CharterBundleId,
234 preserved_refs: Vec<String>,
235 dropped_detail_refs: Vec<String>,
236 guaranteed_query_modes: Vec<String>,
237) -> (
238 ArchiveManifestV1,
239 HistoricalQueryGuaranteeV1,
240 CompactionReceiptV1,
241) {
242 let archive_manifest = ArchiveManifestV1 {
243 schema_version: ARCHIVE_MANIFEST_V1_SCHEMA.into(),
244 archive_manifest_id: ArchiveManifestId::generate(),
245 charter_bundle_id,
246 preserved_refs,
247 replay_guarantee: "historical replay remains reconstructable through the archive manifest"
248 .into(),
249 };
250 let historical_query_guarantee = HistoricalQueryGuaranteeV1 {
251 schema_version: HISTORICAL_QUERY_GUARANTEE_V1_SCHEMA.into(),
252 historical_query_guarantee_id: HistoricalQueryGuaranteeId::generate(),
253 archive_manifest_id: archive_manifest.archive_manifest_id.clone(),
254 guarantee_text:
255 "compaction may drop detail refs but historical constitutional queries stay answerable"
256 .into(),
257 horizon_only: false,
258 guaranteed_query_modes,
259 };
260 let compaction_receipt = CompactionReceiptV1 {
261 schema_version: COMPACTION_RECEIPT_V1_SCHEMA.into(),
262 compaction_receipt_id: CompactionReceiptId::generate(),
263 archive_manifest_id: archive_manifest.archive_manifest_id.clone(),
264 dropped_detail_refs,
265 queryable_degradation_note:
266 "detail refs were compacted, but the archive manifest and historical query guarantee remain authoritative for replay".into(),
267 };
268
269 (
270 archive_manifest,
271 historical_query_guarantee,
272 compaction_receipt,
273 )
274}