Picking Robust now asks how many of the admins have to approve a change
instead of silently assuming a simple majority. The majority is still
where the answer starts; it is just no longer the only one available.
database/model/types/ChatRoomType.kt
* approvalThreshold(adminCount) becomes defaultQuorum(adminCount): same
simple majority, but named for what it now is -- an opening position
rather than the rule.
* MINIMUM_QUORUM = 2. One approval is not a quorum, it is one person
acting alone, which is what CONVENIENT already offers.
* quorumRange(adminCount) = MINIMUM_QUORUM..adminCount -- never fewer
than two, never more admins than exist to approve. Because ROBUST is
gated at MINIMUM_ROBUST_GROUP_SIZE = 3, the range always holds at
least two choices, so the picker is never a control with nothing to
pick.
ui/view/model/SelectChatRoomTypeViewModel.kt
* The fixed approvalThreshold field becomes quorum: MutableState<Int>,
seeded from defaultQuorum(adminCount), alongside the quorumRange the
UI clamps against.
* setQuorum() coerces into quorumRange, so the value cannot escape the
bounds even if the buttons' own enablement is wrong, and freezes once
createdChatRoomId is set -- past creation the governance is already
stamped into the epoch-0 group context, exactly as selectChatRoomType
does.
* quorumExplanation() states what the choice costs day to day: "Any 3 of
you can approve a change -- the other 2 don't have to be around", and
at the top of the range "Every admin has to agree. If one of you goes
quiet, nothing about the group can change." Unanimity is a real
liveness risk and the user should read that before choosing it, not
after.
ui/composable/SelectChatRoomTypeScreen.kt
* ChatRoomTypeCard gains a trailing content slot, and the robust card
fills it with the new QuorumPicker -- but only while robust is the
selected type. Before that there is no decision to make and the
question would be noise.
* QuorumPicker is a stepper, not a text field: the range is small, both
ends are bounded, and a stepper cannot produce a value that has to be
rejected. The -/+ buttons disable at quorumRange.first/last and the
caption re-reads on every step.
* The robust card's own prose drops the hard number -- "approved by a
quorum of you" rather than "approved by 3 of the 5 admins" -- because
the number is now a choice rather than a fact, and the picker is the
thing that states it.
Not done, and called out in the TODO next to the admin list: the chosen
quorum is not persisted. It cannot ride in MarmotGroupData -- MIP-01's
wire format is fixed and an extra field would break byte-compatibility
with mdk/whitenoise -- so it needs a ChatRoom column and the Room
migration off schema version 1 that comes with it. Until then the quorum
is a stated intent sitting beside the admin list, in the same way the
t-of-n enforcement itself is still waiting on FROST signing over admin
changes.
Verified:
./gradlew :composeApp:compileCommonMainKotlinMetadata
./gradlew :composeApp:compileDebugKotlinAndroid
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>