1pub mod error;
33pub use error::*;
34
35use schemars::JsonSchema;
36use serde::{Deserialize, Serialize};
37use stack_ids::{
38 FitRunId, HypothesisLibraryId, MechanismBundleId, RolloutStabilityReportId,
39 SimulationContractId, SurfaceStatus, TheoryLibraryId, TheoryRefuterSuiteId, TheoryVersionId,
40};
41
42pub const MECHANISM_BUNDLE_V1_SCHEMA: &str = "mechanism_bundle_v1";
43pub const THEORY_VERSION_V1_SCHEMA: &str = "theory_version_v1";
44pub const THEORY_LIBRARY_V1_SCHEMA: &str = "theory_library_v1";
45pub const HYPOTHESIS_LIBRARY_V1_SCHEMA: &str = "hypothesis_library_v1";
46pub const SIMULATION_CONTRACT_V1_SCHEMA: &str = "simulation_contract_v1";
47pub const FIT_RUN_V1_SCHEMA: &str = "fit_run_v1";
48pub const THEORY_REFUTER_SUITE_V1_SCHEMA: &str = "theory_refuter_suite_v1";
49pub const ROLLOUT_STABILITY_REPORT_V1_SCHEMA: &str = "rollout_stability_report_v1";
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
52pub struct MechanismBundleV1 {
53 pub schema_version: String,
54 pub mechanism_bundle_id: MechanismBundleId,
55 pub mechanism_name: String,
56 pub canonical_owner: String,
57 pub publication_status: SurfaceStatus,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
61pub struct TheoryVersionV1 {
62 pub schema_version: String,
63 pub theory_version_id: TheoryVersionId,
64 pub mechanism_bundle_id: MechanismBundleId,
65 pub theory_name: String,
66 pub semantic_version: String,
67 pub advisory_only: bool,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
71pub struct TheoryLibraryV1 {
72 pub schema_version: String,
73 pub theory_library_id: TheoryLibraryId,
74 #[serde(default)]
75 pub theory_version_ids: Vec<TheoryVersionId>,
76 pub canonical_owner: String,
77 pub publication_status: SurfaceStatus,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
81pub struct HypothesisLibraryV1 {
82 pub schema_version: String,
83 pub hypothesis_library_id: HypothesisLibraryId,
84 #[serde(default)]
85 pub hypothesis_refs: Vec<String>,
86 pub publication_status: SurfaceStatus,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
90pub struct SimulationContractV1 {
91 pub schema_version: String,
92 pub simulation_contract_id: SimulationContractId,
93 pub mechanism_bundle_id: MechanismBundleId,
94 pub target_outcome: String,
95 #[serde(default)]
96 pub required_inputs: Vec<String>,
97 pub advisory_only: bool,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
101#[serde(rename_all = "snake_case")]
102pub enum FitDisposition {
103 AdvisoryFitOnly,
104 PromotionBlockedMissingRefuter,
105 PromotionBlockedFailingRefuter,
106 PromotionBlockedStabilityRisk,
107 EligibleForLocalReview,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
111pub struct FitRunV1 {
112 pub schema_version: String,
113 pub fit_run_id: FitRunId,
114 pub mechanism_bundle_id: MechanismBundleId,
115 pub theory_version_id: TheoryVersionId,
116 pub simulation_contract_id: SimulationContractId,
117 pub fit_score: f64,
118 pub disposition: FitDisposition,
119 pub advisory_only: bool,
120 pub degraded: bool,
121 pub refuter_ready: bool,
122 pub stability_clear: bool,
123 pub theory_refuter_suite_id: TheoryRefuterSuiteId,
124 pub rollout_stability_report_id: RolloutStabilityReportId,
125 #[serde(default)]
126 pub notes: Vec<String>,
127 pub generated_at: String,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
131pub struct TheoryRefuterSuiteV1 {
132 pub schema_version: String,
133 pub theory_refuter_suite_id: TheoryRefuterSuiteId,
134 pub theory_version_id: TheoryVersionId,
135 #[serde(default)]
136 pub required_refuters: Vec<String>,
137 #[serde(default)]
138 pub available_refuters: Vec<String>,
139 #[serde(default)]
140 pub failing_refuters: Vec<String>,
141 pub horizon_only: bool,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
145pub struct RolloutStabilityReportV1 {
146 pub schema_version: String,
147 pub rollout_stability_report_id: RolloutStabilityReportId,
148 pub theory_version_id: TheoryVersionId,
149 pub invariance_summary: String,
150 pub advisory_only: bool,
151 pub locally_reviewable: bool,
152 #[serde(default)]
153 pub blocking_findings: Vec<String>,
154}
155
156pub fn evaluate_fit_run(
157 mechanism: &MechanismBundleV1,
158 theory: &TheoryVersionV1,
159 contract: &SimulationContractV1,
160 refuter_suite: &TheoryRefuterSuiteV1,
161 stability_report: &RolloutStabilityReportV1,
162 fit_score: f64,
163 generated_at: impl Into<String>,
164) -> FitRunV1 {
165 let missing_required_refuters = refuter_suite
166 .required_refuters
167 .iter()
168 .filter(|required| !refuter_suite.available_refuters.contains(required))
169 .cloned()
170 .collect::<Vec<_>>();
171 let refuter_ready = missing_required_refuters.is_empty()
172 && refuter_suite.failing_refuters.is_empty()
173 && !refuter_suite.horizon_only;
174 let stability_clear = !stability_report.advisory_only
175 && stability_report.locally_reviewable
176 && stability_report.blocking_findings.is_empty();
177
178 let disposition = if !refuter_ready && !refuter_suite.failing_refuters.is_empty() {
179 FitDisposition::PromotionBlockedFailingRefuter
180 } else if !refuter_ready {
181 FitDisposition::PromotionBlockedMissingRefuter
182 } else if !stability_clear {
183 FitDisposition::PromotionBlockedStabilityRisk
184 } else if fit_score >= 0.8 && !theory.advisory_only && !contract.advisory_only {
185 FitDisposition::EligibleForLocalReview
186 } else {
187 FitDisposition::AdvisoryFitOnly
188 };
189
190 let mut notes = Vec::new();
191 if !missing_required_refuters.is_empty() {
192 notes.push(format!(
193 "missing required refuters: {}",
194 missing_required_refuters.join(", ")
195 ));
196 }
197 if !refuter_suite.failing_refuters.is_empty() {
198 notes.push(format!(
199 "failing refuters remain visible: {}",
200 refuter_suite.failing_refuters.join(", ")
201 ));
202 }
203 if !stability_report.blocking_findings.is_empty() {
204 notes.push(format!(
205 "stability blockers: {}",
206 stability_report.blocking_findings.join(", ")
207 ));
208 }
209 if matches!(disposition, FitDisposition::EligibleForLocalReview) {
210 notes
211 .push("fit run is eligible for local review only; no authority is created here".into());
212 } else if notes.is_empty() {
213 notes.push("fit run remains advisory until typed refuter and stability gates clear".into());
214 }
215
216 FitRunV1 {
217 schema_version: FIT_RUN_V1_SCHEMA.into(),
218 fit_run_id: FitRunId::generate(),
219 mechanism_bundle_id: mechanism.mechanism_bundle_id.clone(),
220 theory_version_id: theory.theory_version_id.clone(),
221 simulation_contract_id: contract.simulation_contract_id.clone(),
222 fit_score,
223 disposition,
224 advisory_only: disposition != FitDisposition::EligibleForLocalReview,
225 degraded: disposition != FitDisposition::EligibleForLocalReview,
226 refuter_ready,
227 stability_clear,
228 theory_refuter_suite_id: refuter_suite.theory_refuter_suite_id.clone(),
229 rollout_stability_report_id: stability_report.rollout_stability_report_id.clone(),
230 notes,
231 generated_at: generated_at.into(),
232 }
233}