# CC Soccer — Outstanding Issues Tracker

**Created:** July 28, 2026 · **Updated:** July 30 (D19 promoted out of Parked) · **August 4** (roster-sync
work merged to `main` — see the banner below)

> ## 🟠 READ FIRST — "resolved in code" here means LOCAL-tested, NOT deployed
>
> Six code fixes merged to `main` on Aug 4 via PR #124: **CF1, CF1b, CF2 + D2, CF12, CF4, CF8, CF5.**
>
> **[Updated Aug 5] All of it has now been linted and click-through tested on LOCAL** —
> `ROSTER_RECONCILIATION_PLAN.md` §10.8 passed, and registration 5088 was cleaned up. ~1,100 lines that
> had never run have now run. **None of it is on TEST or PROD.**
>
> Items below marked **`◐ FIXED IN CODE`** have a merged fix that has been watched working on LOCAL but
> is not deployed. **They are not closed and their operating rules stay in force until the code reaches
> PROD.** Items marked **`✅ RESOLVED`** are verified.
>
> **Also landing, not covered by the list below:** the **D-8** package (CF10 + B + E) on branch
> `fix/solo_group_auto_delete` — `dde90b5` and `995e286` committed, a third commit written but not yet
> linted or tested. Spec and test plan in `GROUP_CLEANUP_BRIEF.md`.
>
> **`ddev drush cr` is mandatory before testing** — four constructor/service changes landed.
>
> **The test checklist is `ROSTER_RECONCILIATION_PLAN.md` §10.8.** That document is the working plan
> for this whole cluster; this tracker is the prioritized list. Start there.
**Purpose:** Single source of truth for what's still open across `INVITATION_FLOW_ANALYSIS.md`,
`ORDER_FLOW_EDGE_CASES.md`, `SEASON_TOURNAMENT_DRIFT_AUDIT.md`, `ADMIN_TOOLING_REVIEW.md`, and the
`INVITATION_FIX_LOCAL_TEST_CHECKLIST.md`. Those five docs remain as the detailed analysis of record;
this one is the pruned, prioritized list. Completed items are summarized at the bottom, not repeated in full.

**Context:** Registration for the new season is open. Rosters, schedules, and the SLO Friendly tournament
will be built over the next few weeks. Priorities below are ordered around that work.

**Explicitly out of scope (per Andrew, July 28):** abandoned-cart / abandoned-order edge cases — a user
starts an order or leaves something in the cart and an admin closes registration, orphaning it. The league
is ~200 players/season and ~200 in the tournament; affected players can email or reach out directly.
Deprioritized items of that shape are listed under "Parked" and not counted as blockers.

---

## In-flight work to land first

> **[Aug 4] All six items in this section are done and on `main`.** Verified with
> `git merge-base --is-ancestor`. This section caused a wrong sequencing recommendation on Aug 2 by
> listing already-deployed work as pending — the exact failure `DOC_CLEANUP_CONSOLIDATION_WIP.md` §1
> was written about. It is replaced rather than annotated.
>
> | Was listed as pending | Actual |
> |---|---|
> | `fix/team_display` (9dfe05f) — D4 + T9 + manage-page display | merged, PR #119 (`7fd60bf`) |
> | `fix/tournament_nudge_500` (fa76cfd) — D14 | on `main`, **PROD-verified Aug 2** |
> | `feature/update_registration_page` | merged |
> | Core 11.4.4 + security updates (d75ce2e) | on `main`, PROD on 11.4.4 |
> | Season roster builder admin group-cap override | committed `b457883`, merged via PR #119 |
> | Tournament roster builder over-capacity warning | committed `b457883`, merged via PR #119 |

**The only in-flight work now is the Aug 4 roster-sync cluster — merged but never executed.**
See the 🔴 banner at the top. First job, in order:

1. `ddev php -l` on the twelve changed files.
2. **`ddev drush cr`** — mandatory, four constructor/service changes.
3. `ROSTER_RECONCILIATION_PLAN.md` §10.8 — the consolidated LOCAL checklist. **Clean up registration
   5088 first**; it carries bad state from the Aug 4 reproduction and will confound tournament tests.
4. TEST → PROD. Two operating rules lift only when it reaches PROD (see D19 and D2 below).

---

## P1 — Roster building (do before building rosters)

**Cancelled-registration hygiene (one root cause, many symptoms).** Cancelled registrations still carry
their old `group_id`/`invited_by`, still count toward group size, still render on rosters, and can be
picked as group manager — and a cancelled player's stale group ref blocks re-inviting a replacement.
Add a `status NOT IN (cancelled, expired)` filter to the season group queries and reconcile existing data
before rosters are built, or counts will be wrong going in.
- `INVITATION_FLOW_ANALYSIS.md` E4 · `ORDER_FLOW_EDGE_CASES.md` M5 · `SEASON_TOURNAMENT_DRIFT_AUDIT.md` D13 · `ADMIN_TOOLING_REVIEW.md` R4

> **◐ PARTIALLY FIXED IN CODE [Aug 4] — the write side is done, the read side is not.**
>
> - **Stops being created:** **CF8** (`b22734f`) clears `group_id`/`invited_by`/`invitation_status` on
>   season cancel, and dissolves the group if the canceller was the manager (decision D-2). **CF4**
>   (`b46c28b`) stops every write path resolving to a cancelled row.
> - **Still open — the status filters themselves.** **CF3 is not started**: nine season group queries
>   still count cancelled rows. That is the "counts will be wrong going in" half of this item, and it
>   is unchanged. See `ROSTER_RECONCILIATION_PLAN.md` CF3 — **its brief carries stale line numbers,
>   re-locate by method name.**
> - **Existing drift is NOT repaired.** Andrew's July 28 decision stands: code fixes only, no data
>   repair this cycle. Rows already carrying stale refs stay as they are.
> - **One canonical definition now exists:** `LiveRegistrationTrait::isDeadRegistrationStatus()` —
>   dead is `cancelled` or `expired`, `waitlist` is live (decisions D-4, D-13). **Anything new that
>   needs to ask "is this registration live" should use it** rather than inventing a third answer.

**Season roster builder:**
- **R3 (HIGH)** — coed "≥1 woman per team" is a scoring bonus, not a constraint; teams can finalize with
  zero women. Needs a hard pass + validation summary.
- **R2 — LOW, accepted (July 28).** "Suggest Rosters" leaves unplaceable players in the workbench without the
  toast saying so (leftover individuals go to `unassigned` silently; an oversized group — ~11+, not reachable
  by 3/4 player groups — would error and clear the board). Accepted as-is: the board runs Suggest repeatedly
  as the league fills, then manually reviews and places the workbench before publishing, so unplaced players
  are always seen. Optional future nicety: surface the unassigned count in the toast. Not doing now.

**Tournament roster builder (SLO Friendly):**
- **T1 — ✅ AS DESIGNED (July 28).** Multi-drag force-adds past the roster cap. Accepted as intentional:
  admin-only (`manage tournaments`), single-day friendly with no scores/standings, board validates team
  sizes before publishing. Verified nothing downstream breaks (schedule gen uses registration counts as
  display metadata only; checkout re-checks capacity and flags rather than overflowing; `Team.players` is
  unbounded). A non-blocking over-capacity warning was added to `move()` so accidental overflow is visible
  at drag time. **The real risk to "validate before publish" is T2/T3/T5, not this.**
- **T2 (CRIT) — ◐ FIXED IN CODE, existing ghosts remain.** Ghost members (cancelled regs never removed
  from `Team.players`) inflate `isFull()` → admin drags onto a visibly half-empty team and gets "Team is
  full," OR eyeballs a count that includes a ghost. **CF1/CF1b (`e5317f8`, `8156bc1`) stop new ghosts
  being created** — tournament cancel now removes the player from every team in the tournament, not just
  the one `reg.team` names. **No repair was run**, so ghosts already in the data still inflate counts
  and the "Team is full" symptom persists on affected teams until someone clears them by hand.
- **T3 / T5 (CRIT/HIGH) — ◐ FIXED IN CODE.** move/merge/create loaded registrations with no status
  filter + `reset()`, so a cancelled reg could be team-assigned and moving a ghost left the old team
  dirty. **CF4** (`b46c28b`) replaced the bare `reset()` at all nine roster-builder sites — they write
  `set('team')` and were mis-filed as display-only. **CF5** (`7c9a7c5`) makes `move()`, `mergeToGroup()`
  and `createGroup()` reconcile against every team rather than trusting `reg.team`; moving to the
  workbench now clears the player off all teams.
- **D6 / D7 — ◐ FIXED IN CODE.** Tournament cancel and admin accept/decline never synced
  `Team.players`. **CF1** wires up `unassignPlayerFromTeam()` (which had zero callers) and handles
  captains *and* co-captains; **CF2** (`59c12ec`) syncs both sides on admin accept/decline.
  **CF2 has never been exercised even once** — the Aug 4 testing never touched the admin page.
- **D19 (HIGH — promoted out of Parked July 30)** — the admin **Group Invitations** page shows recently
  joined tournament players as **Pending** when they are confirmed members. `createTournamentRegistration()`
  never sets `invitation_status`, so tournament registrations store the field default `'none'`, which is not
  one of the dropdown's options — the browser then renders the first one, "Pending". The same rows are
  counted as *accepted* by the stats bar above them, so the page contradicts itself (live: Lunch Crew reads
  `Accepted: 7` with three dropdowns on Pending). **A June 26 regression** — `84dac45` unified the checkout
  `group_id` so these players appear on the page at all, and its repair hook 9068 marked the *existing* rows
  accepted while the ongoing write path never got the same line. Fix scoped as **CF12** in
  `ROSTER_RECONCILIATION_PLAN.md`; ships with CF2 + D2 in commit 2. Retires D19 and half of D15.
  > **◐ FIXED IN CODE [Aug 4]** — display half in `e5317f8`, write half in `59c12ec`. Merged, untested.
  >
  > **⚠ THE OPERATING RULE STAYS IN FORCE until this is tested on LOCAL and deployed to PROD: do not
  > click _Save_ on the admin Group Invitations page for a tournament group.** Saving with no edits
  > writes a real `invitation_status = 'pending'` onto those rows, converting a display fault into bad
  > data. Reading the page is safe. Merging the fix does not lift the rule — running it does.
  >
  > Note the fix is not what the original brief described. The member-row `<select>` is not branched by
  > context and dropping an option would not have closed the trap; the real fix was normalising the
  > default value on the tournament side. See the CF12 brief in the plan.

---

## P2 — Scheduling (do before/while building schedules)

- **S2 (HIGH)** — after a season regenerate, the season stays "VISIBLE" while all new games are unpublished:
  players see an empty schedule, no prompt to republish.
- **TS2 (HIGH)** — the tournament schedule goes public *instantly* on regenerate (public view ignores per-game
  `published`). **Operating rule until fixed: keep `schedule_visible` OFF while iterating candidates.**
- **S8 (MED)** — a deleted team leaves "Unknown"/"TBD" opponents and can block regenerate with a
  "Team count mismatch."
- **Operating rules already agreed (decision 2):** finalize teams/slots/fields/dates before generating
  candidates; snapshot before any regenerate; never regenerate after week 1 of a live season.
- **S4 / TS3 (ON HOLD — Andrew deciding):** drag-swap bypasses validation (self-matchups, double-bookings
  save silently). Blocking is *not* wanted. Proposed: `findScheduleConflicts()` + non-blocking warning banner
  + single confirm at publish. No code written pending decision.

---

## P3 — Tournament flow

- **D2 (CRIT) — ◐ FIXED IN CODE, rule still in force.** Admin-accepting a tournament invitation crashes
  mid-transaction (`GroupInvitationsForm` queries a `tournament` field that doesn't exist on the
  Invitation entity), leaving half-committed state: player joined, invitation still pending, competitors
  never declined. **Fixed in `59c12ec` with CF2 and CF12's write half.** Merged to `main`, **never
  linted or executed.**
  > **⚠ OPERATING RULE: the admin Group Invitations page is read-only for tournament groups** — no
  > accept, no Save — **until `59c12ec` is tested on LOCAL and reaches PROD.** This is a live PROD bug;
  > the data damage from a mid-transaction failure was repaired by hand on Aug 1.
  >
  > **This is the least-validated code in the whole Aug 4 cluster.** The reproduction that found the
  > CF4 bug never touched the admin page, so nothing about CF2 or D2 has been observed working. Give it
  > first priority in §10.8's checklist.
- **D3 (CRIT)** — tournament order completion never re-checks that the tournament is still active/visible
  (season branch does + flags the order). Port the season guard to `createTournamentRegistration()`.
- **D17 (DECISION NEEDED)** — no tournament equivalent of `groups_locked`. If the board expects to freeze
  tournament rosters before scheduling, the mechanism doesn't exist. Decide before roster work if needed.
- **D12 (MED)** — tournament "closed" state derives only from `registration_close`; if unset, registration
  stays open forever (Tournament entity has its own `isRegistrationOpen()` the controller doesn't use).
- **D5 (HIGH)** — tournament cancellation notifies nobody (player silently dropped, possibly silently refunded).

---

## P4 — Game-day ops (comes online after schedules publish; not roster/schedule-blocking)

- **G1 (HIGH, DECISION NEEDED)** — cancellation notices are skipped for players with notification preference
  `none` or `text`-without-phone. Board decision: should cancellations bypass notification preference?
- **G2 (HIGH)** — for a tournament-only date, the cancel flow finds 0 players, sends nothing, credits nothing,
  and reports success. Game-day cancel is season-keyed only.
- **G4 / G5 (HIGH)** — public per-season iCal emits cancelled games as `STATUS:CONFIRMED`; printable PDF shows
  cancelled (and unpublished) games as normal. The newer per-user iCal feed handles status correctly.
- **G3 — recommended for DELETION, not repair.** The 3 pm cancellation-reminder cron is a redundant second
  league-wide email+SMS; it does not drive the status banner (that recomputes live per request). **Check PROD
  first** — `drush config:get ccsoccer.game_status notification_log` and `crontab -l` — to learn whether it's
  been firing, then remove per the scope in `ADMIN_TOOLING_REVIEW.md`'s G3 note.
- **G6 (MED)** — legacy Views iCal feed at `schedule-feed/%/ical` is public and emits garbage events.

---

## P5 — Invitation/order gaps still open (not abandoned-cart)

- **E3 residual** — the already-registered token path (`acceptSeasonInvitationDirectly()` /
  `acceptTeamInvitationDirectly()`) still lacks the invitee-email identity check that `GroupPane` now has.
  Narrow trigger (clicking a token addressed to someone else while already holding any registration for that
  season). Add the same email-match guard the "Accept" button path already uses + a status filter to
  `available()`'s already-registered lookup.
- **Accept-from-My-Registrations has no capacity check** — `acceptSeasonInvitation()` /
  `acceptTeamInvitation()` (and the `*Directly()` twins) let a player accept into an already-full group.
  Same class as the checkout-side guard that's now in place.
- **Group-full user messaging (designed, not built)** — (1) in `GroupPane`, show a full group's invitation
  disabled ("Join X's group (full)") rather than omitting it; (2) on the completion page, tell a player whose
  group filled mid-checkout that they're registered but not grouped. The completion-page message is the more
  important half.
- **Credits on standalone jersey purchases (BOARD DECISION — may be as-designed).** `CreditsPane::isVisible()`
  → `hasSeasonInCart()` shows the credits step only when the cart contains a `season_registration` product
  (`CreditsPane.php:45-48, 330`). A standalone jersey purchase (`/purchase-jerseys`, jersey-only cart) therefore
  has no credits step — a player with a credit balance pays cash. Credits DO cover a jersey when it's bought in
  the same order as a season registration. This is long-standing (Dec 2025), not a recent change. Decide whether
  standalone jersey purchases should be payable with credits; if yes, it's a scoped change to the visibility gate
  plus verification that the credit-apply math and `OrderCompleteSubscriber` handle a jersey-only order (both
  currently assume a season registration is present). Board may well resolve this as-designed.
- **S1 (policy)** — `field_notification_preference = none` suppresses transactional email too (receipts,
  invitations). A player who picks `none` pays and gets no receipt. Recommend transactional sends bypass the
  preference; keep it for reminders/announcements. (Relevant to any "notifications aren't arriving" report.)
- **Item 5 — invitation data-repair drush command** — the reusable bulk sweep for orphaned `accepted`
  invitations was never built (Phase 2). PROD data has been patched piecemeal by hand as cases surfaced. Low
  urgency now that Bug 1 is fixed, but the command doesn't exist if a batch is ever needed.

---

## New — found August 4, not previously tracked

**From Caleb's Aug 4 session (capacity race — two players paid, got no registration):**

- **No capacity check at add-to-cart (HIGH).** `addSeasonToCart()` / `addTournamentToCart()` guard
  duplicates, inactive seasons and age eligibility but never capacity. The only check in the whole flow
  is the completion-time guard in `OrderCompleteSubscriber`, which fires *after* payment is captured.
  Two confirmed incidents with real money (orders 304, 309), both manually resolved on PROD.
  Design agreed, not built: a `CapacityManagerService` plus three checkpoints. Full detail in
  `SESSION_HANDOFF.md`.
  > **Cross-reference:** the planned "effective capacity" formula counts *live paid/active
  > registrations*. **That definition is already settled** — decisions D-4 and D-13, implemented as
  > `LiveRegistrationTrait::isDeadRegistrationStatus()` (exclude `cancelled`/`expired`, `waitlist`
  > counts as live), used by eight classes. Build on it; a third definition of "live" is exactly the
  > drift shape this whole cluster exists to stop.
- **`sendRegistrationConfirmation()` sends a false-positive receipt (HIGH).** It fires on cart contents,
  not on whether a registration was actually created — so both affected players were told they were
  registered, which is why neither reported the problem. **Same method as S1 below** (preference `none`
  suppressing transactional mail). One fix, two faults.
- **`FLAGGED_ORDERS_REPORT_PROPOSAL.md`'s July 3 deferral is now demonstrably wrong.** It assumed
  players self-report by email; the false-positive confirmation above is why they did not. Worth
  revisiting whether to build the admin report. Related: **S3** under Parked.
- **Cart-expiration config drift.** Caleb enabled 2-day abandoned-cart expiration on LOCAL, TEST and
  PROD, but it was never `drush cex`'d — `config/sync/commerce_order.commerce_order_type.default.yml`
  still shows `cart_expiration: {}`. All three environments agree, so not urgent; it is drift sitting
  uncaptured.

**From the Aug 4 roster-sync session (found while diagnosing, not fixed):**

- **Phantom captain view (LOW, cosmetic).** A tournament registration with no team renders as a manager
  with an Invite a Player form — `GroupController:419` falls back to `$is_manager = empty(invited_by)`
  when `$team` is NULL. Submitting then errors "No team found for this registration" (`:896`).
  Pre-existing, looks alarming to a player, cheap to fix if someone is already in `manage()`.
- **Destination resolution in the roster-builder group operations (MED).** `mergeToGroup()` and
  `createGroup()` derive the *destination* team from `reg.team` (the first group member's, and the
  member's). If that player is a ghost, the destination resolves to NULL and the pair lands nowhere.
  Same root cause as CF5 but the opposite direction — CF5 fixed the *source* side only.
  `TournamentTeamManager::getPlayerTeam()` already has the roster-scan fallback that would fix it.
- **`NotificationService::send()` docblock is wrong (TRIVIAL, but load-bearing).** Lines 385-386 still
  say *"If not provided, body will be stripped"*. Since the Aug 2 rewrite an empty `$sms_body` means
  send no SMS, silently and without a per-user log line. The docblock documents the exact trap CF8
  nearly fell into. **Caleb's rewrite — his to correct.**

---

## Loose ends / hygiene

- **Verify Avi's refund** — order 87 ($133), Payments tab. Has slipped several sessions; ~30 seconds.
- **Recursion-guard warning fires on every completed order** (parked). Its text is wrong in the common case
  and it retroactively weakened the E-guard verification. Proposed fix: inside the static-guard branch, check
  `ccsoccer_completion_processed` first — set → benign end-of-method save → return silently; not set → warn.
- **`CartEventSubscriber` debug logging is live on PROD** — three notice rows per add-to-cart plus a
  severity-7 line logging each user's notification preference by username. Same class as the
  `NotificationService::send()` DEBUG noise already removed.
- **`notification_log` is runtime state stored in exported config** (`ccsoccer.game_status` in `config/sync`)
  — shows as drift in `config:status` and a `cim` rolls the live log back to the git snapshot. Cure: move to
  `\Drupal::state()` + an update hook. (Cannot cause a double-send.)
- **Ask Andrew** about the unexplained 17-line deletion in `CcsoccerCommands.php` on `fix/tournament_nudge_500`.
- **Season confirmation SMS reads "CC Soccer"** — checklist item 33 scoped the `CC Soccer` → `CCSoccer` fix to
  tournament deposit + jersey only; it's in season confirmations too.
- **Open security follow-ups** (from July 3 review) — E (jersey `variation_id` verify), I (scrub/rotate test
  creds), K (generic exception messages), G residual, #3 watch. Details in `CC_Soccer_Security_Review_2026_07_03.md`.

---

## Parked (out of scope per Andrew — abandoned-order shape; players can email)

- **S3** — flagged "paid but broken" orders have no admin surface (checklist item 18 /
  `FLAGGED_ORDERS_REPORT_PROPOSAL.md`). Interim board-notification on flag writes still worth a look, but not a blocker.
- **S4** — waitlist offers never expire in the system (entry stuck `offered`, next-in-line never offered,
  reserved spot held). Only relevant if season waitlists get real use.
- **E5** — invitation `expired` status is defined but never set (pending invitations reserve capacity forever
  until manually cleared). **[Aug 4] Partially relieved, not closed.** Decision **D-3** settled that
  nothing should ever write `expired` to an Invitation — no code reads it, so using it would create a
  state nothing handles. Decision **D-12** then made a group dissolve decline both pending *and*
  accepted invitations, so a dissolved group no longer strands either kind. Invitations outside a
  dissolve are unchanged and still reserve capacity indefinitely.
- **E7** — dead-token UX: a non-pending emailed link behaves as if it never existed, no message.
- **M1–M4** — confirmation-email wording, admin re-fire pipeline residuals, jersey stale-info on re-registration,
  credits donate-at-completion snapshot.
- **D9–D11, D15, D16, D18, D20** — assorted season↔tournament drift of low practical impact / policy calls.
  (**D19 removed from this list July 30** — promoted to HIGH under P1. **D15 is now half-live**: the option-list
  half lands with CF12; the missing tournament status filter at `GroupInvitationsForm:617-623` stays parked
  and belongs with CF3's sweep. **[Aug 4] CF12 is merged, so D15's option-list half is fixed in code —
  the parked status-filter half is unchanged, and its line reference has drifted; re-locate by method
  name, three commits have edited that file since.**)

---

## Completed since these docs were written (for reference — do not re-open)

**Shipped to PROD (verified July 27):** Bug 1 (early accept at checkout step 2), Bug 2 (`removeMember`
invitation cleanup), Bug 3 (narrow orphan re-invite), E2/E6 post-payment capacity guard, the
`onOrderPlace()` infinite-recursion fix (`d466b97`), Bug 4 (email-only invitees get the email) + the
one-time repair command run live, S2 (tournament auto-decline of all pending invitations), D1 (100× refund
money bug), D8 (`removeMember` cleanup on both branches). Invitation-timing work = `086d04d`; jersey/dead-code
cleanup = `7e1dd4b`.

**Merged to `main` since (Aug 2 verification):** D4 (co-captain remove) + T9 (co-captain cleanup) +
manage-page display bug — PR #119 (`7fd60bf`). D14 (tournament nudge 500) — `fa76cfd`, **PROD-verified
Aug 2**. Core 11.4.4 + security updates. `feature/update_registration_page`. Season roster-builder
admin group-cap override and tournament over-capacity warning (`b457883`).

**◐ MERGED Aug 4 via PR #124 — CODE ONLY, NEVER EXECUTED. Do not treat as closed:**

| Fix | Commit | Closes |
|---|---|---|
| CF1 / CF1b | `e5317f8`, `8156bc1` | D6 write side, T2 (new ghosts only), TA1/TF/TJ |
| CF2 + D2 | `59c12ec` | D2, D7 — **live PROD bug, operating rule still in force** |
| CF12 | `e5317f8` + `59c12ec` | D19, half of D15 — **operating rule still in force** |
| CF4 | `b46c28b` | T3/T5 write paths; the bare-`reset()` class across 8 files |
| CF8 | `b22734f` | E4/D13 season-cancel half, SB/SC/SD/SF |
| CF5 | `7c9a7c5` | TC/T5 — three call sites, not the one the brief scoped |

Decisions recorded with them: **D-12** (a dissolve declines both pending and accepted invitations) and
**D-13** (dead statuses are `cancelled` and `expired`; `waitlist` is live). Both in
`ROSTER_RECONCILIATION_PLAN.md` §7.

**Still not started in that cluster:** CF3 (nine status filters), **CF9** (route the inline
`Team.players` appends through the manager — highest value of the remainder, and the only thing
stopping a duplicate roster entry), CF7 (team predelete hook), CF10 + CF11 (stranded solo managers,
blocked on decision **D-8**).

**Fixed earlier:** R1 (season teams "0 players"), T6 (bad DOB 500-ing the tournament roster builder),
H1 (DEBUG logging in `send()`), S5/restore-snapshot hardening.

**Resolved as designed — documented in code, do not re-flag:** tournament format is single-day round-robin
only (TS1 / decision 1); destructive regenerate is the intended board-meeting workflow (decision 2);
jersey Red-vs-White convention with no home/away (S3 / S7).
