mod detail;
mod form;
mod runs;
pub mod search;
pub use detail::{Stage, meta, outcome_text, shared, stage, technical};
pub use form::{Field, Form, Input, Key, boundary, template};
pub use runs::Runs;
use crate::{
config::{HumanToolKind, ToolName},
human,
store::{self, HumanClaim, Request},
tools::human::{CommandLine, Content, ToolResult},
types::RequestId,
};
use serde_json::Value;
use std::path::PathBuf;
use tokio_util::sync::CancellationToken;
pub const CLAIM_FIRST: &str = "Claim this request before reviewing it.";
pub const NOT_CLAIMED: &str = "The request is not claimed.";
pub fn actionable(
view: Option<&crate::store::RequestView>,
reviewer: &crate::types::ReviewerId,
) -> Result<(RequestId, bool), String> {
let view = view.ok_or("The request is still loading.")?;
if view.request.status() != crate::types::RequestStatus::WaitingHuman {
return Err(format!(
"The request is {}; there is nothing to review.",
view.request.status()
));
}
match &view.claim {
Some(claim) if &claim.reviewer != reviewer => Err(format!(
"Claimed by {}; read-only for {}.",
claim.reviewer, reviewer
)),
claim => Ok((view.request.id.clone(), claim.is_none())),
}
}
pub fn review_refusal(
view: Option<&crate::store::RequestView>,
reviewer: &crate::types::ReviewerId,
) -> Option<String> {
match actionable(view, reviewer) {
Ok((_, false)) => None,
Ok((_, true)) | Err(_) => Some(CLAIM_FIRST.into()),
}
}
pub fn release_refusal(
view: Option<&crate::store::RequestView>,
reviewer: &crate::types::ReviewerId,
) -> Option<String> {
match actionable(view, reviewer) {
Ok((_, false)) => None,
Ok((_, true)) => Some(NOT_CLAIMED.into()),
Err(error) => Some(error),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Job {
Claim { id: RequestId },
Run {
id: RequestId,
tool: ToolName,
claim: bool,
},
Submit {
id: RequestId,
verdict: crate::runtime::Verdict,
result: Value,
claim: bool,
},
Release { ids: Vec<RequestId>, quit: bool },
}
#[derive(Debug)]
pub enum Outcome {
Claimed {
result: Result<HumanClaim, String>,
},
SavedLocally {
request: Box<Request>,
error: String,
},
Ran {
tool: ToolName,
claimed: Option<HumanClaim>,
result: Result<ToolResult, String>,
},
Submitted {
id: RequestId,
claimed: Option<HumanClaim>,
result: Result<Box<Request>, String>,
},
Released {
ids: Vec<RequestId>,
quit: bool,
failed: Vec<(RequestId, String)>,
},
}
impl Outcome {
pub fn notice(&self) -> (String, bool) {
match self {
Self::SavedLocally { error, .. } => (error.clone(), true),
Self::Claimed { result: Ok(_) } => (
"Claimed. Fill GREEN or RED fields, then Submit.".into(),
false,
),
Self::Claimed { result: Err(error) } => (error.clone(), true),
Self::Ran { tool, result, .. } => {
let launched = Content::Launch { launched: true };
match result {
Ok(result) if result.is_error() => (
format!("{tool} failed; a tool error is not a verdict."),
true,
),
Ok(result) if result.content().contains(&launched) => {
(format!("{tool} launched."), false)
}
Ok(_) => (format!("{tool} finished."), false),
Err(_) => (format!("{tool} did not run."), true),
}
}
Self::Submitted {
result: Ok(request),
..
} => (
format!(
"Submitted {} for {} ({}).",
request.status(),
request.eval_id,
request.id
),
false,
),
Self::Submitted {
result: Err(error), ..
} => (error.clone(), true),
Self::Released { failed, .. } if failed.is_empty() => ("Claim released.".into(), false),
Self::Released { failed, .. } => {
let lines = failed.iter().map(|(id, error)| format!("{id}: {error}"));
(lines.collect::<Vec<_>>().join("\n"), true)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tool {
pub name: ToolName,
pub kind: HumanToolKind,
pub description: String,
pub declared: Runs,
}
impl Tool {
pub fn action(&self, resolved: Option<&Result<CommandLine, String>>) -> String {
match (&self.declared, resolved) {
(Runs::Command(_), Some(Ok(command))) => {
let program = crate::platform::path_text(std::path::Path::new(&command.program));
let words = std::iter::once(program).chain(command.args.iter().cloned());
let words: Vec<_> = words.collect();
format!("$ {}", shell(words.iter().map(String::as_str)))
}
(declared, _) => declared.line(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Output {
pub title: String,
pub text: String,
pub error: bool,
}
impl Job {
pub async fn run(
self,
state: PathBuf,
reviewer: crate::types::ReviewerId,
cancel: CancellationToken,
) -> Outcome {
let mut claimed = None;
match self {
Job::Claim { id } => {
let result = async {
let (receipts, _) = human::open(&state, &id).await?;
human::claim(&receipts, &id, &reviewer).await
}
.await;
Outcome::Claimed { result }
}
Job::Run {
id,
tool,
claim: first,
} => {
let result = async {
let (receipts, _) = human::open(&state, &id).await?;
if first {
claimed = Some(human::claim(&receipts, &id, &reviewer).await?);
}
human::run_human_tool(&receipts, &id, &reviewer, &tool, cancel).await
}
.await;
Outcome::Ran {
tool,
claimed,
result,
}
}
Job::Submit {
id,
verdict: _,
result,
claim: first,
} => {
let result = async {
if first {
let (receipts, _) = human::open(&state, &id).await?;
claimed = Some(human::claim(&receipts, &id, &reviewer).await?);
}
human::submit_and_publish(&state, &id, &reviewer, &result, cancel)
.await
.map(Box::new)
}
.await;
if let Err(error) = &result
&& let Ok(view) = store::read_request(&state, &id).await
&& matches!(
view.request.status(),
crate::types::RequestStatus::Green | crate::types::RequestStatus::Red
)
&& view.execution.as_ref().is_some_and(|execution| {
execution.reviewer.as_deref() == Some(reviewer.as_str())
})
{
return Outcome::SavedLocally {
request: Box::new(view.request),
error: error.clone(),
};
}
Outcome::Submitted {
id,
claimed,
result,
}
}
Job::Release { ids, quit } => {
let mut failed = Vec::new();
for id in &ids {
let release = async {
let view = store::read_request(&state, id).await?;
if view.claim.is_none_or(|claim| claim.reviewer != reviewer) {
return Ok(());
}
let (receipts, _) = human::open(&state, id).await?;
human::unclaim(&receipts, id, &reviewer).await.map(drop)
};
if let Err(error) = release.await {
failed.push((id.clone(), error));
}
}
Outcome::Released { ids, quit, failed }
}
}
}
}
pub fn tools(request: &Request) -> Vec<Tool> {
use crate::store::definitions::HumanTool;
let Some(artifacts) = request
.human_definition
.as_ref()
.and_then(|definition| definition.artifacts.value())
else {
return Vec::new();
};
let mut tools: Vec<_> = artifacts
.iter()
.flat_map(|(id, artifact)| {
artifact
.views
.value()
.and_then(|views| views.human_tools.value())
.into_iter()
.flatten()
.filter_map(move |(operation, tool)| {
let (kind, description) = match tool {
HumanTool::Command(tool) => (tool.kind, &tool.description),
HumanTool::Builtin(tool) => (tool.kind, &tool.description),
};
Some(Tool {
name: format!("{operation}_{id}").parse().ok()?,
kind,
description: description.replace("{artifactName}", id.as_str()),
declared: Runs::parse(artifacts, id, tool),
})
})
})
.collect();
tools.sort_by(|a, b| a.name.cmp(&b.name));
tools
}
pub fn schema(
request: &Request,
verdict: crate::runtime::Verdict,
) -> Result<Option<Value>, String> {
let definition = request.human_definition.as_ref().ok_or(
"This request records no Human definition; open the original request its actions go to.",
)?;
let declaration = definition.eval.declaration.value();
let schema = declaration.and_then(|declaration| match verdict {
crate::runtime::Verdict::Green => declaration.pass_schema.value(),
crate::runtime::Verdict::Red => declaration.fail_schema.value(),
});
Ok(schema.map(|schema| Value::Object(schema.clone())))
}
pub fn shell<'a>(words: impl IntoIterator<Item = &'a str>) -> String {
let quote = |word: &str| {
let plain = !word.is_empty()
&& word
.chars()
.all(|c| c.is_ascii_alphanumeric() || "-_./=:,+@%{}".contains(c));
if plain {
word.to_owned()
} else {
format!("\"{}\"", word.replace('"', "\"\""))
}
};
words.into_iter().map(quote).collect::<Vec<_>>().join(" ")
}
pub fn output(tool: &str, result: Result<ToolResult, String>) -> Output {
match result {
Ok(result) => Output {
title: if result.is_error() {
format!("{tool} · tool error")
} else {
tool.to_owned()
},
text: result
.content()
.iter()
.map(|content| match content {
Content::Text { text } => text.trim_end().to_owned(),
Content::Launch { launched: true } => "launched".into(),
Content::Launch { launched: false } => "not launched".into(),
})
.collect::<Vec<_>>()
.join("\n"),
error: result.is_error(),
},
Err(error) => Output {
title: format!("{tool} · failed"),
text: error,
error: true,
},
}
}