Project::suggest_assignments#

Proposes resources for requirement slots of a Project. It fills empty slots, repairs broken ones and rebalances valid ones, scoring each step like evaluate. The project is never modified: the proposal comes back as a change set.

Signature#

pub fn suggest_assignments(
    &self,
    slots: &[Slot],
    options: &SuggestOptions,
) -> Result<SuggestResult, Error>

Takes &self. Synchronous and CPU-only.

Parameters#

Name Type Description
slots &[Slot] The slots to fill or rebalance. To build them from skills and requirements, use plen_assign::slots. To build your own, read the state with roster.
options &SuggestOptions Targets, fixed assignments, penalty, passes and leveling stage

Behavior#

  • Greedy: slots are visited one at a time, and later slots build on earlier picks.
  • It never removes an assignment. It adds one per filled slot and changes the resource of a repaired or rebalanced one.
  • Commit the proposal with apply(&result.changes), then solve(None), or level with the same leveling options when they were set.

Returns#

A SuggestResult with the proposed changes and a step log.

Errors#

Error When
Error::Project(ProjectError::NoBaseline) The project is unsolved, including after apply
Error::Project(ProjectError::StaleBaseline) options.baseline_id doesn't name the current baseline
Error::Input Invalid options (new_resource_penalty, passes, targets[i], fixed[i], level); a cyclic accepted state (path: None); an invalid slot (paths under slots[i])

Options are checked before slots. The accepted values are on the SuggestOptions and Slot pages.

Example#

Fill an open position on task C, then commit the proposal.

use plen_core::{
    AssignmentData, DateTime, Project, ProjectData, ResourceData, Slot, SolveOptions,
    SuggestOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let start = DateTime::from_ymd_hm(2026, 8, 10, 0, 0).ok_or("invalid date")?;
    let task = |id: &str| TaskData {
        start_date: Some(start),
        duration: Some(2.0),
        ..TaskData::new(id)
    };
    let resource = |id: &str| ResourceData { id: id.into(), capacity: None, calendar_id: None };
    let mut data = ProjectData {
        tasks: vec![task("A"), task("C")],
        resources: vec![resource("dev1"), resource("dev2")],
        assignments: vec![AssignmentData {
            id: Some("aA".into()),
            task: "A".into(),
            resource: "dev1".into(),
            value: 1.0,
            offset: None,
            duration: None,
        }],
        ..Default::default()
    };
    data.config.use_calendars = false;

    let mut project = Project::default();
    project.solve(Some(data), &SolveOptions::default())?;

    // Task C needs one person: dev1 or dev2.
    let slots = vec![Slot::new("sC", "C", vec!["dev1".into(), "dev2".into()])];
    let result = project.suggest_assignments(&slots, &SuggestOptions::default())?;
    for step in &result.steps {
        println!("{} -> {}", step.slot, step.to_resource); // sC -> dev2
    }

    let report = project.apply(&result.changes)?;
    if report.rejected.is_empty() {
        project.solve(None, &SolveOptions::default())?;
    }
    Ok(())
}