feat(teams): resolve team dividers by role ID instead of name

Divider placement matched the divider role by its exact Discord name, so
renaming the role in Discord silently broke positioning. Switch the
TEAM_DIVIDER_<SUFFIX> config to hold a role ID; resolve the ID to the
role's current name in apply_team_role_positions and keep the existing
name-based ordering maths downstream unchanged.

- config._parse_team_dividers now parses values as ints (rejects non-ints)
- resolve_divider / _team_divider cache / get_team_dividers return IDs
- apply_team_role_positions resolves each ID via guild.get_role once
- .env.example documents IDs and ships the CS2/LoL divider role IDs
- resolve_divider tests updated to assert IDs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPRsW4tazVtYi2jNzVQkre
This commit is contained in:
Rene Arumetsa
2026-09-03 22:34:34 +03:00
parent deab0f7a55
commit d48a436e26
5 changed files with 64 additions and 40 deletions

View File

@@ -280,7 +280,7 @@ _LINEUP_HEADER_PREFIX = "lineup"
_team_roster: dict[str, list[str]] = {} # team name -> [Discord username, ...]
_team_by_username: dict[str, str] = {} # normalized username -> team name
_team_names: set[str] = set() # universe of all team names
_team_divider: dict[str, str] = {} # team name -> divider role name
_team_divider: dict[str, int] = {} # team name -> divider role ID
@dataclass(frozen=True)
@@ -390,8 +390,8 @@ def parse_team_rosters(rows: list[list]) -> dict[str, list[str]]:
return rosters
def resolve_divider(title: str, dividers: dict[str, str] | None = None) -> str | None:
"""Return the divider role name configured for a section title, if any.
def resolve_divider(title: str, dividers: dict[str, int] | None = None) -> int | None:
"""Return the divider role ID configured for a section title, if any.
A ``TEAM_DIVIDER_<SUFFIX>`` entry matches when every underscore-separated
part of its suffix appears as a whole word in the title, so ``CS2`` matches
@@ -402,15 +402,15 @@ def resolve_divider(title: str, dividers: dict[str, str] | None = None) -> str |
if dividers is None:
dividers = config.TEAM_DIVIDERS
haystack = title.lower()
best_name: str | None = None
best_id: int | None = None
best_parts = 0
for suffix, role_name in dividers.items():
for suffix, role_id in dividers.items():
parts = [p for p in suffix.split("_") if p]
if not parts or len(parts) <= best_parts:
continue
if all(re.search(rf"\b{re.escape(p)}\b", haystack) for p in parts):
best_name, best_parts = role_name, len(parts)
return best_name
best_id, best_parts = role_id, len(parts)
return best_id
def build_username_index(rosters: dict[str, list[str]]) -> dict[str, str]:
@@ -441,7 +441,7 @@ def _refresh_teams_sync() -> dict[str, list[str]]:
spreadsheet = client.open_by_key(config.TEAM_SHEET_ID)
rosters: dict[str, list[str]] = {}
dividers: dict[str, str] = {}
dividers: dict[str, int] = {}
for ws in spreadsheet.worksheets():
for section in parse_team_sections(ws.get_all_values()):
divider = resolve_divider(section.title)
@@ -483,8 +483,8 @@ def get_team_rosters() -> dict[str, list[str]]:
return _team_roster
def get_team_dividers() -> dict[str, str]:
"""Current {team: divider role name} cache.
def get_team_dividers() -> dict[str, int]:
"""Current {team: divider role ID} cache.
Only teams whose section title matched a configured TEAM_DIVIDER_* entry
appear here, so an empty dict means positioning is switched off.