1use serde::{Deserialize, Serialize};
6use serde_json::{Value, json};
7
8use crate::schema::{
9 Identity, IsoDateTime, NonEmptyString, Property, RunxSchema, deserialize_false_bool,
10 deserialize_true_bool, object_schema,
11};
12use crate::{JsonObject, Reference, ReferenceLink};
13
14pub const OPERATIONAL_PROPOSAL_SCHEMA: &str = "runx.operational_proposal.v1";
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, RunxSchema)]
17pub enum OperationalProposalSchema {
18 #[serde(rename = "runx.operational_proposal.v1")]
19 V1,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, RunxSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum OperationalProposalRedactionStatus {
25 Redacted,
26 SummaryOnly,
27 Blocked,
28}
29
30#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
31#[serde(deny_unknown_fields)]
32pub struct OperationalProposalRecommendedAction {
33 pub action_intent: NonEmptyString,
34 pub summary: NonEmptyString,
35 pub mutating: bool,
36 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub target_refs: Vec<Reference>,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
41#[serde(deny_unknown_fields)]
42pub struct OperationalProposalIdempotency {
43 pub key: NonEmptyString,
44 pub fingerprint: NonEmptyString,
45}
46
47#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct OperationalProposalAuthority {
50 #[serde(deserialize_with = "deserialize_true_bool")]
51 pub proposal_only: bool,
52 #[serde(deserialize_with = "deserialize_false_bool")]
53 pub mutation_authority_granted: bool,
54 #[serde(deserialize_with = "deserialize_false_bool")]
55 pub publication_authority_granted: bool,
56 #[serde(deserialize_with = "deserialize_false_bool")]
57 pub final_decision_authority_granted: bool,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
59 pub notes: Vec<NonEmptyString>,
60}
61
62impl RunxSchema for OperationalProposalAuthority {
63 fn json_schema() -> Value {
64 object_schema(
65 vec![
66 Property::new("proposal_only", const_bool(true), true),
67 Property::new("mutation_authority_granted", const_bool(false), true),
68 Property::new("publication_authority_granted", const_bool(false), true),
69 Property::new("final_decision_authority_granted", const_bool(false), true),
70 Property::new("notes", Vec::<NonEmptyString>::json_schema(), false),
71 ],
72 true,
73 None,
74 )
75 }
76}
77
78#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, RunxSchema)]
79#[serde(deny_unknown_fields)]
80pub struct OperationalProposalHumanGate {
81 pub gate_id: NonEmptyString,
82 pub gate_kind: NonEmptyString,
83 pub required: bool,
84 pub decision: NonEmptyString,
85 pub reason: NonEmptyString,
86}
87
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, RunxSchema)]
89#[serde(deny_unknown_fields)]
90pub struct OperationalProposalOutcome {
91 pub observed: bool,
92 pub status: NonEmptyString,
93 pub summary: NonEmptyString,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub observed_at: Option<IsoDateTime>,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub refs: Vec<Reference>,
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct OperationalProposal {
103 pub schema: OperationalProposalSchema,
104 pub proposal_id: NonEmptyString,
105 pub proposal_kind: NonEmptyString,
106 pub source_event_id: NonEmptyString,
107 pub idempotency: OperationalProposalIdempotency,
108 pub source_ref: Reference,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub source_thread_ref: Option<Reference>,
111 pub hydrated_context_ref: Reference,
112 pub redaction_status: OperationalProposalRedactionStatus,
113 pub decision_summary: NonEmptyString,
114 pub rationale: NonEmptyString,
115 #[serde(default)]
116 pub recommended_actions: Vec<OperationalProposalRecommendedAction>,
117 #[serde(default)]
118 pub evidence_refs: Vec<Reference>,
119 #[serde(default)]
120 pub artifact_refs: Vec<Reference>,
121 #[serde(default)]
122 pub receipt_refs: Vec<Reference>,
123 #[serde(default)]
124 pub story_refs: Vec<Reference>,
125 #[serde(default)]
126 pub result_refs: Vec<ReferenceLink>,
127 #[serde(default)]
128 pub publication_refs: Vec<ReferenceLink>,
129 pub owner_route_id: NonEmptyString,
130 pub confidence: f64,
131 #[serde(default)]
132 pub risks: Vec<NonEmptyString>,
133 #[serde(default)]
134 pub caveats: Vec<NonEmptyString>,
135 #[serde(default)]
136 pub missing_context: Vec<NonEmptyString>,
137 pub authority: OperationalProposalAuthority,
138 #[serde(default)]
139 pub human_gates: Vec<OperationalProposalHumanGate>,
140 #[serde(default)]
141 pub allowed_next_actions: Vec<NonEmptyString>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub final_outcome: Option<OperationalProposalOutcome>,
144 pub public_summary: NonEmptyString,
145 #[serde(skip_serializing_if = "Option::is_none")]
148 pub extensions: Option<JsonObject>,
149}
150
151impl RunxSchema for OperationalProposal {
152 fn json_schema() -> Value {
156 object_schema(
157 vec![
158 Property::new("schema", OperationalProposalSchema::json_schema(), true),
159 Property::new("proposal_id", id_schema(), true),
160 Property::new("proposal_kind", NonEmptyString::json_schema(), true),
161 Property::new("source_event_id", id_schema(), true),
162 Property::new(
163 "idempotency",
164 OperationalProposalIdempotency::json_schema(),
165 true,
166 ),
167 Property::new("source_ref", Reference::json_schema(), true),
168 Property::new("source_thread_ref", Reference::json_schema(), false),
169 Property::new("hydrated_context_ref", Reference::json_schema(), true),
170 Property::new(
171 "redaction_status",
172 OperationalProposalRedactionStatus::json_schema(),
173 true,
174 ),
175 Property::new("decision_summary", NonEmptyString::json_schema(), true),
176 Property::new("rationale", NonEmptyString::json_schema(), true),
177 Property::new(
178 "recommended_actions",
179 Vec::<OperationalProposalRecommendedAction>::json_schema(),
180 false,
181 ),
182 Property::new("evidence_refs", Vec::<Reference>::json_schema(), false),
183 Property::new("artifact_refs", Vec::<Reference>::json_schema(), false),
184 Property::new("receipt_refs", Vec::<Reference>::json_schema(), false),
185 Property::new("story_refs", Vec::<Reference>::json_schema(), false),
186 Property::new("result_refs", Vec::<ReferenceLink>::json_schema(), false),
187 Property::new(
188 "publication_refs",
189 Vec::<ReferenceLink>::json_schema(),
190 false,
191 ),
192 Property::new("owner_route_id", id_schema(), true),
193 Property::new("confidence", confidence_schema(), true),
194 Property::new("risks", Vec::<NonEmptyString>::json_schema(), false),
195 Property::new("caveats", Vec::<NonEmptyString>::json_schema(), false),
196 Property::new(
197 "missing_context",
198 Vec::<NonEmptyString>::json_schema(),
199 false,
200 ),
201 Property::new(
202 "authority",
203 OperationalProposalAuthority::json_schema(),
204 true,
205 ),
206 Property::new(
207 "human_gates",
208 Vec::<OperationalProposalHumanGate>::json_schema(),
209 false,
210 ),
211 Property::new(
212 "allowed_next_actions",
213 Vec::<NonEmptyString>::json_schema(),
214 false,
215 ),
216 Property::new(
217 "final_outcome",
218 OperationalProposalOutcome::json_schema(),
219 false,
220 ),
221 Property::new("public_summary", NonEmptyString::json_schema(), true),
222 Property::new("extensions", JsonObject::json_schema(), false),
223 ],
224 true,
225 Some(Identity::Runx {
226 logical: OPERATIONAL_PROPOSAL_SCHEMA,
227 url: None,
228 }),
229 )
230 }
231}
232
233fn id_schema() -> Value {
234 json!({
235 "minLength": 1,
236 "pattern": "^[A-Za-z0-9_.:-]+$",
237 "type": "string"
238 })
239}
240
241fn confidence_schema() -> Value {
242 json!({
243 "maximum": 1,
244 "minimum": 0,
245 "type": "number"
246 })
247}
248
249fn const_bool(value: bool) -> Value {
250 json!({ "const": value, "type": "boolean" })
251}