A guild without its @everyone role (or that role's permissions) was read
as base 0, a definite deny that would block a start. It now raises
ValueError, which S3 treats as unknown and lets the start go ahead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New pure module app/internal/discord_perms.py:
- effective_permissions(guild, member, channel) -> (bits, trace), in
Discord's order: owner, base (@everyone | member roles), ADMINISTRATOR,
@everyone overwrite, role overwrites (all denies then all allows),
member overwrite, no View Channel = 0, no Connect drops voice perms
(as discord.py does, so labels match the edge), timeout keeps only
View + Read History.
- trace names what removed each radio permission, e.g. "denied by
#dispatch's override for @Muted".
- check_voice(bits, channel, trace=None) -> {ok, is_voice, missing,
reasons, user_limit, limit_bypass}; only type-2 channels count; labels
match node-26#38's _join_refusal ("View Channel", "Connect", "Speak").
- Permission strings parsed with int (values above 2**53 tested);
malformed values raise ValueError so callers treat them as unknown.
No caller yet; S3 wires it into pick_token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>