fix: Ensure all eligible persons are assigned by implementing weighted priority logic in auto assignment#4932
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds centralized assignment conflict constants and restructures assignment path constants into section-driven mappings; implements a data-driven, two‑round autofill orchestration with selection, statistics, dataView-aware history persistence, language‑group handling, and a date-specific person blocking helper. (31 words) Changes
Sequence Diagram(s)sequenceDiagram
participant Hook as "useScheduleAutofill"
participant Orchestrator as "handleDynamicAssignmentAutofill"
participant Stats as "assignments_with_stats"
participant Selector as "assignment_selection"
participant Store as "schedules service"
Hook->>Orchestrator: start,end,languageGroups,meeting
Orchestrator->>Stats: getAssignmentsWithStats(persons,weeks,schedules,settings,languageGroups)
Stats-->>Orchestrator: frequencies,eligibleUIDs,benchmarks
Orchestrator->>Orchestrator: build tasks per-week/view
Orchestrator->>Selector: sortCandidatesMultiLevel(candidates,task,history,metrics)
Selector-->>Orchestrator: orderedCandidates
Orchestrator->>Selector: hasAssignmentConflict(candidate,week,code,history,dataView)
Selector-->>Orchestrator: conflictDecision
Orchestrator->>Store: persist assignments (include dataView)
Store-->>Orchestrator: save result / updated history
Orchestrator-->>Hook: completion / error
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Fix all issues with AI agents
In `@src/constants/index.ts`:
- Line 736: The Week.SPECIAL_TALK mapping currently constructs the Set
incorrectly using new Set(...ASSIGNMENT_PATH_KEYS) which spreads the array into
characters; replace it with new Set(ASSIGNMENT_PATH_KEYS) (i.e., remove the
spread operator) so the Set is constructed from the ASSIGNMENT_PATH_KEYS
iterable as intended; update the entry where Week.SPECIAL_TALK is paired with
new Set(...ASSIGNMENT_PATH_KEYS).
In `@src/features/meetings/week_range_selector/useWeekRangeSelector.tsx`:
- Around line 90-93: In useWeekRangeSelector, remove the hardcoded override that
sets startDate = '2024/11/01' inside the startWeekOptions block so the value
uses the computed result from getFirstWeekPreviousMonth() (formatted via
formatDate); update any related logic that assumed the override to rely on the
dynamic startDate instead (refer to startWeekOptions, startDate,
getFirstWeekPreviousMonth, and formatDate to locate the code).
In `@src/services/app/assignment_selection.ts`:
- Around line 139-147: The filter callback for computing pairings can throw when
assignment.key is undefined; update the check in the history.filter callback
(used to compute pairings) to guard against undefined by using optional chaining
or a null-safe test before calling includes on assignment.key (reference:
assignment.key, AssignmentHistoryType, pairings, history.filter). Ensure the
condition becomes something like a safe check (e.g.,
assignment.key?.includes('_Assistant_') or coercing to a string) so it returns
false when key is undefined and preserves the other checks for
entry.assignment.person and the student match.
- Around line 234-237: Protect against division by zero before computing
recoveryProgress: in assignment_selection.ts, where assistantWeightedWait is
divided by assignmentCodeThreshold (variables assistantWeightedWait,
assistantSafeDist, weightingFactor, recoveryProgress, assignmentCodeThreshold),
add a guard so if assignmentCodeThreshold is 0 (or <= 0) then set
recoveryProgress to 1 (matching the later capping behavior) instead of
performing the division; otherwise compute recoveryProgress =
assistantWeightedWait / assignmentCodeThreshold. This prevents Infinity when
assistantThreshold can be zero from
handleDynamicAssignmentAutofill/assistantThreshold.
In `@src/services/app/assignments_with_stats.ts`:
- Around line 551-554: The AssignmentMetrics.eligibleUIDS property expects a
Set<string> but eligiblePersonsView?.get(code) can be undefined; update the
statsForView.set call (in the block that constructs AssignmentMetrics) to supply
a default empty Set when eligiblePersonsView?.get(code) is missing (e.g.,
replace eligiblePersonsView?.get(code) with a fallback to new Set()). Ensure you
reference the statsForView.set invocation and the eligiblePersonsView lookup so
callers always receive a Set<string>.
- Around line 613-624: The function calculateWeightingFactor returns early with
no value when personScore is undefined or 0, violating its declared number
return type; change the early return to an explicit numeric default (e.g.,
return 1 as a neutral weighting) and ensure any callers expect a number; update
the check in calculateWeightingFactor to return 1 (or another agreed-upon
default numeric constant) when personScore is undefined or zero, and consider
adjusting the parameter type if undefined is valid.
- Around line 41-43: The null-check currently uses
settings.cong_settings.language_groups.enabled which can throw if
language_groups or cong_settings is undefined; update the conditional in
assignments_with_stats.ts that returns relevantViews to safely check the path
using optional chaining and the actual SettingsType shape (check
settings.cong_settings?.language_groups?.enabled?.value) so the guard reads the
boolean value without throwing; ensure you reference the same symbols: settings,
cong_settings, language_groups, enabled, value, and keep the early return of
relevantViews when the feature is not enabled.
In `@src/services/app/autofill.ts`:
- Around line 266-271: The code calls
WEEK_TYPE_ASSIGNMENT_PATH_KEYS.get(Week.CO_VISIT).has(key) without checking for
undefined which can throw; update the COWeekMain branch (where COWeekMain is
derived from mainWeekType === Week.CO_VISIT) to first retrieve the set via const
coSet = WEEK_TYPE_ASSIGNMENT_PATH_KEYS.get(Week.CO_VISIT) and guard against
undefined (e.g. if (!coSet) return relevantAssignmentKeys; or use coSet.has(key)
only when coSet exists), then filter relevantAssignmentKeys using coSet.has(key)
so .has is never called on undefined.
- Around line 729-762: The comparator passed to tasks.sort can return undefined
when all checks tie (diff === 0); update the comparator used in the return
tasks.sort(...) block to always return a number by adding an explicit final
return 0. Locate the comparator that references fixedAssignments,
linkedAssignments, AssignmentCode.MM_AssistantOnly, a.assignmentKey (e.g.,
'WM_Speaker_Part2'), a.dataView, and a.sortIndex and ensure after the diff check
you return 0 so equal items produce a deterministic sort order.
- Around line 95-104: meetingDay can be undefined if no record matches dataView,
which then gets passed to addDays; update the logic that computes meetingDay
(the ternary choosing from settings.cong_settings.midweek_meeting or
weekend_meeting using meeting_type, dataView) to provide a safe default (e.g., 0
or a specific weekday) when the find returns undefined before calling
addDays(weekOf, meetingDay), so addDays always receives a valid number.
- Around line 336-346: The function declares a return type of { code:
AssignmentCode | undefined; elderOnly: boolean } but currently uses bare
`return;` in the early-exit branches (after `const partMatch =
key.match(/AYFPart(\d+)/)` and after `const ayfSourceData =
source.midweek_meeting[\`ayf_part${partIndex}\`]`), which yields undefined;
replace those bare returns with an explicit object that matches the signature
(for example `return { code: undefined, elderOnly: false }`) so callers always
receive the expected shape from this function.
- Around line 949-956: The block checking AssignmentCode.MM_AssistantOnly
accesses studentPerson.person_uid without ensuring studentPerson is defined;
update the conditional to guard against undefined studentPerson (e.g., require
studentPerson truthiness before accessing its person_uid) or merge this logic
with the earlier studentPerson check used with isValidAssistantForStudent so you
never dereference studentPerson when it may be undefined; adjust the condition
that references task.code, studentPerson.person_uid, and p.person_uid
accordingly.
In `@src/services/app/persons.ts`:
- Around line 913-931: Rename the incorrectly named function hanldeIsPersonAway
to handleIsPersonAway and update all references/exports to that new identifier;
inside the function either keep the current logic or replace its body with a
call to the existing personIsAway(person, targetDate) to avoid duplication
(ensure you preserve return type boolean and imports/types for PersonType and
formatDate if you inline logic). Also search the codebase for any usages of
hanldeIsPersonAway and update them to handleIsPersonAway so consumers continue
to work.
🧹 Nitpick comments (9)
src/definition/assignment.ts (2)
35-40: Translate comments to English for consistency.The inline comments are in German. For codebase consistency and maintainability, please translate them to English.
✏️ Proposed translation
const getAssignmentCodes = (prefix: string): AssignmentCode[] => { return Object.values(AssignmentCode) - .filter((val) => typeof val === 'number') // Nur Zahlenwerte - .filter((val) => AssignmentCode[val as number].startsWith(prefix)) // Prüfen, ob der Key mit Prefix startet + .filter((val) => typeof val === 'number') // Only numeric values + .filter((val) => AssignmentCode[val as number].startsWith(prefix)) // Check if key starts with prefix .map((val) => val as AssignmentCode); };
3-4: Clarify deprecation status.The comments "Deprecated?" with question marks are ambiguous. If these values are deprecated, add proper JSDoc
@deprecatedannotations with migration guidance. If the deprecation status is uncertain, resolve it before merging.src/services/app/schedules.ts (3)
867-876: Remove commented-out code and translate comments.
- Remove the commented-out code block (lines 868-870) - it clutters the codebase
- Translate the German comment on line 867 to English
✏️ Proposed cleanup
- //ist der eingriff hier ein problem - /* if (assignment.includes('MM_Chairman')) { - history.assignment.code = AssignmentCode.MM_Chairman; - } */ + // Differentiate chairman codes: main hall vs auxiliary classroom if (assignment.includes('MM_Chairman_A')) { history.assignment.code = AssignmentCode.MM_Chairman; } if (assignment.includes('MM_Chairman_B')) { history.assignment.code = AssignmentCode.MM_AuxiliaryCounselor; }
2000-2009: Translate German comments to English.The comments explaining the dataView guard logic are in German. Please translate for codebase consistency.
✏️ Proposed translation
const dataView = store.get(userDataViewState); // remove record from history const previousIndex = history.findIndex( (record) => record.weekOf === schedule.weekOf && record.assignment.key === assignment && - // --- WICHTIGE ÄNDERUNG START --- - // Wir löschen nur, wenn der Eintrag auch zu unserem aktuellen View gehört! + // Only delete if entry belongs to current data view record.assignment.dataView === dataView - // --- WICHTIGE ÄNDERUNG ENDE --- );
2049-2058: Translate German comments to English.The comments for the optional dataView parameter are in German.
✏️ Proposed translation
export const schedulesAutofillSaveAssignment = ({ assignment, schedule, value, history, - dataView: dataViewOverride, // <--- NEU: Optionaler Parameter + dataView: dataViewOverride, // Optional parameter to override store value }: { schedule: SchedWeekType; assignment: AssignmentFieldType; value: PersonType; history: AssignmentHistoryType[]; - dataView?: string; // <--- NEU: Typ-Definition + dataView?: string; }) => { - // Wenn ein Override übergeben wurde, nimm den. Sonst hol aus dem Store. + // Use override if provided, otherwise get from store const dataView = dataViewOverride || store.get(userDataViewState);src/services/app/assignment_selection.ts (1)
534-541: Redundant DataView check in student task validation.The
entry.assignment.dataView === currentDataViewcheck at line 537 is redundant. At this point in the code,tasksInWeekonly contains entries that passed the dataView check (lines 504-508 returntrueearly for cross-dataView conflicts), so all remaining entries are guaranteed to be incurrentDataView.♻️ Proposed simplification
if (STUDENT_TASK_CODES.includes(currentTaskCode)) { const hasStudentPart = tasksInWeek.some( - (entry) => - entry.assignment.dataView === currentDataView && - STUDENT_TASK_CODES.includes(entry.assignment.code) + (entry) => STUDENT_TASK_CODES.includes(entry.assignment.code) ); if (hasStudentPart) return true; }src/services/app/autofill.ts (1)
764-815: Remove commented-out alternative implementation.This large block of commented-out code (alternative
getSortedTaskswith chronological ordering) should be removed before merging. If this approach might be needed in the future, consider documenting it in an ADR or issue rather than leaving it in the codebase.src/services/app/assignments_with_stats.ts (2)
592-592: Linter warning: forEach callback returns a value.The
Set.add()call returns the Set, which becomes the implicit return value of the arrow function. While this doesn't affect behavior (forEach ignores return values), wrapping in braces silences the linter.♻️ Proposed fix
- metrics.eligibleUIDS?.forEach((uid) => assignablePersonsSet.add(uid)); + metrics.eligibleUIDS?.forEach((uid) => { assignablePersonsSet.add(uid); });
182-190: Remove non-English comments.Several comments in this file are in German (e.g., "KORREKTUR", "Prüfung auf .size > 0", "Hinweis"). For consistency and maintainability in an English codebase, these should be translated.
|
@nobodyzero1: I just started playing with it a little bit, and I hope it’s not only me, but it seems like this introduces more repetition than what the current version does. Just tested with one month, and two persons get repeated twice in a month, although there are still a lot of available persons. |
|
Thanks @rhahao for testing this out! That is very interesting because I ran extensive simulations locally before submitting the PR and couldn't reproduce that repetition behavior. To verify the stability, I simulated a period of 1.5 years across three different data scenarios. I have attached the results of these simulations as Excel exports below. In these datasets, the distribution remains balanced, and I didn't observe the repetition issue. 📂 Simulations: Potential Cause: Long-Term Fairness vs. Short-Term Availability This might be the most likely explanation for what you are seeing. The algorithm distinguishes between available (free time) and prioritized (due for assignment). The only reason I can think of right now is the following: Could it be that the brothers receiving double assignments are those who are qualified for almost all types of tasks? And conversely, are the 'available' brothers only qualified for a limited number of task types? The first sorting criterion checks if the waiting time for any assignment is generally fulfilled (Global Tier). For brothers who can do everything, the math often dictates that they need an assignment almost every meeting on average. If they have also waited longer for this specific task than the others, they will be prioritized. Debugging the issue To find out exactly whether this is the intended "Catch-Up" logic or a bug, we need to inspect the scoring weights at the moment of assignment. Could you temporarily add to the end of sortCandidatesMultiLevel function the code from the file? |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/services/app/assignment_selection.ts`:
- Around line 473-493: The hasAssignmentConflict function assumes
currentTaskCode maps to an AssignmentCode string that begins with 'MM_' or 'WM_'
but has no validation; update hasAssignmentConflict to validate the mapped code
before slicing: retrieve const codeStr = AssignmentCode[currentTaskCode]; if
codeStr is falsy or does not start with 'MM_' or 'WM_' then either return false
(no conflict) or throw a clear error depending on caller expectations, and add a
short comment explaining the enforced precondition; reference the existing
symbols currentTaskCode, AssignmentCode, targetPrefix and the function
hasAssignmentConflict when making the change.
In `@src/services/app/autofill.ts`:
- Around line 358-367: The function early-returns with a bare `return;` when
`!isValidAssistantPart` — change this to `return undefined;` to match the
declared return type `{ code: AssignmentCode; elderOnly: boolean } | undefined`
and to be consistent with the other returns in this function (see the other
`return undefined;` uses and the `isValidAssistantPart`/`STUDENT_ASSIGNMENT`
logic).
🧹 Nitpick comments (2)
src/constants/index.ts (1)
1006-1023: Consider extractingALL_ASSIGNMENTCODESas an exported constant.The
ALL_ASSIGNMENTCODESset is currently a module-level constant but not exported. If other modules need to reference all valid assignment codes, this could be useful as a public export.♻️ Optional: Export ALL_ASSIGNMENTCODES
-const ALL_ASSIGNMENTCODES = new Set( +export const ALL_ASSIGNMENTCODES = new Set( Object.values(AssignmentCode).filter( (v): v is AssignmentCode => typeof v === 'number' ) );src/services/app/assignment_selection.ts (1)
363-379: Non-deterministic sorting may complicate debugging.The
Math.random()tie-breaker at line 371 introduces non-determinism that could make it difficult to reproduce specific autofill results during debugging. Consider using a seeded PRNG or a deterministic fallback (e.g., person_uid comparison) for reproducibility when needed.💡 Optional: Deterministic fallback for debugging
const randomFactor = Math.random(); + // Alternative for debugging: use person_uid hash for deterministic ordering + // const randomFactor = hashCode(p.person_uid) / Number.MAX_SAFE_INTEGER; metaCache.set(p.person_uid, {
|
@rhahao |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/services/app/autofill.ts (1)
685-698: Consider using an options object to reduce parameter count.The function accepts 12 parameters, which exceeds typical complexity thresholds and makes call sites harder to read and maintain. Grouping related parameters into a single options object would improve readability.
♻️ Suggested approach
// Define an options type type GetTasksArrayOptions = { weeksList: SchedWeekType[]; sources: SourceWeekType[]; ignoredKeys: AssignmentPathKey[]; dataView: DataViewKey; lang: string; sourceLocale: string; settings: SettingsType; meeting_type: MeetingType; fullHistory: AssignmentHistoryType[]; persons: PersonType[]; eligibilityMapView: Map<AssignmentCode, Set<string>>; checkAssignmentsSettingsResult: AssignmentSettingsResult; }; export const getTasksArray = (options: GetTasksArrayOptions): AssignmentTask[] => { const { weeksList, sources, ignoredKeys, /* ... */ } = options; // ... };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/app/autofill.ts` around lines 685 - 698, The getTasksArray function currently takes many parameters; refactor it to accept a single options object by creating a GetTasksArrayOptions type that bundles weeksList, sources, ignoredKeys, dataView, lang, sourceLocale, settings, meeting_type, fullHistory, persons, eligibilityMapView, and checkAssignmentsSettingsResult, then change the getTasksArray signature to getTasksArray(options: GetTasksArrayOptions) and destructure those fields inside the function; update all call sites to pass a single options object (or spread an existing object) and ensure TS types/imports for GetTasksArrayOptions and any renamed symbols are updated accordingly.src/services/app/assignments_with_stats.ts (1)
732-862: Acknowledge high cognitive complexity incalculateOpportunityScore.Static analysis flags cognitive complexity of 32 (threshold: 15). The complexity is inherent to the opportunity scoring algorithm which handles:
- Fixed assignment blocking via conflict matrix
- Cross-view frequency corrections for multi-group persons
- Fixed person full-frequency vs. pooled-frequency logic
The function is well-documented with clear sections. While refactoring into smaller helpers is possible, the current structure maintains the algorithm's logical flow in one place. Consider extracting the fixed-assignment blocking logic (lines 770-787) and cross-view correction (lines 801-815) into separate helpers if this function grows further.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/app/assignments_with_stats.ts` around lines 732 - 862, The function calculateOpportunityScore has high cognitive complexity; extract the fixed-assignment blocking and cross-view frequency correction into small helpers to reduce complexity: create getBlockedCodesForPerson(personUID, targetDataView, fixedAssignmentsByCode, ASSIGNMENT_CONFLICTS) that returns a Set<number> of blocked codes (replace the block built in the for-loop that references fixedAssignmentsByCode and ASSIGNMENT_CONFLICTS), and create adjustFrequencyForCrossViews(code, targetDataView, person, assignmentsMetrics, currentFreq) that returns the corrected freq (replace the inner assignmentsMetrics.forEach block used when targetDataView === 'main'); call these helpers from calculateOpportunityScore and keep the rest of the logic intact (references: calculateOpportunityScore, viewFixedAssignments, MM_ASSIGNMENT_CODES, WM_ASSIGNMENT_CODES, assignmentsView, assignmentsMetrics).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/services/app/assignments_with_stats.ts`:
- Around line 732-862: The function calculateOpportunityScore has high cognitive
complexity; extract the fixed-assignment blocking and cross-view frequency
correction into small helpers to reduce complexity: create
getBlockedCodesForPerson(personUID, targetDataView, fixedAssignmentsByCode,
ASSIGNMENT_CONFLICTS) that returns a Set<number> of blocked codes (replace the
block built in the for-loop that references fixedAssignmentsByCode and
ASSIGNMENT_CONFLICTS), and create adjustFrequencyForCrossViews(code,
targetDataView, person, assignmentsMetrics, currentFreq) that returns the
corrected freq (replace the inner assignmentsMetrics.forEach block used when
targetDataView === 'main'); call these helpers from calculateOpportunityScore
and keep the rest of the logic intact (references: calculateOpportunityScore,
viewFixedAssignments, MM_ASSIGNMENT_CODES, WM_ASSIGNMENT_CODES, assignmentsView,
assignmentsMetrics).
In `@src/services/app/autofill.ts`:
- Around line 685-698: The getTasksArray function currently takes many
parameters; refactor it to accept a single options object by creating a
GetTasksArrayOptions type that bundles weeksList, sources, ignoredKeys,
dataView, lang, sourceLocale, settings, meeting_type, fullHistory, persons,
eligibilityMapView, and checkAssignmentsSettingsResult, then change the
getTasksArray signature to getTasksArray(options: GetTasksArrayOptions) and
destructure those fields inside the function; update all call sites to pass a
single options object (or spread an existing object) and ensure TS types/imports
for GetTasksArrayOptions and any renamed symbols are updated accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 899c2e8f-0882-47f8-bbc0-6ed25476db9e
📒 Files selected for processing (2)
src/services/app/assignments_with_stats.tssrc/services/app/autofill.ts
|
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
… feature/improved-assignments-distribution
… feature/improved-assignments-distribution
… feature/improved-assignments-distribution
|
|
Hi @rhahao ! To help facilitate the review process, I've put together a comparison between the current scheduling algorithm and the new logic introduced in this PR, simulating a period of 1.5 years. You can find the side-by-side results in the attached Excel file. In the "Tasks count" sheet, you can see that the new logic achieves a much more even distribution of assignments (take a look at "Anton Wolf" or "Zoe Berger" as examples). The "coefficient of variation" sheet evaluates the variance in the time intervals between assignments for each person. For most individuals, the new logic results in a significantly more consistent scheduling cadence, and the outliers are much less extreme (e.g., 0.17 vs. 0.75). Is there anything else I can do to assist with the code review, or anything specific you'd like me to adjust? |



Description
This PR introduces a full 2-round weighted autofill pipeline for meeting assignments. It generates assignment tasks from schedules and source material, filters them through congregation rules and availability checks, and then assigns candidates using fairness-based ranking across persons, data views, and meeting types.
The implementation is mainly split across:
autofill.ts— orchestration, task generation, processing flowassignments_with_stats.ts— assignment statistics, opportunity scoring, weightingassignment_selection.ts— candidate validation, conflict checks, rankingLogic Implementation Details:
High-level flow
At a high level, autofill works in five stages:
1. Data snapshot and preparation
The autofill process starts by reading the current state for:
The logic works on an in-memory snapshot of these structures so that the assignment process is consistent during a single autofill run.
Before task selection begins, the implementation also prepares derived helper structures, for example:
main+ active language groups with meetings)AssignmentCode -> eligible person UIDs)2. Settings-driven assignment rules
The autofill engine derives three important rule groups from meeting settings:
Ignored keys
Some assignment keys are excluded from autofill entirely. This includes cases such as:
Linked assignments
Some tasks depend on another task in the same week. Typical examples are prayers linked to chairman assignments. In those cases, the dependent task is not freely assigned; it inherits the already assigned person from its linked “master” task.
Fixed assignments
Some tasks are permanently assigned by configuration, for example a default Watchtower Study Conductor or an auxiliary class counselor. These tasks are processed with top priority so they immediately lock the relevant person.
3. Statistics and fairness model
A major part of the autofill logic is based on expected assignment opportunity, not just raw historical counts.
Assignment statistics per data view
The statistics engine computes assignment metrics for each relevant data view. For each
AssignmentCode, it calculates:This frequency model combines:
A synthetic
totalview is also built by aggregating all data views. This is later used for benchmark and weighting calculations.Opportunity score per person
For each person and each data view, the system computes a theoretical opportunity score. Conceptually, this answers:
The score is based on:
This produces:
mm_globalScorewm_globalScoreview_globalScoreWeighting factor per person
After that, the system computes a global weighting factor for every person.
The weighting factor compares:
This creates a fairness multiplier:
That multiplier is later used inside candidate ranking.
4. Task generation
The next step is turning schedules into a flat list of actual assignment tasks that still need to be filled.
Each generated task contains:
AssignmentCodeGlobal filtering
The initial assignment key list is filtered by:
MM_vsWM_)_Btasks if there is only one class)Week-specific filtering
For each week, additional filters are applied:
localSpeaker, local speaker parts are removedSkip already assigned tasks
If a task is already assigned in history for the same week and same data view, it is excluded from autofill. This prevents overwriting existing manual assignments.
Resolve assignment code and qualification
For every remaining assignment key, the engine resolves the actual
AssignmentCodeand whether the task is elder-only.This resolution is source-aware and includes special handling for:
For example:
Scarcity-based sort index
Each task gets a
sortIndexbased on the number of currently valid candidates.Fewer valid candidates means:
sortIndexThis is important because the algorithm tries to fill the hardest tasks first.
For dependent task families, scarcity is unified:
WM_Speaker_Part2inherits the scarcity basis ofWM_Speaker_Part1This keeps related tasks close together during processing.
5. Task ordering
Once all tasks are generated, they are sorted into an execution order.
The sort rules are:
This ordering is intentional:
6. Candidate validation
Before a person can be considered for a task, they must pass all validation checks.
The validation pipeline includes:
Base eligibility
The person must be part of the precomputed eligible UID set for the task’s assignment code.
Elder requirement
If the task is marked
elderOnly, the person must satisfy the elder check.Assistant compatibility
If the task involves a student-assistant pairing, the candidate must be valid for that student. The compatibility logic allows:
It also prevents assigning the same person as both student and assistant.
Availability
The person must not be blocked on the task date. This uses the person availability / away-date helper.
Assignment conflicts
The person must not already have a conflicting assignment in the same week.
Conflict detection operates on two levels:
There is also an explicit rule preventing multiple student parts in the same meeting.
Student viability
For student tasks, the engine additionally checks whether at least one valid assistant would exist. If no valid assistant can be found, that student candidate is filtered out.
7. Forced assignments
Before normal ranking is applied, the engine checks whether a task should be forced to a specific person.
There are two sources for this:
Linked assignment resolution
If the task is linked to another task, the system looks up the already assigned person of that master task in the same week and same data view.
Fixed assignment resolution
If settings specify a fixed person for the task, that person is used.
If a forced person exists and still passes validation, that person is returned as the only candidate.
If the forced person is invalid, the engine falls back to normal candidate selection.
8. Candidate ranking
Eligible candidates are ranked using a multi-level fairness model implemented in
sortCandidatesMultiLevel().The ranking uses several metrics:
_AtasksHistorical load calculation
The ranking logic does not simply count raw assignments.
Instead, it calculates a person’s actual load inside a dynamic time window based on nearby past and future assignments.
This gives a more contextual measure of how recently and how frequently someone has already been used.
Tier score
Expected load and actual load are compared through a tier score.
In general:
That score is also multiplied by the person-specific weighting factor from the statistics stage.
9. Two-round processing strategy
The actual autofill assignment is done in two rounds.
Round 1: default strategy
The first round performs the broad initial distribution.
For each task:
The default strategy prioritizes:
This round establishes a stable first-pass distribution and also records how many total assignments each person received.
Round 2: alternative strategy
After Round 1, the algorithm performs a second pass that aims to preserve or improve distribution quotas.
Conceptually, Round 2 uses the total per-person assignment counts from Round 1 as target caps.
Then it reprocesses the tasks while preferring candidates who are still below their Round 1 count.
The Round 2 flow is:
The alternative ranking strategy prioritizes:
This second round is effectively a refinement step that tries to better align the final assignment distribution with theoretical opportunity shares.
10. Special handling details
Symposium speakers
For public talks, symposium speakers are normalized so that they can participate in Part 1 selection together with standard speakers. A later check determines whether
WM_Speaker_Part2is actually needed based on the assigned speaker’s symposium qualification.Assistant pairing
For assistant roles, the ranking prefers pairings that have not occurred recently, which helps avoid repeatedly matching the same student-assistant combination.
Room 1 / Room 2 balancing
For certain
_Atasks in the alternative round, the system performs a small post-processing swap between the top two candidates if that improves Room 2 rotation fairness.11. Persistence
During processing, assignments are written immediately into the in-memory working structures:
This immediate in-memory update is necessary so that later tasks in the same run can see newly created assignments.
Between Round 1 and Round 2, these temporary assignments are partially cleared again for the affected tasks, so the second round can re-run the same week with adjusted quotas and candidate ordering.
Actual persistence to storage happens only after the autofill run finishes:
handleDynamicAssignmentAutofill()returns the modified weeks and updated schedulesschedulesStartAutofill()then persists the changed weeks with a singledbSchedBulkUpdate(...)Summary of the design
The autofill system is not a simple “pick the next available person” implementation.
It is a rule-driven scheduling pipeline that combines:
The result is a more balanced and more context-aware assignment distribution across weeks, views, and assignment types.
Additional review material
To help with reviewing the new distribution logic, I am attaching three exports showing the generated assignment results over roughly 1.5 years:
I am also including a link to a fork that contains the same logic, but with additional debug logging. It logs, for each assignment, which candidates were considered and in what order.
That fork also download two CSV files, which can be imported automatically from the Downloads folder and analyzed with the attached Excel files using Power Query.
Checking Autfill results.zip
Fork with additional logging: https://github.com/nobodyzero1/organized-app/tree/feature/improved-assignments-debug
Fixes #4456
Type of change
Please delete options that are not relevant.
Checklist: