Skip to main content

jar_genesis/workflow/
pr_opened.rs

1use std::path::Path;
2
3use crate::cache;
4use crate::git;
5use crate::github;
6use crate::lean;
7use crate::snapshot;
8use crate::types::SelectTargetsOutput;
9
10/// Run the PR-opened workflow: compute and post comparison targets.
11pub fn run(pr: u64, created_at: &str) -> Result<(), Box<dyn std::error::Error>> {
12    let repo_root = git::repo_root()?;
13    let spec_dir = Path::new(&repo_root).join("spec");
14
15    // Fetch and read cache
16    git::fetch("origin", "genesis-state")?;
17    let cache_json = git::show_file("origin/genesis-state:genesis.json")?;
18    let cache_indices: Vec<serde_json::Value> = serde_json::from_str(&cache_json)?;
19
20    // Check cache staleness
21    if let Err(e) = cache::check_staleness(&cache_indices, &spec_dir) {
22        github::pr_comment(
23            pr,
24            &format!(
25                "**JAR Bot:** Genesis cache is stale — cannot compute comparison targets. {e}"
26            ),
27        )?;
28        return Err(e.into());
29    }
30
31    // Parse PR created_at to epoch
32    let pr_created_epoch = parse_iso8601_to_epoch(created_at)?;
33
34    // Get ranking + variances snapshot
35    let ranking_json =
36        git::show_file("origin/genesis-state:ranking.json").unwrap_or_else(|_| "{}".to_string());
37    let ranking_map: serde_json::Value = serde_json::from_str(&ranking_json)?;
38    let scores_json =
39        git::show_file("origin/genesis-state:scores.json").unwrap_or_else(|_| "{}".to_string());
40    let scores_map: serde_json::Value = serde_json::from_str(&scores_json)?;
41
42    let snap = snapshot::find(&cache_indices, &ranking_map, &scores_map, pr_created_epoch)?;
43
44    // Build input for genesis_select_targets
45    let mut input = serde_json::json!({
46        "prId": pr,
47        "prCreatedAt": pr_created_epoch,
48        "indices": cache_indices,
49    });
50    if let Some(s) = &snap {
51        input["ranking"] = s.ranking.clone();
52        if let Some(v) = &s.variances {
53            input["variances"] = v.clone();
54        }
55    }
56
57    let output: SelectTargetsOutput = lean::invoke("genesis_select_targets", &input, &spec_dir)?;
58
59    // Format and post comment
60    let mut comment = String::from("## Genesis Review\n\n**Comparison targets:**\n\n");
61    for target in &output.targets {
62        let short = &target[..8.min(target.len())];
63        comment.push_str(&format!("- `{short}` ({target})\n"));
64    }
65    comment.push_str("\n### How to review\n\n");
66    comment.push_str("Post a comment with the following format (rank from best to worst):\n\n");
67    comment.push_str("```\n/review\n");
68    comment.push_str("difficulty: <commit1>, <commit2>, ..., <commitN>, currentPR\n");
69    comment.push_str("novelty: <commit1>, <commit2>, ..., <commitN>, currentPR\n");
70    comment.push_str("design: <commit1>, <commit2>, ..., <commitN>, currentPR\n");
71    comment.push_str("verdict: merge\n```\n\n");
72    comment.push_str("Use the short commit hashes above and `currentPR` for this PR.\n");
73    comment.push_str("Each line ranks all comparison targets + this PR from best to worst.\n\n");
74    comment.push_str("To meta-review another reviewer's comment, react with 👍 or 👎.");
75
76    github::pr_comment(pr, &comment)?;
77
78    Ok(())
79}
80
81fn parse_iso8601_to_epoch(s: &str) -> Result<u64, Box<dyn std::error::Error>> {
82    // Simple ISO 8601 parsing: "2026-03-25T10:51:59Z"
83    // Use the `date` command for robustness
84    let output = std::process::Command::new("date")
85        .args(["-d", s, "+%s"])
86        .output()?;
87    if !output.status.success() {
88        return Err(format!("failed to parse date '{s}'").into());
89    }
90    let epoch: u64 = String::from_utf8_lossy(&output.stdout).trim().parse()?;
91    Ok(epoch)
92}