use std::borrow::Cow;
use argonauts_core::events::run::RunStarted;
use argonauts_core::names::{ModelRef, RoleName};
use argonauts_core::settings::Settings;
use argonauts_core::steps::intake::InstructionKind;
use argonauts_core::{Note, NoteLevel};
use super::{candidates, conflict, display_name, provider};
use crate::engine::host::Host;
use crate::error::EngineResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Layer {
#[default]
Settings,
Words,
Command,
}
impl Layer {
fn yours(self) -> &'static str {
match self {
Layer::Settings => "your settings",
Layer::Words => "your request",
Layer::Command => "your command",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Rule {
PerTask,
Workers,
Reviewers,
OtherProvider,
}
const GIVE_WAY: [Rule; 4] = [
Rule::PerTask,
Rule::Workers,
Rule::Reviewers,
Rule::OtherProvider,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Layers {
pub per_task: Layer,
pub workers: Layer,
pub reviewers: Layer,
pub other_provider: Layer,
}
impl Layers {
pub fn of(started: Option<&RunStarted>) -> Self {
let Some(started) = started else {
return Self::default();
};
let commanded = |keys: &[&str]| {
started.overrides.iter().any(|set| {
set.from == "flag"
&& keys.iter().any(|key| {
set.key == *key
|| set
.key
.strip_prefix(key)
.is_some_and(|rest| rest.starts_with('.'))
})
})
};
let said = |kinds: &[InstructionKind]| {
started
.instructions
.iter()
.any(|instruction| instruction.followed() && kinds.contains(&instruction.kind))
};
let layer = |keys: &[&str], kinds: &[InstructionKind]| {
if commanded(keys) {
Layer::Command
} else if said(kinds) {
Layer::Words
} else {
Layer::Settings
}
};
use InstructionKind as Said;
Self {
per_task: layer(&["review.per_task"], &[Said::Review]),
workers: layer(
&["roles.worker", "crew"],
&[Said::Build, Said::Crew, Said::Exclude],
),
reviewers: layer(
&["roles.reviewer", "crew"],
&[Said::Review, Said::Crew, Said::Exclude],
),
other_provider: layer(&["review.other_provider"], &[]),
}
}
fn of_rule(&self, rule: Rule) -> Layer {
match rule {
Rule::PerTask => self.per_task,
Rule::Workers => self.workers,
Rule::Reviewers => self.reviewers,
Rule::OtherProvider => self.other_provider,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TaskRoles {
pub workers: Vec<ModelRef>,
pub reviewers: Vec<ModelRef>,
pub per_task: u32,
pub other_provider: bool,
pub left_out: Vec<(ModelRef, String)>,
pub relaxed: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
struct Rules {
workers: Vec<ModelRef>,
reviewers: Vec<ModelRef>,
per_task: u32,
other_provider: bool,
}
impl Rules {
fn of(settings: &Settings) -> Self {
Self {
workers: candidates(settings, RoleName::Worker),
reviewers: candidates(settings, RoleName::Reviewer),
per_task: settings.review.per_task,
other_provider: settings.review.other_provider,
}
}
fn reviewers_for(
&self,
worker: &ModelRef,
provider: &dyn Fn(&ModelRef) -> String,
) -> Vec<ModelRef> {
reviewers_for(&self.reviewers, self.other_provider, worker, provider)
}
fn may_own(&self, worker: &ModelRef, provider: &dyn Fn(&ModelRef) -> String) -> bool {
self.reviewers_for(worker, provider).len() >= self.per_task as usize
}
fn hold(&self, provider: &dyn Fn(&ModelRef) -> String) -> bool {
self.workers
.iter()
.any(|worker| self.may_own(worker, provider))
}
fn eased(
&self,
rule: Rule,
everyone: &[ModelRef],
provider: &dyn Fn(&ModelRef) -> String,
) -> Option<Rules> {
let widened = |pinned: &[ModelRef]| {
let mut all = pinned.to_vec();
all.extend(everyone.iter().filter(|w| !pinned.contains(w)).cloned());
(all.len() > pinned.len()).then_some(all)
};
let mut eased = self.clone();
match rule {
Rule::PerTask => {
let most = self
.workers
.iter()
.map(|worker| self.reviewers_for(worker, provider).len())
.max()
.unwrap_or(0) as u32;
if most == 0 || most >= self.per_task {
return None;
}
eased.per_task = most;
}
Rule::Workers => eased.workers = widened(&self.workers)?,
Rule::Reviewers => eased.reviewers = widened(&self.reviewers)?,
Rule::OtherProvider => {
if !self.other_provider {
return None;
}
eased.other_provider = false;
}
}
Some(eased)
}
fn why_not_worker(&self, worker: &ModelRef, provider: &dyn Fn(&ModelRef) -> String) -> String {
let name = display_name(worker);
let others: Vec<&ModelRef> = self
.reviewers
.iter()
.filter(|reviewer| {
conflict(RoleName::Reviewer, reviewer, Some(worker), provider).is_none()
})
.collect();
if others.is_empty() {
return if self.reviewers.len() == 1 {
format!("{name} is the only reviewer you allowed, so {name} can't own a task")
} else {
format!("Every reviewer you allowed is {name}, so {name} can't own a task")
};
}
let fit = self.reviewers_for(worker, provider).len();
if self.other_provider && fit == 0 {
return format!(
"Every other reviewer you allowed is from {name}'s provider, and nobody reviews \
their own provider's work, so {name} can't own a task"
);
}
format!(
"{} of the reviewers you allowed may review {name}'s tasks, and each needs {}, so \
{name} can't own a task",
count(fit),
self.per_task
)
}
fn eased_line(&self, rule: Rule, from: &Rules) -> String {
match rule {
Rule::PerTask => format!(
"each task gets {} reviewer{}, not {}",
self.per_task,
plural(self.per_task as usize),
from.per_task
),
Rule::Workers => format!(
"any of the crew may own a task, not only {}",
names(&from.workers)
),
Rule::Reviewers => format!(
"any of the crew may review, not only {}",
names(&from.reviewers)
),
Rule::OtherProvider => "a task may be reviewed by its worker's provider".to_owned(),
}
}
}
pub fn reviewers_for(
pool: &[ModelRef],
other_provider: bool,
worker: &ModelRef,
provider: &dyn Fn(&ModelRef) -> String,
) -> Vec<ModelRef> {
let worker_provider = provider(worker);
pool.iter()
.filter(|reviewer| conflict(RoleName::Reviewer, reviewer, Some(worker), provider).is_none())
.filter(|reviewer| !other_provider || provider(reviewer) != worker_provider)
.cloned()
.collect()
}
pub fn settle(
settings: &Settings,
layers: Layers,
provider: &dyn Fn(&ModelRef) -> String,
) -> TaskRoles {
let pinned = Rules::of(settings);
let mut everyone: Vec<ModelRef> = Vec::new();
for model in &settings.crew.members() {
if !everyone.contains(model) {
everyone.push(model.clone());
}
}
if everyone.is_empty() {
everyone.push(settings.crew.main.clone());
}
let mut order = GIVE_WAY.to_vec();
order.sort_by_key(|rule| layers.of_rule(*rule));
let mut rules = pinned.clone();
let mut gave_way: Vec<Rule> = Vec::new();
if !rules.hold(provider) {
let alone = order.iter().find_map(|rule| {
let eased = rules.eased(*rule, &everyone, provider)?;
eased.hold(provider).then_some((*rule, eased))
});
match alone {
Some((rule, eased)) => {
gave_way.push(rule);
rules = eased;
}
None => {
for rule in &order {
if rules.hold(provider) {
break;
}
if let Some(eased) = rules.eased(*rule, &everyone, provider) {
gave_way.push(*rule);
rules = eased;
}
}
}
}
}
let relaxed = (!gave_way.is_empty()).then(|| {
let mut said = Vec::new();
let mut at = pinned.clone();
for rule in &gave_way {
let eased = at
.eased(*rule, &everyone, provider)
.unwrap_or_else(|| at.clone());
said.push(format!(
"the one from {} gives way: {}",
layers.of_rule(*rule).yours(),
eased.eased_line(*rule, &at)
));
at = eased;
}
let why: Vec<String> = pinned
.workers
.iter()
.filter(|worker| !pinned.may_own(worker, provider))
.map(|worker| format!("{}.", pinned.why_not_worker(worker, provider)))
.collect();
format!(
"Your rules for who owns and reviews tasks can't all hold, so {}.\n{}",
said.join("; and "),
why.join("\n")
)
.trim_end()
.to_owned()
});
let (workers, left_out): (Vec<ModelRef>, Vec<ModelRef>) = rules
.workers
.iter()
.cloned()
.partition(|worker| rules.may_own(worker, provider));
let (workers, left_out) = if workers.is_empty() {
(rules.workers.clone(), Vec::new())
} else {
(workers, left_out)
};
TaskRoles {
workers,
left_out: left_out
.into_iter()
.map(|worker| {
let why = rules.why_not_worker(&worker, provider);
(worker, why)
})
.collect(),
reviewers: rules.reviewers,
per_task: rules.per_task,
other_provider: rules.other_provider,
relaxed,
}
}
pub fn for_run(host: &Host) -> TaskRoles {
let layers = host.read(|state| Layers::of(state.run.started.as_deref()));
settle(host.settings(), layers, &|worker| provider(host, worker))
}
pub fn settled(host: &Host) -> Cow<'_, Settings> {
for_run(host).apply(host.settings())
}
impl TaskRoles {
pub fn apply<'a>(&self, settings: &'a Settings) -> Cow<'a, Settings> {
if self.relaxed.is_none() && self.left_out.is_empty() {
return Cow::Borrowed(settings);
}
let mut settled = settings.clone();
settled.roles.entry(RoleName::Worker).or_default().models = self.workers.clone();
settled.roles.entry(RoleName::Reviewer).or_default().models = self.reviewers.clone();
settled.review.per_task = self.per_task;
settled.review.other_provider = self.other_provider;
Cow::Owned(settled)
}
pub fn listed(&self) -> Vec<ModelRef> {
let mut workers: Vec<ModelRef> = Vec::new();
let all = self
.workers
.iter()
.chain(&self.reviewers)
.chain(self.left_out.iter().map(|(worker, _)| worker));
for worker in all {
if !workers.contains(worker) {
workers.push(worker.clone());
}
}
workers
}
pub fn named(&self, text: &str) -> Option<ModelRef> {
named_in(text, &self.listed()).or_else(|| text.trim().parse().ok())
}
pub fn may_own(&self, worker: &ModelRef) -> bool {
self.workers.contains(worker)
}
pub fn may_review(&self, worker: &ModelRef) -> bool {
self.reviewers.contains(worker)
}
pub fn reviewers_for(
&self,
worker: &ModelRef,
provider: &dyn Fn(&ModelRef) -> String,
) -> Vec<ModelRef> {
reviewers_for(&self.reviewers, self.other_provider, worker, provider)
}
pub fn why_not_worker(&self, worker: &ModelRef) -> String {
if let Some((_, why)) = self.left_out.iter().find(|(left, _)| left == worker) {
return why.clone();
}
format!("only {} may own tasks", names(&self.workers))
}
pub fn why_not_reviewer(
&self,
reviewer: &ModelRef,
worker: &ModelRef,
provider: &dyn Fn(&ModelRef) -> String,
) -> String {
if !self.may_review(reviewer) {
return format!("only {} may review tasks", names(&self.reviewers));
}
if conflict(RoleName::Reviewer, reviewer, Some(worker), provider).is_some() {
return "nobody reviews their own task".to_owned();
}
format!(
"{} is from the provider of the task's worker, {}",
display_name(reviewer),
display_name(worker)
)
}
pub fn lines(&self, provider: &dyn Fn(&ModelRef) -> String) -> Vec<String> {
self.listed()
.iter()
.map(|worker| {
let may = match (self.may_own(worker), self.may_review(worker)) {
(true, true) => "may own tasks and review them",
(true, false) => "may own tasks, not review them",
(false, true) => "may review tasks, not own them",
(false, false) => "may neither own nor review tasks",
};
let mut line = format!(
"`{worker}` is {}, from {}: it {may}.",
display_name(worker),
provider(worker)
);
if let Some((_, why)) = self.left_out.iter().find(|(left, _)| left == worker) {
line.push_str(&format!(" {why}."));
}
line
})
.collect()
}
pub fn review_line(&self) -> String {
format!(
"Each task gets {} reviewer{}, never its worker{}.",
self.per_task,
plural(self.per_task as usize),
if self.other_provider {
", each from another provider than its worker"
} else {
""
}
)
}
pub fn announce(&self, host: &Host) -> EngineResult<()> {
let Some(text) = &self.relaxed else {
return Ok(());
};
let first = text.lines().next().unwrap_or_default();
if host.read(|state| state.run.said.contains_key(first)) {
return Ok(());
}
host.record(
None,
None,
argonauts_core::events::RunEventKind::Note(Note {
level: NoteLevel::Warn,
text: text.clone(),
}),
)?;
Ok(())
}
}
pub fn named_in(text: &str, pool: &[ModelRef]) -> Option<ModelRef> {
let named: ModelRef = text.trim().parse().ok()?;
if pool.contains(&named) {
return Some(named);
}
let mut same_cli = pool.iter().filter(|worker| worker.agent == named.agent);
match (same_cli.next(), same_cli.next()) {
(Some(only), None) => Some(only.clone()),
_ => None,
}
}
pub fn names(workers: &[ModelRef]) -> String {
let names: Vec<String> = workers.iter().map(display_name).collect();
match names.as_slice() {
[] => "nobody".to_owned(),
[one] => one.clone(),
[rest @ .., last] => format!("{} and {last}", rest.join(", ")),
}
}
fn count(n: usize) -> String {
match n {
0 => "None".to_owned(),
1 => "Only 1".to_owned(),
n => format!("Only {n}"),
}
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
#[cfg(test)]
mod tests;