Skip to content

fix: Ensure all eligible persons are assigned by implementing weighted priority logic in auto assignment#4932

Open
nobodyzero1 wants to merge 40 commits intosws2apps:mainfrom
nobodyzero1:feature/improved-assignments-distribution
Open

fix: Ensure all eligible persons are assigned by implementing weighted priority logic in auto assignment#4932
nobodyzero1 wants to merge 40 commits intosws2apps:mainfrom
nobodyzero1:feature/improved-assignments-distribution

Conversation

@nobodyzero1
Copy link
Copy Markdown
Contributor

@nobodyzero1 nobodyzero1 commented Jan 27, 2026

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 flow
  • assignments_with_stats.ts — assignment statistics, opportunity scoring, weighting
  • assignment_selection.ts — candidate validation, conflict checks, ranking

Logic Implementation Details:

High-level flow

At a high level, autofill works in five stages:

  1. Load and normalize the current data snapshot
  2. Compute assignment statistics and person weighting
  3. Generate all fillable assignment tasks
  4. Run a 2-round assignment process
  5. Persist the final assignments

1. Data snapshot and preparation

The autofill process starts by reading the current state for:

  • persons
  • schedules
  • assignment history
  • settings
  • source material
  • active data view and locale-related settings

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:

  • the set of relevant data views (main + active language groups with meetings)
  • per-view eligibility maps (AssignmentCode -> eligible person UIDs)
  • fixed and linked assignment rules from congregation settings
  • normalized speaker handling for symposium speakers

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:

  • tasks handled manually
  • tasks disabled by configuration
  • linked tasks that should not be independently filled
  • public talk speaker slots when the user is not the public talk coordinator

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:

  • the expected frequency of that assignment
  • the set of eligible persons for that code in that view

This frequency model combines:

  • configured baseline frequencies from settings
  • variable weekly counts derived from source material
  • correction factors for special weeks such as CO visits, assemblies, or no-meeting weeks

A synthetic total view 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:

“Given this person’s qualifications and the congregation’s assignment demand, how much assignment load should this person reasonably receive?”

The score is based on:

  • which assignment codes the person can do
  • how frequently those codes occur
  • how many other eligible persons exist for the same codes
  • fixed assignment blocking rules
  • cross-view corrections for persons active in multiple views

This produces:

  • mm_globalScore
  • wm_globalScore
  • view_globalScore
  • per-code percentage shares within the view

Weighting factor per person

After that, the system computes a global weighting factor for every person.

The weighting factor compares:

  • the congregation-wide benchmark assignment load
  • the person’s total theoretical opportunity across all views

This creates a fairness multiplier:

  • people with relatively few assignment opportunities are boosted
  • people with relatively high assignment opportunity are slightly deprioritized

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:

  • the schedule/week
  • the exact meeting date
  • the assignment path/key
  • the resolved AssignmentCode
  • whether the role is elder-only
  • a scarcity-based sort index
  • the target data view

Global filtering

The initial assignment key list is filtered by:

  • meeting type (MM_ vs WM_)
  • class count (for example, removing _B tasks if there is only one class)
  • ignored keys from settings

Week-specific filtering

For each week, additional filters are applied:

  • Week type filter: only assignments allowed for that special week remain
  • Public talk filter: if the talk is not localSpeaker, local speaker parts are removed

Skip 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 AssignmentCode and whether the task is elder-only.

This resolution is source-aware and includes special handling for:

  • assistant parts in AYF
  • student parts
  • LC parts
  • static assignment defaults such as chairman or prayer

For example:

  • assistant parts are only created if the underlying source material actually requires an assistant
  • LC parts may be skipped if the item is effectively a video or otherwise does not require an assignment
  • some LC parts may be marked elder-only depending on title/description analysis

Scarcity-based sort index

Each task gets a sortIndex based on the number of currently valid candidates.

Fewer valid candidates means:

  • lower sortIndex
  • higher scheduling priority

This is important because the algorithm tries to fill the hardest tasks first.

For dependent task families, scarcity is unified:

  • assistant parts inherit the scarcity basis of the corresponding student part
  • WM_Speaker_Part2 inherits the scarcity basis of WM_Speaker_Part1

This 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:

  1. Chronological order by week
  2. Fixed assignments first
  3. Linked assignments last
  4. Scarcity order inside the remaining tasks
  5. Sub-parts after the main task within the same family

This ordering is intentional:

  • fixed assignments should lock people early
  • linked assignments cannot be resolved until their master task is known
  • scarce tasks should be filled before easy tasks
  • assistants and dependent speaker parts should follow the main assignment immediately

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:

  • family pairings
  • same-gender non-family pairings

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:

  • Cross-dataView hard block: a person cannot be assigned to the same meeting type in another data view during the same week
  • Same-dataView conflict matrix: conflicts are checked using the configured assignment conflict matrix

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:

  • overall global fairness
  • fairness inside the current data view
  • fairness inside the current meeting type (midweek vs weekend)
  • fairness for the exact assignment code
  • current load in the same meeting
  • pairing distance for assistant assignments
  • room rotation logic for some _A tasks

Historical 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:

  • higher tier score = person is more due / more underutilized
  • lower tier score = person has been used more recently or more often

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:

  1. filter valid candidates
  2. sort candidates with the default strategy
  3. assign the top-ranked candidate
  4. immediately write that result into the in-memory history and schedule structures

The default strategy prioritizes:

  1. fewer tasks already in the current meeting
  2. higher global fairness tier
  3. higher data-view fairness tier
  4. higher meeting-type fairness tier
  5. higher task-code fairness tier
  6. longer time since last assistant pairing (for assistant tasks)

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:

  1. remove the Round 1 task assignments from the temporary history
  2. keep the Round 1 per-person totals as quota references
  3. process tasks again
  4. prefer only candidates whose current count is still below their Round 1 count
  5. if that restricted candidate list becomes empty, fall back to the full candidate list

The alternative ranking strategy prioritizes:

  1. larger percentage gap between expected and actual share of that assignment code
  2. fewer tasks already in the current meeting
  3. higher task-code tier
  4. longer time since last assistant pairing

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_Part2 is 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 _A tasks 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:

  • the current schedule objects
  • the temporary assignment history used for conflict detection and linked-task resolution

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 schedules
  • schedulesStartAutofill() then persists the changed weeks with a single dbSchedBulkUpdate(...)
  • afterwards, the schedules state and full assignment history are refreshed

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:

  • congregation settings
  • source-material-driven task resolution
  • special-week filtering
  • availability and conflict validation
  • expected opportunity scoring
  • person-specific weighting
  • scarcity-first task ordering
  • 2-round fairness optimization

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:

  • a regular congregation
  • a congregation with two classes
  • an edge case with two classes and one language group that has midweek meetings only once per month

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.

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
staging-organized-app Ready Ready Preview May 5, 2026 8:36pm
test-organized-app Ready Ready Preview May 5, 2026 8:36pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 27, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds 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

Cohort / File(s) Summary
Assignment Conflict Matrix
src/constants/assignmentConflicts.ts
New file exporting STUDENT_TASK_CODES and ASSIGNMENT_CONFLICTS mapping assignment-code conflicts used by conflict checks.
Constants & Definitions
src/constants/index.ts, src/definition/assignment.ts
Restructures ASSIGNMENT_PATH into section-driven ASSIGNMENT_PATHS_SECTIONS with derived ASSIGNMENT_PATH/ASSIGNMENT_DEFAULTS, adds section/section-name types, week-type keyed maps, grouped assignment structures, and exports MM_ASSIGNMENT_CODES, WM_ASSIGNMENT_CODES plus stat-related types.
Autofill Orchestration (large)
src/services/app/autofill.ts
Replaces hard-coded autofill with a data-driven task pipeline: AssignmentTask type, settings processing, week-type/date helpers, task generation/filtering/sorting, symposium/assistant helpers, two-round autofill workflow, handleDynamicAssignmentAutofill and schedulesStartAutofill.
Selection Engine
src/services/app/assignment_selection.ts
New selection module: distance/last-date utilities, partner lookups, actual-load metrics, tier scoring, multi-level candidate sort (default/alternative), Room1/Room2 helpers, assistant-student validation and conflict detection.
Statistics & Eligibility
src/services/app/assignments_with_stats.ts
New stats module: per-view frequencies, eligible-UID maps, variable/correction counts, benchmark & weighting calculations, per-person opportunity scoring, and batch metric precomputation; exports many types/functions.
Schedules & History Persistence
src/services/app/schedules.ts
History updates now map MM_Chairman_A/B to correct codes and scope replace/removal of autofill history entries to the current dataView when recording changes.
Person Utilities
src/services/app/persons.ts
Adds isPersonBlockedOnDate(person, targetDate) to evaluate timeAway records for date-specific blocking.
Hook & Dev Utilities
src/features/meetings/schedule_autofill/useScheduleAutofill.tsx, src/utils/dev.ts
Hook now reads languageGroups via atom and passes them into autofill; dev tooling adjusted to use language-group-aware autofill, extend month window, bulk update schedules, rebuild history, and sequence autofill steps with language-group context.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

released

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing weighted priority logic in auto assignment to ensure fair distribution.
Description check ✅ Passed The description provides comprehensive details on the 2-round weighted autofill pipeline, statistics model, task generation, and candidate ranking logic directly related to the changeset.
Linked Issues check ✅ Passed The PR implements all key requirements from #4456: fair distribution across eligible persons using weighted fairness model, full assignment history, conflict detection, multi-level ranking, and two-round optimization.
Out of Scope Changes check ✅ Passed All changes focus on autofill logic, assignment statistics, candidate selection, and supporting constants; none are unrelated to the fair assignment distribution objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @deprecated annotations 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.

  1. Remove the commented-out code block (lines 868-870) - it clutters the codebase
  2. 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 === currentDataView check at line 537 is redundant. At this point in the code, tasksInWeek only contains entries that passed the dataView check (lines 504-508 return true early for cross-dataView conflicts), so all remaining entries are guaranteed to be in currentDataView.

♻️ 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 getSortedTasks with 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.

Comment thread src/constants/index.ts Outdated
Comment thread src/features/meetings/week_range_selector/useWeekRangeSelector.tsx
Comment thread src/services/app/assignment_selection.ts
Comment thread src/services/app/assignment_selection.ts Outdated
Comment thread src/services/app/assignments_with_stats.ts Outdated
Comment thread src/services/app/autofill.ts Outdated
Comment thread src/services/app/autofill.ts Outdated
Comment thread src/services/app/autofill.ts
Comment thread src/services/app/autofill.ts Outdated
Comment thread src/services/app/persons.ts Outdated
Comment thread src/features/meetings/week_range_selector/useWeekRangeSelector.tsx Outdated
@rhahao
Copy link
Copy Markdown
Member

rhahao commented Feb 4, 2026

@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.

@nobodyzero1
Copy link
Copy Markdown
Contributor Author

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:
Testresult autofill.zip

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?

sortCandidatesMultiLevel.txt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 extracting ALL_ASSIGNMENTCODES as an exported constant.

The ALL_ASSIGNMENTCODES set 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, {

Comment thread src/services/app/assignment_selection.ts
Comment thread src/services/app/autofill.ts
@nobodyzero1
Copy link
Copy Markdown
Contributor Author

nobodyzero1 commented Feb 5, 2026

@rhahao
I realized that restricting the frequency calculation to a 4-month window before the planing start period (as originally implemented) creates fairness issues for low-frequency tasks or larger congregations.
Problem: If a task rotates only every 6-8 months (e.g., in a large group), a 4-month window makes 90% of the publishers look identical (0 assignments), causing the algorithm to fall back to global tiers too aggressively and potentially repeating recent assignments.
Fix: I removed the date restriction (historyLimitDate). The algorithm now utilizes the full available history to establish accurate personal assignment frequencies. This ensures that the rotation remains fair over long periods (years) and correctly prioritizes those who haven't handled a task in a long time.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 5, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 5, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Feb 5, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 in calculateOpportunityScore.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5af0aee and 5d85935.

📒 Files selected for processing (2)
  • src/services/app/assignments_with_stats.ts
  • src/services/app/autofill.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 11, 2026
@sonarqubecloud
Copy link
Copy Markdown

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 27, 2026

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

@nobodyzero1
Copy link
Copy Markdown
Contributor Author

Hi @rhahao !
Comparison of current and new autofill logic.xlsx

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] Meeting Autofill Skips Eligible Students

2 participants