pub const REP_GAIN_ON_ACCEPT: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
Executive,
ProductManager,
Coder,
Reviewer,
Accounting,
Hr,
Marketing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Criteria {
pub min_quality: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Submission {
pub quality: u8,
pub claims_impossible: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stage {
Planned,
Posted,
Assigned { worker_id: u64 },
Submitted { worker_id: u64, submission: Submission },
Accepted { worker_id: u64, rating: u8, paid: u128 },
Rejected { worker_id: u64, rating: u8 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
pub id: u64,
pub role: Role,
pub reward: u128,
pub min_reputation: u32,
pub criteria: Criteria,
pub stage: Stage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorkerState {
pub id: u64,
pub role: Role,
pub reputation: u32,
pub available: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Backlog {
pub tasks: Vec<Task>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Assignment {
pub task_id: u64,
pub worker_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AcceptDecision {
Accept { rating: u8 },
Reject { rating: u8 },
}
impl AcceptDecision {
pub fn is_accept(self) -> bool {
matches!(self, AcceptDecision::Accept { .. })
}
pub fn rating(self) -> u8 {
match self {
AcceptDecision::Accept { rating } | AcceptDecision::Reject { rating } => rating,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
PostBounty { task_id: u64, reward: u128 },
AssignTask { task_id: u64, worker_id: u64 },
AcceptResult { task_id: u64, worker_id: u64 },
RejectResult { task_id: u64, worker_id: u64 },
Payout { task_id: u64, worker_id: u64, amount: u128 },
Attest { subject_id: u64, rating: u8, work_ref: u64 },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct State {
pub backlog: Backlog,
pub workers: Vec<WorkerState>,
pub treasury: u128,
}
pub fn assign_next_task(backlog: &Backlog, workers: &[WorkerState]) -> Option<Assignment> {
use crate::hiring::{best_candidate, Candidate, RoleNeed};
let mut posted: Vec<&Task> =
backlog.tasks.iter().filter(|t| t.stage == Stage::Posted).collect();
posted.sort_by(|a, b| b.reward.cmp(&a.reward).then(a.id.cmp(&b.id)));
let pool: Vec<Candidate> = workers.iter().copied().map(Candidate::from).collect();
for task in posted {
let need = RoleNeed { role: task.role, min_reputation: task.min_reputation };
if let Some(best) = best_candidate(&need, &pool) {
return Some(Assignment { task_id: task.id, worker_id: best.id });
}
}
None
}
pub fn evaluate_result(submission: &Submission, criteria: &Criteria) -> AcceptDecision {
if submission.claims_impossible {
return AcceptDecision::Reject { rating: 1 };
}
let rating = submission.quality.clamp(1, 5);
if rating >= criteria.min_quality {
AcceptDecision::Accept { rating }
} else {
AcceptDecision::Reject { rating }
}
}
pub fn compute_payout(task: &Task, treasury_balance: u128) -> u128 {
task.reward.min(treasury_balance)
}
impl State {
pub fn deliver(&mut self, task_id: u64, submission: Submission) -> bool {
for t in &mut self.backlog.tasks {
if t.id == task_id {
if let Stage::Assigned { worker_id } = t.stage {
t.stage = Stage::Submitted { worker_id, submission };
return true;
}
return false;
}
}
false
}
}
pub fn step(state: &State) -> (State, Vec<Action>) {
let mut next = state.clone();
let submitted_idx = next
.backlog
.tasks
.iter()
.enumerate()
.filter(|(_, t)| matches!(t.stage, Stage::Submitted { .. }))
.min_by_key(|(_, t)| t.id)
.map(|(i, _)| i);
if let Some(i) = submitted_idx {
let task = next.backlog.tasks[i].clone();
let Stage::Submitted { worker_id: w, submission } = &task.stage else {
return (next, Vec::new()); };
let worker_id = *w;
let mut actions = Vec::new();
match evaluate_result(submission, &task.criteria) {
AcceptDecision::Accept { rating } => {
let amount = compute_payout(&task, next.treasury);
next.treasury -= amount;
actions.push(Action::AcceptResult { task_id: task.id, worker_id });
actions.push(Action::Payout { task_id: task.id, worker_id, amount });
actions.push(Action::Attest { subject_id: worker_id, rating, work_ref: task.id });
next.backlog.tasks[i].stage = Stage::Accepted { worker_id, rating, paid: amount };
free_worker(&mut next.workers, worker_id, true);
}
AcceptDecision::Reject { rating } => {
actions.push(Action::RejectResult { task_id: task.id, worker_id });
actions.push(Action::Attest { subject_id: worker_id, rating, work_ref: task.id });
next.backlog.tasks[i].stage = Stage::Rejected { worker_id, rating };
free_worker(&mut next.workers, worker_id, false);
}
}
return (next, actions);
}
if let Some(a) = assign_next_task(&next.backlog, &next.workers) {
if let Some(ti) = next.backlog.tasks.iter().position(|t| t.id == a.task_id) {
next.backlog.tasks[ti].stage = Stage::Assigned { worker_id: a.worker_id };
}
if let Some(wi) = next.workers.iter().position(|w| w.id == a.worker_id) {
next.workers[wi].available = false;
}
return (next, vec![Action::AssignTask { task_id: a.task_id, worker_id: a.worker_id }]);
}
if let Some(i) = min_id_idx(&next.backlog.tasks, |t| t.stage == Stage::Planned) {
let (id, reward) = (next.backlog.tasks[i].id, next.backlog.tasks[i].reward);
next.backlog.tasks[i].stage = Stage::Posted;
return (next, vec![Action::PostBounty { task_id: id, reward }]);
}
(next, Vec::new())
}
fn min_id_idx(tasks: &[Task], pred: impl Fn(&Task) -> bool) -> Option<usize> {
tasks
.iter()
.enumerate()
.filter(|(_, t)| pred(t))
.min_by_key(|(_, t)| t.id)
.map(|(i, _)| i)
}
fn free_worker(workers: &mut [WorkerState], id: u64, accepted: bool) {
if let Some(w) = workers.iter_mut().find(|w| w.id == id) {
w.available = true;
if accepted {
w.reputation = w.reputation.saturating_add(REP_GAIN_ON_ACCEPT);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn task(id: u64, role: Role, reward: u128, min_rep: u32, min_quality: u8) -> Task {
Task {
id,
role,
reward,
min_reputation: min_rep,
criteria: Criteria { min_quality },
stage: Stage::Planned,
}
}
fn posted(id: u64, role: Role, reward: u128, min_rep: u32, min_quality: u8) -> Task {
Task { stage: Stage::Posted, ..task(id, role, reward, min_rep, min_quality) }
}
fn worker(id: u64, role: Role, rep: u32) -> WorkerState {
WorkerState { id, role, reputation: rep, available: true }
}
fn sub(quality: u8) -> Submission {
Submission { quality, claims_impossible: false }
}
#[test]
fn assign_prefers_high_reward_then_high_reputation() {
let backlog = Backlog {
tasks: vec![posted(1, Role::Coder, 10, 0, 3), posted(2, Role::Coder, 50, 0, 3)],
};
let workers = vec![worker(7, Role::Coder, 2), worker(8, Role::Coder, 9)];
assert_eq!(
assign_next_task(&backlog, &workers),
Some(Assignment { task_id: 2, worker_id: 8 })
);
}
#[test]
fn assign_respects_role_and_min_reputation_and_skips_unstaffable() {
let backlog = Backlog {
tasks: vec![
posted(1, Role::Reviewer, 100, 5, 3), posted(2, Role::Coder, 20, 0, 3),
],
};
let workers = vec![
worker(3, Role::Reviewer, 4), worker(4, Role::Coder, 1), ];
assert_eq!(
assign_next_task(&backlog, &workers),
Some(Assignment { task_id: 2, worker_id: 4 })
);
}
#[test]
fn assign_tie_breaks_lowest_worker_id() {
let backlog = Backlog { tasks: vec![posted(1, Role::Coder, 10, 0, 3)] };
let workers = vec![worker(9, Role::Coder, 5), worker(4, Role::Coder, 5)];
assert_eq!(assign_next_task(&backlog, &workers).unwrap().worker_id, 4);
}
#[test]
fn assign_none_when_no_available_matching_worker() {
let backlog = Backlog { tasks: vec![posted(1, Role::Coder, 10, 0, 3)] };
assert_eq!(assign_next_task(&backlog, &[worker(1, Role::Reviewer, 9)]), None);
let busy = WorkerState { available: false, ..worker(1, Role::Coder, 9) };
assert_eq!(assign_next_task(&backlog, &[busy]), None);
assert_eq!(assign_next_task(&backlog, &[]), None);
let planned = Backlog { tasks: vec![task(1, Role::Coder, 10, 0, 3)] };
assert_eq!(assign_next_task(&planned, &[worker(1, Role::Coder, 9)]), None);
}
#[test]
fn evaluate_accepts_at_or_above_bar_rejects_below() {
let crit = Criteria { min_quality: 3 };
assert_eq!(evaluate_result(&sub(3), &crit), AcceptDecision::Accept { rating: 3 });
assert_eq!(evaluate_result(&sub(5), &crit), AcceptDecision::Accept { rating: 5 });
assert_eq!(evaluate_result(&sub(2), &crit), AcceptDecision::Reject { rating: 2 });
assert!(evaluate_result(&sub(3), &crit).is_accept());
assert_eq!(evaluate_result(&sub(2), &crit).rating(), 2);
}
#[test]
fn evaluate_clamps_rating_and_auto_rejects_hallucination() {
let crit = Criteria { min_quality: 3 };
assert_eq!(evaluate_result(&sub(9), &crit), AcceptDecision::Accept { rating: 5 });
assert_eq!(evaluate_result(&sub(0), &crit), AcceptDecision::Reject { rating: 1 });
let halluc = Submission { quality: 5, claims_impossible: true };
assert_eq!(evaluate_result(&halluc, &crit), AcceptDecision::Reject { rating: 1 });
}
#[test]
fn payout_clamps_to_treasury() {
let t = task(1, Role::Coder, 100, 0, 3);
assert_eq!(compute_payout(&t, 250), 100); assert_eq!(compute_payout(&t, 100), 100); assert_eq!(compute_payout(&t, 40), 40); assert_eq!(compute_payout(&t, 0), 0); }
#[test]
fn deliver_only_transitions_assigned_tasks() {
let mut state = State {
backlog: Backlog {
tasks: vec![Task {
stage: Stage::Assigned { worker_id: 7 },
..task(1, Role::Coder, 50, 0, 3)
}],
},
workers: vec![],
treasury: 0,
};
assert!(state.deliver(1, sub(4)));
assert_eq!(
state.backlog.tasks[0].stage,
Stage::Submitted { worker_id: 7, submission: sub(4) }
);
assert!(!state.deliver(1, sub(5)));
assert!(!state.deliver(99, sub(5)));
}
#[test]
fn step_posts_then_assigns_then_idles_on_undelivered_work() {
let state = State {
backlog: Backlog { tasks: vec![task(1, Role::Coder, 50, 0, 3)] },
workers: vec![worker(7, Role::Coder, 2)],
treasury: 1_000,
};
let (s1, a1) = step(&state);
assert_eq!(a1, vec![Action::PostBounty { task_id: 1, reward: 50 }]);
assert_eq!(s1.backlog.tasks[0].stage, Stage::Posted);
let (s2, a2) = step(&s1);
assert_eq!(a2, vec![Action::AssignTask { task_id: 1, worker_id: 7 }]);
assert_eq!(s2.backlog.tasks[0].stage, Stage::Assigned { worker_id: 7 });
assert!(!s2.workers[0].available);
let (s3, a3) = step(&s2);
assert!(a3.is_empty());
assert_eq!(s3.backlog.tasks[0].stage, Stage::Assigned { worker_id: 7 });
}
#[test]
fn full_accept_cycle_pays_and_attests() {
let mut state = State {
backlog: Backlog { tasks: vec![task(1, Role::Coder, 30, 0, 3)] },
workers: vec![worker(7, Role::Coder, 2)],
treasury: 100,
};
let (s, _) = step(&state);
state = s; let (s, _) = step(&state);
state = s; assert!(state.deliver(1, sub(5))); let (s, acts) = step(&state);
state = s; assert_eq!(
acts,
vec![
Action::AcceptResult { task_id: 1, worker_id: 7 },
Action::Payout { task_id: 1, worker_id: 7, amount: 30 },
Action::Attest { subject_id: 7, rating: 5, work_ref: 1 },
]
);
assert_eq!(state.backlog.tasks[0].stage, Stage::Accepted { worker_id: 7, rating: 5, paid: 30 });
assert_eq!(state.treasury, 70);
assert!(state.workers[0].available);
assert_eq!(state.workers[0].reputation, 3); assert!(step(&state).1.is_empty());
}
#[test]
fn reject_cycle_attests_low_and_pays_nothing() {
let mut state = State {
backlog: Backlog { tasks: vec![task(1, Role::Coder, 30, 0, 4)] },
workers: vec![worker(7, Role::Coder, 2)],
treasury: 100,
};
let (s, _) = step(&state);
state = s; let (s, _) = step(&state);
state = s; assert!(state.deliver(1, sub(2))); let (s, acts) = step(&state);
state = s;
assert_eq!(
acts,
vec![
Action::RejectResult { task_id: 1, worker_id: 7 },
Action::Attest { subject_id: 7, rating: 2, work_ref: 1 },
]
);
assert_eq!(state.backlog.tasks[0].stage, Stage::Rejected { worker_id: 7, rating: 2 });
assert_eq!(state.treasury, 100); assert!(state.workers[0].available); assert_eq!(state.workers[0].reputation, 2); }
#[test]
fn accept_cycle_clamps_payout_to_treasury() {
let mut state = State {
backlog: Backlog { tasks: vec![task(1, Role::Coder, 100, 0, 3)] },
workers: vec![worker(7, Role::Coder, 2)],
treasury: 40, };
let (s, _) = step(&state);
state = s;
let (s, _) = step(&state);
state = s;
assert!(state.deliver(1, sub(5)));
let (s, acts) = step(&state);
state = s;
assert!(acts.contains(&Action::Payout { task_id: 1, worker_id: 7, amount: 40 }));
assert_eq!(state.backlog.tasks[0].stage, Stage::Accepted { worker_id: 7, rating: 5, paid: 40 });
assert_eq!(state.treasury, 0); }
#[test]
fn multi_step_run_drives_two_tasks_to_terminal() {
let mut state = State {
backlog: Backlog {
tasks: vec![
task(1, Role::Coder, 30, 0, 3), task(2, Role::Reviewer, 20, 0, 3), ],
},
workers: vec![worker(7, Role::Coder, 1), worker(8, Role::Reviewer, 1)],
treasury: 100,
};
let mut log: Vec<Action> = Vec::new();
for _ in 0..50 {
let assigned: Vec<u64> = state
.backlog
.tasks
.iter()
.filter(|t| matches!(t.stage, Stage::Assigned { .. }))
.map(|t| t.id)
.collect();
for id in assigned {
state.deliver(id, sub(if id == 1 { 5 } else { 1 }));
}
let (s, acts) = step(&state);
state = s;
if acts.is_empty() {
break;
}
log.extend(acts);
}
assert!(matches!(state.backlog.tasks[0].stage, Stage::Accepted { paid: 30, .. }));
assert!(matches!(state.backlog.tasks[1].stage, Stage::Rejected { .. }));
assert_eq!(state.treasury, 70);
assert!(log.iter().any(|a| matches!(a, Action::PostBounty { task_id: 1, .. })));
assert!(log.iter().any(|a| matches!(a, Action::PostBounty { task_id: 2, .. })));
assert!(log.contains(&Action::Payout { task_id: 1, worker_id: 7, amount: 30 }));
assert!(log.iter().any(|a| matches!(a, Action::Attest { subject_id: 7, rating: 5, work_ref: 1 })));
assert!(log.iter().any(|a| matches!(a, Action::Attest { subject_id: 8, work_ref: 2, .. })));
assert!(state.workers.iter().all(|w| w.available));
assert_eq!(state.workers[0].reputation, 2); assert_eq!(state.workers[1].reputation, 1); }
}