Nordhavn / specs / convene / core / Group

convene / coreprojection

Group

Purpose

The group as its members think of it — one membership, one current key — assembled from the epoch revisions the member has been able to unseal from their inbox. Because membership travels sealed under each epoch's key and is dropped only into the inboxes of that epoch's members, the revisions a member holds are exactly the epochs they were let into: a founder holds the whole chain, a later joiner holds only a suffix, and that shorter reading is not an error but their honest view. The revisions arrive in no promised order; they are the players here, and their interaction — each revision naming the one it succeeds — is what assembles the group before anything is answered. The assembly is also the guard: a lineage is a chain, never a tree, and only the founder mints — so two revisions naming the same parent are not a merge to attempt but substitution evidence, refused with the fork as proof; and revisions of two different lineages are no group at all, refused rather than ignored. Over the assembled chain the group answers what members ask — who is in it now, which epoch is current, whom its reader has ever seen, and the epochs themselves, eldest first: what a seat reads by, and the epochs a conversation's copies travel under. The group a member can assemble is the access model in one reading — the revisions they hold carry exactly the key ids they can ever read.

Role Players

Role Methods

revisions

Functionalities

epochs

Yields the group's epochs — the GroupRevisions assembled eldest first: what a seat reads by, and the epochs a conversation's copies travel under. Result: the GroupRevisions assembled eldest first; fails when the lineage forks or mixes

founder

Reports the founder's member name — the sub the eldest held revision records as its creator, which is the founder's on every revision, because only the founder mints. Result: the founder's member name as Text; fails when the lineage forks or mixes

current members

Reports the member names of the current revision — the group as it stands. Result: each member name as Text; fails when the lineage forks or mixes

current revision

Reports the latest GroupRevision — the assembled chain's end, the epoch new messages are sealed under. Result: the latest GroupRevision; fails when the lineage forks or mixes, and when no revision is held

seen members

Produces the Members named across the assembled chain, each once, in the order they first appear — every epoch's roster, not only the latest: a message from a closed epoch is still read, and its speaker still has a name. Result: the Members named across the chain, each once, in first-appearance order; fails when the lineage forks or mixes

Scenarios

Delivery order is not epoch order

Given: revisions delivered as "g-7421.2" (parent "g-7421", members "anne.bruun" and "sofie.lind", key "k-0150") then "g-7421" (no parent, members "anne.bruun", key "k-0142") When: epochs is asked Then: the epochs read "g-7421" then "g-7421.2" — assembled by parent references, not by arrival

A later joiner's suffix still assembles

Given: revisions "g-7421.4" (parent "g-7421.3", members "anne.bruun" and "sofie.lind", key "k-0264") and "g-7421.3" (parent "g-7421.2", members "anne.bruun", "sofie.lind", and "karin.dahl", key "k-0163") — a suffix whose eldest names a parent no held revision answers to When: epochs is asked Then: the epochs read "g-7421.3" then "g-7421.4" — the eldest held revision is the one whose named parent none of the others answers to, and the shorter reading is the joiner's honest view, not an error

The group as it stands after a removal

Given: revisions "g-7421" (members "anne.bruun", key "k-0142"), "g-7421.2" (members "anne.bruun" and "sofie.lind", key "k-0150"), "g-7421.3" (members "anne.bruun", "sofie.lind", and "karin.dahl", key "k-0163"), and "g-7421.4" (members "anne.bruun" and "sofie.lind", key "k-0264"), in any order When: current members and current revision are asked Then: the current members are "anne.bruun" and "sofie.lind", and the current revision is "g-7421.4" under key id "k-0264"

The founder is the founding revision's creator

Given: revisions "g-7421" (members "anne.bruun", key "k-0142", created by "anne.bruun") and "g-7421.2" (members "anne.bruun" and "sofie.lind", key "k-0150") When: founder is asked Then: the answer is "anne.bruun"

The reader's seen members span the epochs

Given: revisions "g-7421" (members "anne.bruun", key "k-0142"), "g-7421.2" (members "anne.bruun" and "sofie.lind", key "k-0150"), "g-7421.3" (members "anne.bruun", "sofie.lind", and "karin.dahl", key "k-0163"), and "g-7421.4" (members "anne.bruun" and "sofie.lind", key "k-0264") When: seen members is asked Then: the answer is anne.bruun, sofie.lind, and karin.dahl, each once, in the order they first appear — karin.dahl's removal since does not unname her

An empty group has no current revision

Given: no revisions When: current revision is asked Then: it fails because no revision is held — there is no epoch to stand as current

A forked lineage is refused as evidence

Given: revisions "g-7421" (members "anne.bruun", key "k-0142"), "g-7421.4" (parent "g-7421", members "anne.bruun", key "k-0264"), and "g-7421.4b" (parent "g-7421", members "anne.bruun", key "k-0271") When: epochs is asked Then: the assembly fails because two revisions name the parent "g-7421" — a substitution Alarm carries the fork as evidence

Two lineages are no group

Given: revisions "g-7421" (no parent, lineage "g-7421", members "anne.bruun", key "k-0142") and "g-9001" (no parent, lineage "g-9001", members "karin.dahl", key "k-0301") When: epochs is asked Then: the assembly fails because the revisions name different lineages — a stray epoch is refused, not ignored

spoken of by