SkillLayer.Slots#

Turns a Pool of workers and a list of Requirements into the Slots that Project.SuggestAssignments takes. Existing active assignments are bound to the slots, so repeated runs stay stable after a commit. Part of the Plen.Assign package (namespace Plen.Assign).

dotnet add package Plen.Assign

Signature#

public static List<Slot> Slots(Project project, Pool pool, IReadOnlyList<Requirement> requirements)

Synchronous and CPU-only. Reads the project's accepted state through Project.Roster and never modifies the project.

Parameters#

Name Type Description
project Project A solved project with no unsolved edits. Tasks, resources and assignments are checked against its accepted state.
pool Pool Workers that may be assigned. Pool order sets the order of each slot's Allowed.
requirements IReadOnlyList<Requirement> What each task needs. An empty list returns an empty list.

Returns#

A new List<Slot>: Count slots for each requirement, in requirement order, then instance order. Slot properties:

Property Value
Id "<requirement id>#<n>", n = 1..Count (for example be#1, be#2)
Task The requirement's Task
Allowed Pool workers for which every SkillMatch of the requirement holds, in pool order. An empty Matches list allows every worker.
Value The requirement's Units
Assignment The id of the existing active assignment bound to this slot, or null

On the next call, an assignment created from a slot binds back to that slot by id. That's why a requirement id must stay on its task; see Requirement.Id.

SuggestAssignments takes the result as is.

Binding#

The task's existing active assignments are bound per task, across all requirements on that task, in three steps:

  1. Slot id. An assignment whose id is one of the requirement's slot ids binds to that slot, even when its resource isn't in Allowed. The slot is then broken (AssignTarget.Broken). This step runs for every requirement of the task before any explicit list.
  2. Explicit list. Requirement.Assignments: a non-null list replaces automatic binding for that requirement. A listed slot-id assignment keeps its slot, the other entries take the remaining slots in order, and leftover slots stay empty. An empty list leaves every slot empty.
  3. Matching. A deterministic maximum bipartite matching binds the remaining assignments to the empty slots of requirements without a list. An assignment can take a slot only when its resource is in the slot's Allowed. Input order decides between equal matchings, and an earlier requirement can't take the only worker a later one fits.

Assignments left unbound stay on the task without a slot.

Errors#

Checks run in this order, and the first failure throws: the project state, then the pool, then each requirement's fields in input order, then binding.

Error When
ProjectException NoBaseline The project is unsolved or has unsolved edits (after Apply).
InputException (Kind = Validation) The pool or a requirement is invalid. Path names the item; see the table below.
ArgumentNullException project, pool or requirements is null.

InputException.Path values:

Path When
pool.workers[i].resource Unknown resource, or the same resource listed as two workers
pool.workers[i].skills[j].id The same skill listed twice on one worker
pool.workers[i].skills[j].value Not finite
requirements[i].id Duplicate requirement id, or one of its slot ids is an active assignment on another task
requirements[i].task Unknown task, or not a scheduled leaf task
requirements[i].count 0
requirements[i].units Not finite or <= 0
requirements[i].matches[j].value Not finite
requirements[i].assignments More entries than Count, or an assignment with one of this requirement's slot ids left out of the list
requirements[i].assignments[k] Unknown or inactive assignment, an assignment on another task, or one bound twice (the message names the other requirement when it already holds it)

Both exceptions derive from PlenException, so one catch (PlenException) covers this call and the rest of the API. The method takes no baseline id, so it never throws StaleBaseline.

Example#

using Plen;
using Plen.Assign;

var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var data = new ProjectData
{
    Tasks = [new TaskData("B") { StartDate = monday, Duration = 2 }],
    Resources = [new ResourceData { Id = "ann" }, new ResourceData { Id = "bob" }],
};
var project = new Project();
project.Solve(data, new SolveOptions());

var pool = new Pool
{
    Workers =
    [
        new Worker { Resource = "ann", Skills = [new Skill { Id = "backend", Value = 1 }] },
        new Worker { Resource = "bob", Skills = [new Skill { Id = "backend", Value = 2 }] },
    ],
};
List<Requirement> requirements = [new("be", "B", [SkillMatch.AtLeast("backend", 2)])];

var slots = SkillLayer.Slots(project, pool, requirements);
// One slot "be#1" on task "B", Allowed = [bob]

var proposal = project.SuggestAssignments(slots, new SuggestOptions());
project.Apply(proposal.Changes);
project.Solve(null, new SolveOptions());