Say what the task needs, not who
Nobody is assigned yet. Each task says what it needs: “Build API” wants two people with backend 2 or more, “Release” one with devops 2 and one with any QA. Press Find team and the whole plan is staffed at once. Leave it pressed and change a grade in the skills matrix or a requirement: the plan is staffed again immediately.
This builds on the auto-assign demo, where every person may take every task. Here the question “who may take it” has an answer of its own.
A requirement is a query over a pool
The pool lists people and their grades. A requirement names a task, a number of people and the conditions each of them must meet: ≥, ≤ or = on a skill, all of them at once. Neither is stored in the project: they are arguments, so the demo keeps them in its own state and passes them again on every call.
// Who can do what: grades 0–3. A skill a person does not list counts as 0.
const pool = { workers: [
{ resource: "ana", skills: [{ id: "design", value: 3 }, { id: "frontend", value: 1 }] },
{ resource: "ben", skills: [{ id: "backend", value: 3 }, { id: "frontend", value: 2 }] },
{ resource: "dee", skills: [{ id: "backend", value: 2 }, { id: "devops", value: 2 }] },
… // Cy, Eli
{ resource: "fay", skills: [{ id: "backend", value: 1 }, { id: "qa", value: 2 }] },
{ resource: "gus", skills: [{ id: "devops", value: 3 }] },
] };
// What each task needs, not who. Every condition of a requirement must hold.
const requirements = [
{ id: "api", task: 5, matches: [{ skill: "backend", op: "at_least", value: 2 }], count: 2 },
{ id: "ops", task: 10, matches: [{ skill: "devops", op: "at_least", value: 2 }] },
{ id: "rqa", task: 10, matches: [{ skill: "qa" }] }, // defaults: at_least 1, count 1
…
];Slots: the answers
slots runs the query in the engine and returns one slot per person needed, with the people who match. The demo does not repeat the matching: the “Eligible now” line in the requirement editor is the slot's allowed list as the engine returned it. A new assignment takes its slot's id, so after a commit the next slots call finds who holds which slot, also when that person no longer matches.
// The engine answers the query: one slot per person needed, the people who match, in pool order.
const slots = project.slots(pool, requirements);
// [{ id: "api#1", task: 5, allowed: ["ben", "dee"], value: 1 },
// { id: "api#2", task: 5, allowed: ["ben", "dee"], value: 1 }, …]
// After a commit the assignments carry the slot ids, so the next call binds them by itself.
// With Build API raised to backend ≥ 3:
// { id: "api#2", task: 5, allowed: ["ben"], value: 1, assignment: "api#2" } ← Dee holds it, but no longer matchesThe engine chooses among the answers
suggestAssignments sees only slots: a task and a list of people. It fills empty slots, repairs slots whose person stopped matching, and moves people when the plan gets better, scoring every pick on the leveled plan, as the auto-assign demo does. The scheduler and the leveler never see a skill. Find team commits right away: apply the changes, then level with the options that scored them.
const run = project.suggestAssignments(slots, {
targets: ["empty", "broken", "valid"], // fill, repair people who stopped matching, rebalance the rest
newResourcePenalty: 0, // the Prefer slider
level: { allowSplits: true }, // score every pick on the leveled plan
clearFirst: true, // ... from zero, not on top of the last leveling
});
project.apply(run.changes); // Find team commits at once
project.level({ allowSplits: true });When nobody matches
Raise “Build API” to backend 3 with Find team pressed. Dee's slot is now broken, but Ben, the only other match, is already on the task. The engine keeps Dee and reports the slot asinvalid: the chip turns red with the reason. Raise Fay's backend to 3 and the next run repairs the slot to Fay. Lower the requirement back instead, and Dee matches again: the slot is valid with no change. The reasons in the log (“nobody matches”, “everyone else eligible is already on this task”) are worked out by the demo: the engine drops candidates silently. Press why? on a step to compare its person with everyone else who could take the slot, on the plan as it is now.
run.steps = [
{ slot: "brief#1", task: 1, pass: 0, fromResource: null, toResource: "ana", cost: { before: 0, after: 0 } },
{ slot: "wire#1", task: 2, pass: 0, fromResource: null, toResource: "ana", cost: { before: 0, after: 0 } },
…
{ slot: "api#1", task: 5, pass: 0, fromResource: null, toResource: "ben", cost: { before: 0, after: 0 } },
{ slot: "api#2", task: 5, pass: 0, fromResource: null, toResource: "dee", cost: { before: 0, after: 0 } },
…
];
// Build API raised to backend ≥ 3: Ben is the only match and already holds api#1.
run.steps = [];
run.invalid = ["api#2"]; // Dee keeps the slot: the engine never removes an assignment
// Fay's backend raised to 3: a repair, pass 0, from the person who no longer matches.
run.steps = [{ slot: "api#2", task: 5, pass: 0, fromResource: "dee", toResource: "fay", … }];
run.invalid = [];