feat(join): offer every way in to a vetting community at once - #342
Merged
Merged
Conversation
The join flow already asked a community what it required before anything about the applicant was sent. What it did with the answer was print the requirements and leave the applicant to work out what to do with them — so "this community vets" read as "go and get vetted", even to someone holding an invitation that would have admitted them without it. The vetting page now lists the ways in as a chooser: present an invitation, be vetted, or send an open request. Each row says whether it can be taken and, when it cannot, why — a blocked route keeps its place and reads dim with the reason where its detail would be, because *why not* is the question the page exists to answer. Taking a blocked route says the same thing rather than doing nothing (issue #29's rule). Which way in is open depends on the community's manifest *and* on what this account holds, so the community's usable invitations are collected before the routes are drawn rather than two steps later on the identity step. `ensure_invitations` does it once per community, keyed on a marker rather than on the list being empty — holding none is an answer. Two shapes the manifest distinguishes and the page now does too: an invitation marked `required` is asked for on top of the statements, not instead of them, so it is left to the requirement bullets and the vetting row's detail rather than offered as a row that reads like a way around vetting; and a community that admits nobody by invitation says so, even to someone holding one. Also: a first join could not ask the community anything at all. The State-A loop has no inbound arm, so there was no reply to hear — and the flow fell through to an open request in silence, which a vetting community refers to its moderators. It now says the community was not asked and why, and withholds "ask again" there rather than binding a key that could only ever fail. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
This was referenced Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Joining a community that vets read as a dead end. The flow already asked the
community for its manifest before anything about the applicant was sent — but
all it did with the answer was print the requirements, so "this community vets"
came across as "go to the Vetting page first", even to someone holding an
invitation that would have admitted them without being vetted at all.
The invitations were the specific miss: they were only collected in
continue_join, after the vetting decision, so the page that told you thecommunity vets had no way of knowing you already held a way past it.
What changed
The vetting page is a chooser, not a requirements sheet. It lists the ways
in — present an invitation, be vetted, send an open request — with the cursor on
the first one that can actually be taken. ↑/↓ moves, Enter takes it;
Aopensthe application and
Jjoins now, as before.A route that cannot be taken keeps its row. It reads dim with the reason
where its detail would be ("none held for this community", "Kernel does not
admit anyone by invitation"). Hiding it would leave why not unanswered, which
is the question the page exists to settle. Pressing Enter on one says the same
thing rather than doing nothing — issue #29's rule.
Invitations are collected before the routes are drawn.
ensure_invitationsdoes it once per community, keyed on a marker on the join state rather than on
the list being empty: holding none is a legitimate answer, not a cache miss.
continue_joinand the resume-from-manifest path go through it too, so thevault listing does not happen twice on a path that already did it.
Two manifest shapes the page now distinguishes:
invitation: "required"is asked for on top of the statements, so it gets norow of its own — it stays in the requirement bullets and in the vetting row's
detail. A row would read as a way around being vetted.
invitation: "none"blocks the row with that reason, even for someone holdingone, rather than offering something the community will refuse.
An open request admits that statements ride with it whenever the joining
persona has gathered any, because the submit attaches them without asking.
Fixed
A first join never asked the community anything. The State-A degraded loop has
no inbound arm — a documented invariant — so
hears_repliesisfalsethere andthe manifest request could not be made; the flow then fell through a
_armstraight to an open request, in silence. A community that vets refers that
request to its moderators, and the applicant never learns there was a way in they
could have taken. The page now says the community was not asked and why, and
withholds "ask again" there instead of binding a key that could only ever fail.
Not in this PR
Choosing to be vetted still leaves the join:
JoinVettingApplystarts theapplication and returns to the Vetting panel, so the community has to be entered
again afterwards. Suspending and resuming the join is the follow-up.
Testing
cargo fmt --all,cargo clippy --all-targets --all-features— cleancargo test --all-features -- --include-ignored— all green, including theMockVta e2es only CI's coverage job normally runs
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features— cleanNew tests cover the routes builder (invitation in hand is first and available;
holding none keeps the row with its reason;
requiredyields no standalone row;noneblocks it even while holding one; applying needs a persona but an openrequest does not; an open request names the statements that go with it) and the
page's keys (Enter takes the highlighted route whatever it is, and "ask again" is
neither drawn nor bound when no answer could be heard).
Live run against a vetting community not yet done.