Files
mantra-kmp/composeApp
Kgothatso Ngako 9b3044f6fb feat: let robust groups choose their own quorum
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>
2026-08-29 16:53:23 +02:00
..