Skip to content

feat(auto-assign): default internal matching to lexicographic ranking#146

Closed
guangshinhaha wants to merge 1 commit into
mainfrom
claude/review-auto-assignment-logic-5GMKA
Closed

feat(auto-assign): default internal matching to lexicographic ranking#146
guangshinhaha wants to merge 1 commit into
mainfrom
claude/review-auto-assignment-logic-5GMKA

Conversation

@guangshinhaha
Copy link
Copy Markdown
Collaborator

Replaces the cascading-filter algorithm as the default for internal teacher
auto-assignment. Each rule now scores candidates in [0,1] and the pool is
ranked by the score tuple; no candidate is eliminated mid-cascade, so a
teacher slightly heavier on workload can still win the slot if they're the
only subject + class match. The legacy cascade stays available as
School.algoMode = 'CASCADE' for schools that prefer strict rule-priority
elimination — flip via DB until a settings UI ships.

Settings page now surfaces the hard constraints (no self-cover, no
double-book, no assign while sick, hard weekly cap) as locked uneditable
cards above the soft-rule list, and the soft-rule copy is updated to
describe ranking instead of cascade narrowing. hard_weekly_cap moves out
of the soft list (it remains in the rule registry for cap-value config).

Replaces the cascading-filter algorithm as the default for internal teacher
auto-assignment. Each rule now scores candidates in [0,1] and the pool is
ranked by the score tuple; no candidate is eliminated mid-cascade, so a
teacher slightly heavier on workload can still win the slot if they're the
only subject + class match. The legacy cascade stays available as
School.algoMode = 'CASCADE' for schools that prefer strict rule-priority
elimination — flip via DB until a settings UI ships.

Settings page now surfaces the hard constraints (no self-cover, no
double-book, no assign while sick, hard weekly cap) as locked uneditable
cards above the soft-rule list, and the soft-rule copy is updated to
describe ranking instead of cascade narrowing. hard_weekly_cap moves out
of the soft list (it remains in the rule registry for cap-value config).
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

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

Project Deployment Actions Updated (UTC)
relief-teacher-planning Ready Ready Preview, Comment Apr 30, 2026 1:04pm

guangshinhaha added a commit that referenced this pull request May 4, 2026
…ives #146) (#149)

* feat(auto-assign): ROC-weighted ranking + 2-opt global swap pass

Replaces the per-slot cascading-filter algorithm as the default for
internal teacher matching. The new WEIGHTED mode runs in two stages:

  Stage 1 — Rank-Order Centroid (ROC) weighted sum across enabled rules.
  Weights are derived automatically from the admin's drag-to-reorder
  rule order (Barron 1992); no manual tuning. For 6 rules the weights
  are roughly 41% / 24% / 16% / 10% / 6% / 3% — rule #1 dominates but
  isn't absolute, so a candidate strong on multiple lower-rank rules
  can beat one only strong on rule #1. Fixes the small-pool collapse
  problem where strict lex / cascade always pick on rule #1.

  Stage 2 — 2-opt pairwise swap pass over the Stage-1 proposals.
  Catches cross-slot teacher trades — e.g. a teacher who is the only
  Math + 3A match shouldn't get "used up" on a less critical earlier
  slot. Each swap must strictly improve total weighted score and
  satisfy hard constraints (no double-booking, no busy-period
  conflicts, no self-cover). Deterministic, sub-100ms for n ≤ 20.

CASCADE mode (the legacy strict-priority filter) stays available as
an opt-in via School.algoMode = 'CASCADE'. Hard constraints (no
self-cover, no double-book, no assign while sick, hard weekly cap)
are surfaced in the Settings → Algorithm page as locked, uneditable
cards above the editable soft-rule list.

* fix(auto-assign): address review of ROC + 2-opt branch (PR #146 revival)

B1 — 2-opt was a no-op for the headline use case. `scoreCandidateForSlot`
called `rule.score(teacher, ctx, [teacher])`. The soft-skip branch in
`match_subject` / `prefer_same_class` (`if (!pool.some(...)) return 1`)
fired for every (teacher, slot) pair because the single-element pool
never matched, so swap deltas were always 0 and the cross-slot trades
the algorithm exists to find never fired. Pass the full teacher pool
through `runTwoOptImprovement` instead.

B2 — `pickedRuleId` reported the dominant ROC contributor, not the
differentiator. With heavy front-loaded weights, a tied rule #1 still
won attribution even when both candidates scored equally on it. Switch
to `argmax weights[i] * (score(picked) - score(runnerUp))` so the
deciding rule is the one that actually separated the pick from the
runner-up. Fixes nonsensical "Lower balance_workload" alternative
strings as a side effect.

C5 — flip rollout default to CASCADE. Schema and migration now default
new schools to the legacy filter; WEIGHTED is opt-in per-tenant via DB
flip until a settings UI ships.

C7 — `AlgorithmTab.handleReset` hardcoded `enabled: true` for every
rule and wiped `config` blocks. Now reads `defaultEnabled` from the
registry and preserves existing config (the workload-window settings
admins set explicitly).

C3 — explicit caveat in `scoreCandidateForSlot` docstring that
`prefer_consecutive` is excluded from Stage 2 because the prior-period
teacher is null; a swap can break a deliberately-consecutive pair.

Tests: Q4 multi-tenant CASCADE/WEIGHTED isolation, Q5 hard_weekly_cap
exclusion in WEIGHTED mode, C4 3-proposal forward-sweep ordering.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants