1use super::proof_traffic_receipt::ProofTrafficDecision;
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, BTreeSet};
12
13pub const PROOF_TRAFFIC_PARKING_LOT_SCHEMA_VERSION: &str = "proof-traffic-parking-lot-v1";
15
16const NO_CLAIM_BOUNDARIES: &[&str] = &[
17 "No release-readiness claim.",
18 "No broad workspace-health claim.",
19 "No runtime-correctness claim.",
20 "No performance-improvement claim.",
21 "No live RCH fleet-availability claim.",
22 "No local Cargo fallback approval.",
23 "No peer-owned build cancellation authority.",
24 "Parked, refused, or stale attempts are not green proof evidence.",
25];
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct ProofTrafficRetryPredicate {
31 pub predicate_id: String,
33 pub condition: String,
35 pub required_signal: String,
37 pub satisfied: bool,
39}
40
41impl ProofTrafficRetryPredicate {
42 #[must_use]
44 pub fn new(
45 predicate_id: impl Into<String>,
46 condition: impl Into<String>,
47 required_signal: impl Into<String>,
48 satisfied: bool,
49 ) -> Self {
50 Self {
51 predicate_id: predicate_id.into(),
52 condition: condition.into(),
53 required_signal: required_signal.into(),
54 satisfied,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct ParkedProofAttempt {
62 pub attempt_id: String,
64 pub blocker_key: String,
66 pub head_commit: String,
68 pub command_intent: String,
70 pub exact_rch_command: Option<String>,
72 pub blocker_marker: String,
74 pub target_dir: String,
76 pub owned_paths: Vec<String>,
78 pub reservation_evidence: Vec<String>,
80 pub blocker_class: ProofTrafficDecision,
82 pub blocker_owner: Option<String>,
84 pub handoff_thread: Option<String>,
86 pub retry_predicate: ProofTrafficRetryPredicate,
88 pub no_claim_boundaries: Vec<String>,
90}
91
92impl ParkedProofAttempt {
93 #[must_use]
95 pub fn new(
96 attempt_id: impl Into<String>,
97 blocker_key: impl Into<String>,
98 head_commit: impl Into<String>,
99 command_intent: impl Into<String>,
100 target_dir: impl Into<String>,
101 blocker_class: ProofTrafficDecision,
102 retry_predicate: ProofTrafficRetryPredicate,
103 ) -> Self {
104 Self {
105 attempt_id: attempt_id.into(),
106 blocker_key: blocker_key.into(),
107 head_commit: head_commit.into(),
108 command_intent: command_intent.into(),
109 exact_rch_command: None,
110 blocker_marker: "# PARKED: no proof command emitted".to_string(),
111 target_dir: target_dir.into(),
112 owned_paths: Vec::new(),
113 reservation_evidence: Vec::new(),
114 blocker_class,
115 blocker_owner: None,
116 handoff_thread: None,
117 retry_predicate,
118 no_claim_boundaries: NO_CLAIM_BOUNDARIES
119 .iter()
120 .map(|boundary| (*boundary).to_string())
121 .collect(),
122 }
123 }
124
125 #[must_use]
128 pub fn with_exact_rch_command(mut self, command: impl Into<String>) -> Self {
129 self.exact_rch_command = Some(command.into());
130 self
131 }
132
133 #[must_use]
135 pub fn with_blocker_marker(mut self, marker: impl Into<String>) -> Self {
136 self.blocker_marker = marker.into();
137 self
138 }
139
140 #[must_use]
142 pub fn with_paths(
143 mut self,
144 owned_paths: Vec<String>,
145 reservation_evidence: Vec<String>,
146 ) -> Self {
147 self.owned_paths = sorted_unique(owned_paths);
148 self.reservation_evidence = sorted_unique(reservation_evidence);
149 self
150 }
151
152 #[must_use]
154 pub fn with_handoff(
155 mut self,
156 blocker_owner: Option<String>,
157 handoff_thread: Option<String>,
158 ) -> Self {
159 self.blocker_owner = blocker_owner;
160 self.handoff_thread = handoff_thread;
161 self
162 }
163
164 #[must_use]
166 pub fn can_be_cited_as_green(&self) -> bool {
167 false
168 }
169
170 #[must_use]
172 pub fn status_label(&self) -> &'static str {
173 if self.retry_predicate.satisfied {
174 "retry-ready"
175 } else {
176 "parked"
177 }
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct ParkedProofGroup {
184 pub blocker_key: String,
186 pub attempt_ids: Vec<String>,
188 pub command_intents: Vec<String>,
190 pub owned_paths: Vec<String>,
192 pub attempt_count: usize,
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198pub struct ProofTrafficParkingLot {
199 pub schema_version: String,
201 pub lot_id: String,
203 pub attempts: Vec<ParkedProofAttempt>,
205 pub groups: Vec<ParkedProofGroup>,
207 pub no_claim_boundaries: Vec<String>,
209}
210
211impl ProofTrafficParkingLot {
212 #[must_use]
214 pub fn new(lot_id: impl Into<String>, attempts: Vec<ParkedProofAttempt>) -> Self {
215 let mut attempts = attempts;
216 attempts.sort_by(|left, right| left.attempt_id.cmp(&right.attempt_id));
217 let groups = group_attempts(&attempts);
218 Self {
219 schema_version: PROOF_TRAFFIC_PARKING_LOT_SCHEMA_VERSION.to_string(),
220 lot_id: lot_id.into(),
221 attempts,
222 groups,
223 no_claim_boundaries: NO_CLAIM_BOUNDARIES
224 .iter()
225 .map(|boundary| (*boundary).to_string())
226 .collect(),
227 }
228 }
229
230 #[must_use]
236 pub fn render_resume(&self, attempt_id: &str) -> Option<String> {
237 let attempt = self
238 .attempts
239 .iter()
240 .find(|attempt| attempt.attempt_id == attempt_id)?;
241 if attempt.retry_predicate.satisfied {
242 if let Some(command) = &attempt.exact_rch_command {
243 return Some(command.clone());
244 }
245 }
246 Some(render_fresh_blocker(attempt))
247 }
248
249 #[must_use]
251 pub fn render_markdown(&self) -> String {
252 let mut out = String::new();
253 out.push_str("## Proof-traffic parking lot - ");
254 out.push_str(&self.lot_id);
255 out.push_str("\n\n");
256 out.push_str(&format!(
257 "- schema_version: `{}`\n- attempt_count: `{}`\n- group_count: `{}`\n\n",
258 self.schema_version,
259 self.attempts.len(),
260 self.groups.len()
261 ));
262
263 out.push_str("### groups\n");
264 if self.groups.is_empty() {
265 out.push_str("- _none_\n");
266 } else {
267 for group in &self.groups {
268 out.push_str("- `");
269 out.push_str(&group.blocker_key);
270 out.push_str("` attempts=");
271 out.push_str(&group.attempt_ids.join(","));
272 out.push_str(" count=");
273 out.push_str(&group.attempt_count.to_string());
274 out.push('\n');
275 }
276 }
277 out.push('\n');
278
279 out.push_str("### attempts\n");
280 if self.attempts.is_empty() {
281 out.push_str("- _none_\n");
282 } else {
283 for attempt in &self.attempts {
284 out.push_str("- `");
285 out.push_str(&attempt.attempt_id);
286 out.push_str("` status=`");
287 out.push_str(attempt.status_label());
288 out.push_str("` blocker=`");
289 out.push_str(attempt.blocker_class.label());
290 out.push_str("` retry=`");
291 out.push_str(&attempt.retry_predicate.predicate_id);
292 out.push_str("`\n");
293 }
294 }
295 out.push('\n');
296
297 push_string_section(&mut out, "no_claim_boundaries", &self.no_claim_boundaries);
298 out
299 }
300
301 #[must_use]
303 pub fn agent_mail_body(&self) -> String {
304 let mut out = String::new();
305 out.push_str("proof_traffic_parking_lot:\n");
306 out.push_str(&format!(
307 "- lot_id: `{}`\n- attempt_count: `{}`\n- group_count: `{}`\n",
308 self.lot_id,
309 self.attempts.len(),
310 self.groups.len()
311 ));
312 for group in &self.groups {
313 out.push_str("- blocker_key: `");
314 out.push_str(&group.blocker_key);
315 out.push_str("` attempts: `");
316 out.push_str(&group.attempt_ids.join(","));
317 out.push_str("`\n");
318 }
319 out
320 }
321
322 #[must_use]
324 pub fn br_comment_body(&self) -> String {
325 let mut out = String::new();
326 out.push_str("Proof-traffic parking lot\n\n");
327 out.push_str(&format!(
328 "- lot_id: `{}`\n- attempt_count: `{}`\n- group_count: `{}`\n",
329 self.lot_id,
330 self.attempts.len(),
331 self.groups.len()
332 ));
333 out
334 }
335}
336
337fn group_attempts(attempts: &[ParkedProofAttempt]) -> Vec<ParkedProofGroup> {
338 let mut by_key: BTreeMap<String, Vec<&ParkedProofAttempt>> = BTreeMap::new();
339 for attempt in attempts {
340 by_key
341 .entry(attempt.blocker_key.clone())
342 .or_default()
343 .push(attempt);
344 }
345
346 by_key
347 .into_iter()
348 .map(|(blocker_key, attempts)| {
349 let attempt_ids = attempts
350 .iter()
351 .map(|attempt| attempt.attempt_id.clone())
352 .collect::<Vec<_>>();
353 let command_intents = attempts
354 .iter()
355 .map(|attempt| attempt.command_intent.clone())
356 .collect::<BTreeSet<_>>()
357 .into_iter()
358 .collect::<Vec<_>>();
359 let owned_paths = attempts
360 .iter()
361 .flat_map(|attempt| attempt.owned_paths.iter().cloned())
362 .collect::<BTreeSet<_>>()
363 .into_iter()
364 .collect::<Vec<_>>();
365 ParkedProofGroup {
366 blocker_key,
367 attempt_count: attempt_ids.len(),
368 attempt_ids,
369 command_intents,
370 owned_paths,
371 }
372 })
373 .collect()
374}
375
376fn render_fresh_blocker(attempt: &ParkedProofAttempt) -> String {
377 format!(
378 "# PARKED: blocker={} retry_predicate={} satisfied={} required_signal={}; no proof command emitted",
379 attempt.blocker_class.label(),
380 attempt.retry_predicate.predicate_id,
381 attempt.retry_predicate.satisfied,
382 attempt.retry_predicate.required_signal
383 )
384}
385
386fn push_string_section(out: &mut String, title: &str, values: &[String]) {
387 out.push_str(&format!("### {title} ({})\n", values.len()));
388 if values.is_empty() {
389 out.push_str("- _none_\n");
390 } else {
391 for value in values {
392 out.push_str("- ");
393 out.push_str(value);
394 out.push('\n');
395 }
396 }
397 out.push('\n');
398}
399
400fn sorted_unique(mut values: Vec<String>) -> Vec<String> {
401 values.sort();
402 values.dedup();
403 values
404}