jar_genesis/workflow/
review.rs1use std::path::Path;
2
3use crate::cache;
4use crate::git;
5use crate::github;
6use crate::lean;
7use crate::review;
8use crate::snapshot;
9use crate::types::{MergeReadiness, SelectTargetsOutput};
10
11pub fn run(
13 pr: u64,
14 comment_author: &str,
15 _comment_body: &str,
16) -> Result<(), Box<dyn std::error::Error>> {
17 let state_json = github::pr_view(pr, "state")?;
19 let state = state_json["state"].as_str().unwrap_or("");
20 if state != "OPEN" {
21 github::pr_comment(
22 pr,
23 &format!("**JAR Bot:** PR is not open (state: {state}) — ignoring `/review`."),
24 )?;
25 return Err(format!("PR #{pr} is not open (state: {state})").into());
26 }
27
28 let repo_root = git::repo_root()?;
29 let spec_dir = Path::new(&repo_root).join("spec");
30
31 git::fetch("origin", "genesis-state")?;
33 let cache_json = git::show_file("origin/genesis-state:genesis.json")?;
34 let cache_indices: Vec<serde_json::Value> = serde_json::from_str(&cache_json)?;
35
36 if let Err(e) = cache::check_staleness(&cache_indices, &spec_dir) {
38 github::pr_comment(
39 pr,
40 &format!("**JAR Bot:** Genesis cache is stale — cannot process review. {e}"),
41 )?;
42 return Err(e.into());
43 }
44
45 let pr_json = github::pr_view(pr, "headRefOid,createdAt")?;
47 let head_sha = pr_json["headRefOid"].as_str().unwrap_or("").to_string();
48 let pr_created_at = pr_json["createdAt"].as_str().unwrap_or("");
49
50 let pr_created_epoch = {
51 let output = std::process::Command::new("date")
52 .args(["-d", pr_created_at, "+%s"])
53 .output()?;
54 String::from_utf8_lossy(&output.stdout)
55 .trim()
56 .parse::<u64>()?
57 };
58
59 let ranking_json =
61 git::show_file("origin/genesis-state:ranking.json").unwrap_or_else(|_| "{}".to_string());
62 let ranking_map: serde_json::Value = serde_json::from_str(&ranking_json)?;
63 let scores_json =
64 git::show_file("origin/genesis-state:scores.json").unwrap_or_else(|_| "{}".to_string());
65 let scores_map: serde_json::Value = serde_json::from_str(&scores_json)?;
66
67 let snap = snapshot::find(&cache_indices, &ranking_map, &scores_map, pr_created_epoch)?;
68
69 let mut targets_input = serde_json::json!({
71 "prId": pr,
72 "prCreatedAt": pr_created_epoch,
73 "indices": cache_indices,
74 });
75 if let Some(s) = &snap {
76 targets_input["ranking"] = s.ranking.clone();
77 if let Some(v) = &s.variances {
78 targets_input["variances"] = v.clone();
79 }
80 }
81
82 let targets_output: SelectTargetsOutput =
83 lean::invoke("genesis_select_targets", &targets_input, &spec_dir)?;
84 let targets = targets_output.targets;
85
86 let collected = review::collect(pr, &head_sha, &targets)?;
88
89 let check_input = serde_json::json!({
91 "reviews": collected.reviews,
92 "metaReviews": collected.meta_reviews,
93 "indices": cache_indices,
94 });
95 let readiness: MergeReadiness = lean::invoke("genesis_check_merge", &check_input, &spec_dir)?;
96
97 let review_count = collected.reviews.len();
98 let meta_count = collected.meta_reviews.len();
99
100 if readiness.ready {
101 github::workflow_run("genesis-merge.yml", &[("pr_number", &pr.to_string())])?;
103
104 github::pr_comment(
105 pr,
106 &format!(
107 "**JAR Bot:** Quorum reached — triggering merge.\n\
108 Reviews: {review_count}, meta-reviews: {meta_count}.\n\
109 Merge weight: {}/{} (>50%).",
110 readiness.merge_weight, readiness.total_weight
111 ),
112 )?;
113 } else {
114 github::pr_comment(
115 pr,
116 &format!(
117 "**JAR Bot:** Review recorded from @{comment_author} ({review_count} reviews, {meta_count} meta-reviews).\n\
118 Merge weight: {}/{} (need >50%).",
119 readiness.merge_weight, readiness.total_weight
120 ),
121 )?;
122 }
123
124 Ok(())
125}