Skip to content

fix(practice): assistant coaches enter the conflict check; slot-expansion docstring corrected (8.1) - #363

Open
JoelA510 wants to merge 3 commits into
mainfrom
feat/phase8-1-practice-defects
Open

fix(practice): assistant coaches enter the conflict check; slot-expansion docstring corrected (8.1)#363
JoelA510 wants to merge 3 commits into
mainfrom
feat/phase8-1-practice-defects

Conversation

@JoelA510

@JoelA510 JoelA510 commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Phase 8 task 8.1 (docs/PHASE_8_PLAN.md §8.1). Two defects in the practice scheduler — fixed in the core modules and in the shipped Edge Function path (review rounds 1–2) — one raised-not-fixed item, and the reachability finding that turned into the port.

Closes #362.

What changed

Defect 1 — assistant coaches were invisible to the conflict check.

Core. packages/core/src/practiceScheduling.js exports listTeamCoachIds(team): head coach plus assistants, deduplicated, empty ids dropped; read as assistantCoachIds first, then assistant_coach_ids (the precedence teamSnapshot.js already applies). A missing or null list is "no assistants" (the sibling if (team.coachId) contract); a list that is present and not an array throws a TypeError rather than reading as conflict-free. Every team.coachId site consults every coach:

  • practiceScheduling.js — booking, release, the multi-team priority (busiest coach, computed once per team alongside coachIds), the overlap check, and the unavailableSlotIds hard constraint (unioned over all coaches). Preferred days/slots are scoring hints and still follow the head coach — 8.2's one-coach-model work, left open on purpose.
  • autoScheduler.jscheckHardConstraints no longer returns valid on !team.coachId; unavailability and overlap loop over team.coachIds, precomputed once per run.
  • practiceMetrics.jscoachConflicts is one entry per overlapping pair: coachIds lists every coach the pair shares, coachId the first. computeFitness charges an overlap once whether the pair shares one coach or two; evaluationPipeline.js emits one error issue naming all of them ("Coaches h, s have overlapping practices"). coachLoad still counts each coach. Stated in the JSDoc above the first @param.

Shipped path (rounds 1–2). The app POSTs to supabase/functions/auto-scheduler/index.ts, a Deno port of these modules, and PracticeSchedulingPage.normalizeTeam dropped assistants before the request.

  • supabase/functions/_shared/engines/practice-coaches.ts: listTeamCoachIds (same contract as core: camelCase then snake, null/undefined → none, present non-array → throw) and checkHardConstraints; in _shared so a Deno test reaches the seam without importing the serve() module.
  • supabase/functions/_shared/schemas/auto-scheduler.ts: the request schema, moved out of index.ts onto the import-map zod so it is testable and composes the one shared TeamSchema (index.ts composed it with its own esm.sh zod, which does not type-check across two zod instances; index.ts used z nowhere else, so that import is gone). assistantCoachIds is z.array(z.string()).nullable().optional() in _shared/schemas/scoring.ts.
  • index.ts: every coachId-only site (buildState, mutation add/remove, greedy assign, priority sort) loops team.coachIds, resolved once at input preparation.
  • _shared/engines/scoring-engine.ts: mirrors practiceMetrics.js — each coach's assignments sorted, every overlapping pair visited, pairs merged across coaches; a precomputed team.coachIds is honoured, else the helper; reason and issue built after the merge, naming every shared coach.
  • frontend/src/pages/PracticeSchedulingPage.jsx normalizeTeam forwards assistantCoachIds: team.assistantCoachIds ?? team.assistant_coach_ids ?? []; exported for its vitest case (3 cases).

Defect 2 — practiceSlotExpansion.js docstring. Chosen: correct the docstring, not implement the adjustment (stated in the commit); implementing it is 8.9. Guarded behaviourally: expanding with and without sunset / lighting / daylight input gives identical output, an honoured input (fewer phases) changes it, and the declared parameter shape names no such input (a fake ({ slots, sunset }) fails the same predicate).

Raised, not fixed: #361practice_slots.day_of_week CHECK forbids Friday while the league runs 19 Friday rows. Migration is its own PR with revert + smoke scripts under docs/sql/.

Claims verified

Claim Result Actual
practiceScheduling.js and autoScheduler.js consult only team.coachId HELD (those two files) every coach path keyed on coachId at the lines listed
No other coach path exists DID NOT HOLD practiceMetrics.js; auto-scheduler/index.ts; _shared/engines/scoring-engine.ts; page normalizeTeam; PracticeOverridePanel.jsx (head-coach display names, manual-override UI — untouched). All but the last fixed here.
Corpus: 215 assignments / 132 teams / 196 people HELD derived from loadSeason2026() at run time
"Roughly 83 co-coach assignments unseen" HELD, exactly non-slot-1 assignments = 83; teams with >1 coach = 82 (81 with two, 1 with three; 0 without a head)
practiceSlotExpansion.js:13 daylight claim; no sunset/daylight/lighting input HELD only match was line 13; function takes { slots, seasonPhases }
Migration CHECK at line 501 ('mon','tue','wed','thu'); 19 Friday rows HELD enum on line 29 already includes fri; CSV Friday rows = 19; 8.0 loader asserts 19
Assistant spelling the check must read resolved from the repo engine/typedef/loader: assistantCoachIds; DB column and mock client: assistant_coach_ids; both helpers read camelCase first, then snake; the request schema declares camelCase (what the page sends) and the snake spelling arrives via passthrough.

Failing-then-passing evidence

Vitest. Six suites with the new tests and core sources reverted to origin/main: 11 failed | 137 passed. Layer isolation: with only autoScheduler.js reverted, 2 failed | 18 passed — the two "relocate cannot seat…" tests, built where fitness favours the clash. Item-4 comparator change (round 1): the processing-order fixture (read off which slot each team receives, since assignments is returned sorted by id) passed identically before and after.

Deno (executed via npx --yes deno@2, 2.9.6, DENO_NO_PACKAGE_JSON=1, remote imports through the proxy):

  • deno check on index.ts, practice-coaches.ts, scoring-engine.ts, schemas/auto-scheduler.ts: exactly one error, pre-existingTS2339 Property 'catch' does not exist on type 'void', now index.ts:566 (:644 on origin/main, same line of code). No new errors.
  • deno test _shared/tests/practice-coaches_test.ts: 14 passed after round 2. With the four round-1 sources restored (--no-check): 7 failed | 7 passed — every round-2 control red: snake spelling in the helper; TeamSchema null; request-schema null; issue naming both coaches; three-team merge (T2/T3 = [h, s]); assistant_coach_ids-only row; precomputed coachIds. With scoring-engine.ts + scoring.ts at origin/main (round 1): 2 failed | 6 passed. The three checkHardConstraints cases have no red "before" — the seam is new — and are proven by their in-test controls.
  • Pre-existing red, untouched: _shared/tests/scoring-engine_test.ts "coach conflict detection" (line 49) asserts result.coachConflicts[0].reason.includes('Time overlap'). It fails on origin/main and still fails: tests/unit/scoring-engine.test.ts (Vitest, green on main) asserts the same field contains 'overlapping practices', so the two pre-existing tests contradict each other and the main wording is kept. Not made to pass naturally.

Positive controls proven to fail: no-head-coach teams; "distinct assistants" controls; fitness control (two-coach pair equals one-coach pair, both below no-clash); pipeline control (one issue each, zero without a clash); optimizer oracle on a shared-assistant pair and a head/assistant crossover; normalizeTeam control (no raw assistant_coach_ids key survives); docstring test's fake ({ slots, sunset }) and phase-count change; request schema still rejects a non-array; singular rendering for a one-coach pair.

Deliberately left open

Verification

Test-verified (executed, output captured): npm run test — main baseline 2216 / 34 / 62242 / 34 / 6 (160 files, +26 tests). Deno suite 14/14 after, 7 red before. All before/after and isolation runs above.

Executed: npm run lint — 0 errors, 1 warning (the useVirtualizer baseline). npm run typecheck — 0 errors. npm run frontend:build — clean. npm run check:advisors — PASS. npm run check:bundle — PASS; both re-run after each Edge Function edit. prettier --check on the TypeScript files — clean. deno check as above. /code-review at high before opening (6 findings, all addressed).

Statically reviewed only: the index.ts booking loops in buildState, the mutation add/remove and the greedy seed — they call the tested seam but the module runs serve() at top level and cannot be imported by a test; the Edge Function needs a deploy to take effect.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ppMMPnorTKrb9ZVaGTpS9

…sion docstring corrected (8.1)

Defect 1: the practice conflict check read team.coachId only, so the
assistant coaches on teams.assistant_coach_ids never entered it. In the
season-2026 corpus that is 83 of 215 assignments (82 of 132 teams carry
more than one coach), all unseen.

- practiceScheduling.js exports listTeamCoachIds(team): head coach plus
  assistants, deduplicated, empty ids dropped. The list is read as
  assistantCoachIds first, then assistant_coach_ids (teamSnapshot.js's
  precedence). A missing or null list is "no assistants", matching the
  `if (team.coachId)` contract for a missing head coach; a list that is
  present and not an array throws a TypeError rather than reading as
  conflict-free.
- The greedy pass books, releases, counts (multi-team priority) and
  overlap-checks every coach on the team, and unions unavailableSlotIds
  over every coach. Preferred days/slots (scoring) still follow the head
  coach; that is 8.2's one-coach-model work.
- autoScheduler.js: checkHardConstraints no longer returns valid on
  !team.coachId; unavailability and overlap loop over team.coachIds,
  precomputed once per run rather than per mutation. buildState and the
  mutation add/remove book every coach.
- practiceMetrics.js: coachConflicts and coachLoad are keyed by every
  coach on the team, so a shared-assistant overlap is reported. One entry
  per coach, as before; documented.

Defect 2: practiceSlotExpansion.js claimed to "account for daylight
adjustments" with no sunset, daylight or lighting input anywhere in the
module. Chosen: correct the docstring, not implement the adjustment.
Implementing it is task 8.9 and depends on data this repo does not yet
hold. A test now guards the module against re-acquiring the claim.

Tests (19 new; every behavioural one fails on origin/main's sources and
passes here): shared assistant with only overlapping slots is reported as
a coach conflict; no-head-coach team still enters the check; locked
assignment books the assistant; assistant unavailability blocks as a hard
constraint; the optimizer's relocate cannot seat a team on its assistant's
clash or unavailability (these two fail with only autoScheduler.js
reverted, so they pin the optimizer layer specifically); evaluator flags
and controls; the teams-column spelling is read; a present non-array is
rejected.

Raised, not fixed: #361 (practice_slots.day_of_week CHECK forbids the 19
Friday rows the league runs) and #362 (the app runtime is a Deno port of
these modules with the same head-only check, and the page adapter drops
assistants before the request; the core fix is unreachable from the app
until both change).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ppMMPnorTKrb9ZVaGTpS9
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Sep 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
squadlogic Ready Ready Preview Sep 5, 2026 7:03am UTC

… auto-scheduler; one conflict per overlapping pair (8.1 review round 1)

Review round 1 on #363.

1. The shipped path. The app runs supabase/functions/auto-scheduler/index.ts,
   not the core modules, and the page dropped assistants before the request.
   - New _shared/engines/practice-coaches.ts: listTeamCoachIds and
     checkHardConstraints, mirroring the core fix exactly; kept in _shared so
     a Deno test can reach the seam without importing the serve() module.
   - index.ts: TeamSchema gains assistantCoachIds; every coachId-only site
     (buildState, mutation add/remove, greedy assign, multi-team priority)
     loops team.coachIds, resolved once at input preparation.
   - _shared/schemas/scoring.ts Team gains assistantCoachIds;
     scoring-engine.ts reports conflicts for every coach on the team, one
     entry per overlapping pair with coachIds listing the coaches shared.
   - PracticeSchedulingPage normalizeTeam forwards assistantCoachIds (read
     from either spelling the rows carry); exported for its vitest case.
   - Deno: `deno check` on the three files reports only the pre-existing
     TS2339 (`.catch` on void, line 638; line 644 on origin/main). New
     _shared/tests/practice-coaches_test.ts: 8 passed; with scoring-engine.ts
     and scoring.ts at origin/main the two scoring-engine cases fail. The
     checkHardConstraints cases have no "before" -- the seam is new. The
     pre-existing scoring-engine_test.ts "coach conflict detection" case
     fails on origin/main too (expects 'Time overlap' in a reason that reads
     'has overlapping practices'); untouched.

2. practiceMetrics.js: coachConflicts is one entry per overlapping pair of
   assignments, coachIds listing every coach the pair shares and coachId the
   first; computeFitness therefore charges an overlap once whether the pair
   shares one coach or two, and evaluationPipeline emits one issue naming
   all of them. coachLoad still counts each coach. Controls: equal fitness
   for one- and two-coach pairs, one pipeline issue each, zero without a
   clash.

3. practiceMetrics.js: the coachConflicts note now sits above the first
   @PARAM instead of between params.slots and @returns.

4. practiceScheduling.js: busiestCoachCount is computed once per team
   alongside coachIds; the comparator compares stored numbers. A fixture
   test pins the processing order (read off which slot each team receives)
   and passed unchanged before and after the change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ppMMPnorTKrb9ZVaGTpS9
… assistant spellings, admits null (8.1 review round 2)

Review round 2 on #363.

1. scoring-engine.ts visited only the first overlap per coach per
   assignment (schedule.find), so a pair's coachIds could omit coaches it
   shares and a pair could vanish. It now mirrors practiceMetrics.js: each
   coach's assignments sorted, every overlapping pair visited, pairs merged
   across coaches. Deno control: T1{h}, T2{h,s}, T3{h,s} all overlapping
   reports T2/T3 as [h, s] and T1/T2, T1/T3 as [h].

2. practice-coaches.ts listTeamCoachIds reads assistantCoachIds first, then
   assistant_coach_ids, null/undefined as none, present non-array throws --
   the core helper's contract. scoring-engine.ts honours a precomputed
   team.coachIds (index.ts sets it once at input preparation) and falls
   back to the helper otherwise. Deno controls: a row carrying only
   assistant_coach_ids is conflict-checked on its assistant; a precomputed
   list wins over the raw fields.

3. assistantCoachIds is .nullable().optional() in the shared TeamSchema.
   The request schema moved to _shared/schemas/auto-scheduler.ts on the
   import-map zod (index.ts composed the shared TeamSchema with its own
   esm.sh zod, which does not type-check across two zod instances; index.ts
   used z nowhere else, so its esm.sh import is gone). Deno control: a
   request with assistantCoachIds: null validates and the team is seated on
   its head coach; a non-array is still rejected.

4. The coach-conflict issue and the entry's reason are built after the
   pair is fully merged, naming every shared coach: "Coaches h, s have
   overlapping practices on Monday" / "Coach h has ...", matching
   evaluationPipeline.js. Deno control: T1{h,s} vs T2{h,s} yields one issue
   naming both; a one-coach pair keeps the singular.

5. tests/practiceSlotExpansion.test.js no longer scans the source for
   wording. It expands the same slots with and without sunset / lighting /
   daylight input and asserts identical output, that an honoured input
   (fewer phases) does change it, and that the declared parameter shape
   names no such input (a fake ({ slots, sunset }) fails the same check).

Deno: check reports only the pre-existing TS2339 (.catch on void, now
index.ts:566). practice-coaches_test.ts 14 passed; with the four round-1
sources restored (--no-check) 7 failed / 7 passed -- every new control red.
The pre-existing scoring-engine_test.ts "coach conflict detection" stays
red: it expects reason to contain 'Time overlap' while the Vitest suite
tests/unit/scoring-engine.test.ts, green on main, expects 'overlapping
practices' from the same field; the main wording is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ppMMPnorTKrb9ZVaGTpS9
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.

Assistant coaches are still invisible to the practice conflict check on the app runtime (Deno ports + page adapter)

2 participants