feat(concord): assign CORD-04 roles from the Members roster

`Account.grantConcordRole` had been implemented with zero callers, so role
grants were unreachable from the app while the changelog claimed they ship.
This adds the missing surface: a "Roles…" item beside "Make admin" opening a
multi-select over the roles the viewer may hand out.

Both rank rules are enforced by delegating to `AuthorityResolver` rather than
reimplementing them:

- assignable roles are `roles().filter { myRank < it.position }` — the fold
  drops a grant whose granter does not strictly outrank every assigned role, so
  offering one at or above our own position would publish an edition that every
  client then silently discards;
- reachable members are `authority.canActOn(me, target, MANAGE_ROLES)`, which
  already folds the whole rule (hold the bit, not banned, target isn't the
  owner, strictly outrank) and makes self-promotion fall out for free.

Out-of-reach members show the item disabled *with a reason* instead of omitting
it, so there is no silently no-op control.

The grant REPLACES a member's role set rather than merging into it, so the
dialog preselects their current roles. That preselection is provably complete:
a member's rank is the lowest position they hold, and the dialog only opens
when we strictly outrank that rank, so every role they hold sits strictly below
us and is therefore rendered — no held role can be silently stripped.

`amy concord roles` also gained a `grants:` section reading the post-fixpoint
`authority.roleHolders()`. It previously printed role *definitions* but never
the *grants*, which made the fold outcome unverifiable from the CLI; a
rank-violating grant now shows up as visibly absent rather than as if it landed.

Device-verified on a test community (Admin/QA Lead/Helper/Greeter): the picker
hides roles above the viewer, disables on members who outrank them, preselects
correctly, and a saved grant survived the fold and a fresh relay drain read
back from a second client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-07-20 11:35:48 -04:00
co-authored by Claude Opus 4.8
parent 97b861dd5e
commit 4e7242a295
4 changed files with 180 additions and 3 deletions
@@ -59,6 +59,17 @@ object ConcordModCommands {
state.roles.map { (id, r) ->
mapOf("id" to id, "name" to r.name, "position" to r.position, "permissions" to r.permissions)
},
// The role-holder roster AFTER the authority fixpoint, so a grant that was
// published but dropped on fold (granter didn't outrank the role or the member)
// is visibly absent here rather than looking like it landed.
"grants" to
state.authority.roleHolders().sorted().map { member ->
mapOf(
"member" to member,
"rank" to state.authority.rank(member),
"roles" to state.authority.rolesFor(member).map { it.name },
)
},
"banned" to ConcordModeration.currentBanned(editions, sc.communityId.hexToByteArray(), sc.owner).toList(),
),
)