1use serde::Deserialize;
18use std::collections::BTreeMap;
19use std::io::Cursor;
20use tiny_http::Response;
21
22use crate::handlers::{error_response, json_response, State};
23
24pub fn review_inbox_handler(state: &State, query: &str) -> Response<Cursor<Vec<u8>>> {
28 let branch = query
29 .split('&')
30 .find_map(|kv| kv.strip_prefix("branch="))
31 .map(str::to_string);
32 let store = state.store.lock().unwrap();
33 let branch = branch.unwrap_or_else(|| store.current_branch());
34
35 let head_op = match store.get_branch(&branch) {
36 Ok(Some(b)) => b.head_op,
37 Ok(None) => return error_response(404, format!("unknown branch {branch:?}")),
38 Err(e) => return error_response(500, format!("get_branch: {e}")),
39 };
40 let Some(head_op) = head_op else {
41 return json_response(200, &serde_json::json!({ "branch": branch, "items": [] }));
42 };
43
44 let stage_intent = match stage_intents(&store, &head_op) {
46 Ok(m) => m,
47 Err(e) => return error_response(500, format!("reading intents: {e}")),
48 };
49
50 let head = store.branch_head(&branch).unwrap_or_default();
52 let pairs: Vec<(String, String)> = head.iter().map(|(s, st)| (s.clone(), st.clone())).collect();
53 let asts = store.get_asts_for_sigs_bulk(&pairs);
54
55 let mut items = Vec::new();
56 for ((_sig, stage_id), ast) in pairs.iter().zip(asts) {
57 let name = match ast {
58 Ok(lex_ast::Stage::FnDecl(fd)) => fd.name,
59 Ok(lex_ast::Stage::TypeDecl(td)) => td.name,
60 _ => continue,
61 };
62 let verdict = store
63 .latest_review_verdict(stage_id)
64 .ok()
65 .flatten()
66 .map(|v| match v {
67 lex_vcs::ReviewVerdict::Approve => "approved",
68 lex_vcs::ReviewVerdict::Reject => "rejected",
69 lex_vcs::ReviewVerdict::RequestChanges => "changes_requested",
70 })
71 .unwrap_or("none");
72 let needs_review = verdict != "approved";
75 items.push(serde_json::json!({
76 "stage_id": stage_id,
77 "name": name,
78 "intent": stage_intent.get(stage_id),
79 "review": verdict,
80 "needs_review": needs_review,
81 }));
82 }
83
84 json_response(200, &serde_json::json!({
85 "branch": branch,
86 "head_op": head_op,
87 "items": items,
88 }))
89}
90
91#[derive(Deserialize)]
92struct VerdictReq {
93 stage_id: String,
94 verdict: String,
96 reviewer: String,
97 #[serde(default)]
98 note: Option<String>,
99}
100
101pub fn review_verdict_handler(state: &State, body: &str) -> Response<Cursor<Vec<u8>>> {
105 let req: VerdictReq = match serde_json::from_str(body) {
106 Ok(r) => r,
107 Err(e) => return error_response(400, format!("bad request: {e}")),
108 };
109 let verdict = match req.verdict.as_str() {
110 "approve" => lex_vcs::ReviewVerdict::Approve,
111 "reject" => lex_vcs::ReviewVerdict::Reject,
112 "request_changes" => lex_vcs::ReviewVerdict::RequestChanges,
113 other => return error_response(400, format!("verdict must be approve|reject|request_changes, got {other:?}")),
114 };
115 if req.reviewer.trim().is_empty() {
116 return error_response(400, "reviewer must be non-empty");
117 }
118 let store = state.store.lock().unwrap();
119 if store.get_metadata(&req.stage_id).is_err() {
121 return error_response(404, format!("unknown stage {:?}", req.stage_id));
122 }
123 match store.record_review(&req.stage_id, None, &req.reviewer, verdict, req.note) {
124 Ok(id) => json_response(201, &serde_json::json!({
125 "attestation_id": id,
126 "stage_id": req.stage_id,
127 "reviewer": req.reviewer,
128 })),
129 Err(e) => error_response(500, format!("record_review: {e}")),
130 }
131}
132
133fn stage_intents(
136 store: &lex_store::Store,
137 head_op: &str,
138) -> Result<BTreeMap<String, String>, String> {
139 let log = lex_vcs::OpLog::open(store.root()).map_err(|e| e.to_string())?;
140 let intents = lex_vcs::IntentLog::open(store.root()).map_err(|e| e.to_string())?;
141 let mut out: BTreeMap<String, String> = BTreeMap::new();
142 for rec in log.walk_forward(&head_op.to_string(), None).map_err(|e| e.to_string())? {
143 let Some(intent_id) = &rec.op.intent_id else { continue };
144 let Some(intent) = intents.get(intent_id).map_err(|e| e.to_string())? else { continue };
145 let first_line = intent.prompt.lines().next().unwrap_or("").to_string();
146 for stage_id in rec.produces.stage_ids() {
147 out.insert(stage_id, first_line.clone());
148 }
149 }
150 Ok(out)
151}