docs: record what the adoption plan built, and the eight places it chose differently
The note shipped as research and is now five commits of code, so it says so, in the shape docs/subgroups.md uses: the phases kept as written because they are the reasoning, and a table of where the build disagreed with them. Eight entries, and two are worth more than the rest. **A group can sign, truthfully by all six of `certifies`' questions, that it is its own parent** -- author, subject and named parent one room, the id deriving from the key, the signature verifying. The note never considered it. The self-parent guard began inside the offer branch, where it never ran, because the child branch catches such a certificate first and reads it as an ordinary child. A test caught it, and the gap predates this work: `subgroupsOf` would list the group as its own subgroup today. **The certificate's roster.** The note was careful that a founding certificate's name and `p` tags are the founding roster, frozen, and said nothing about adoption -- where there is no founding. The adopting group's admins are being asked about a group that exists, so what they can weigh is who runs it now, and the current roster is the honest value rather than a compromise. Still frozen the moment it is signed, still never read as live. The rest are smaller and all one shape, which is a note counting a thing and missing its thread: dropping `ChatRoom.parentChatRoomId` also drops the parameter that travelled through four files to reach its only writer; `stateFrom` returning "a state and a lineage" wanted a named type, and `LocalGroupKeyState` is a Room read model that is also a plain data class, so the pure verifier builds one with no database near it. One thing the note asked for is **not** done and cannot be: the key-state summary saying which parents are being *dropped*. A summary is pure and a disavowal is a difference against the row this device holds, so it belongs to the screen. The rough-size table becomes estimated against actual. It guessed about two dozen files and it was 39, with the UI three times its estimate -- a picker needs a route, a screen, a view model and a state, which the note had as one line -- and the repositories half of it, because `DkgRepository`'s changes were return types it never counted. Tests: 7 files, 53 new cases. `docs/README.md` moves the note from research to a phased plan that has been built, and its one-line summary now ends on the self-certificate rather than on the consent gap, which is closed. Docs only -- no code changed, so there are no test counts of this commit's own. The five it describes: 454 common tests, 962 jvm tests, `m3Audit` meeting every budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ silent, or a decision that looked arbitrary and was not.
|
||||
| [shared-key-ceremony.md](./shared-key-ceremony.md) | ChillDKG over NIP-17: the rounds, the approval gates, the chat transcript, participant ordering |
|
||||
| [shared-key-derivation.md](./shared-key-derivation.md) | deriving further keys from the group's threshold key with FROST tweaks — why not BIP32, why no chain code, and the one rule that must not be broken |
|
||||
| [subgroups.md](./subgroups.md) | a group making another group — the four ceremonies, what the parent's signature actually covers, and why the child's key is fresh rather than derived |
|
||||
| [subgroups-multiple-parents.md](./subgroups-multiple-parents.md) | adding a parent to a group that already exists — two signings and no ceremony, why the child's consent is the whole gate, and the one screen that currently asks without saying what it is asking |
|
||||
| [subgroups-multiple-parents.md](./subgroups-multiple-parents.md) | adding a parent to a group that already exists — two signings and no ceremony, why the child's consent is the whole gate, and the certificate a group can truthfully sign about itself |
|
||||
| [frost-batch-signing.md](./frost-batch-signing.md) | signing several events in one ceremony — why one nonce can never cover two messages, and the phased schema, wire and UI work that follows from it |
|
||||
| [marmot-membership.md](./marmot-membership.md) | how members join an MLS group, and the epoch race that makes a missing member look like a successful invite |
|
||||
| [member-chronicle.md](./member-chronicle.md) | handing a member added after the work was done the group's signed record — why the events are not on the wire at all, and why the room's id is enough to verify them |
|
||||
@@ -40,8 +40,8 @@ jvm-target note is unrelated to all of them: it is a build and packaging story.
|
||||
The subgroups note is a phased plan that has been built; it assumes both
|
||||
shared-key notes and reads as the ceremony's second half — what a group does
|
||||
once it has a key, and what it can say about a group that does not yet. The
|
||||
multiple-parents note is research rather than a plan, and reads only after it:
|
||||
it takes the one assumption that note never had to state — that a lineage is
|
||||
multiple-parents note is a phased plan that has been built, and reads only after
|
||||
it: it takes the one assumption that note never had to state — that a lineage is
|
||||
settled at founding — and finds that dropping it costs less than founding a
|
||||
subgroup does, because no ceremony is involved.
|
||||
The Material Design note is a phased plan that has not been built, and is the
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
# Adding a parent to a group that already exists
|
||||
|
||||
What it would take for a group to gain a second parent after it was founded.
|
||||
Research, not a plan: nothing here has been built.
|
||||
How a group gains a second parent after it was founded.
|
||||
|
||||
**Built**, five phases, one commit each. The phases below are kept as written
|
||||
because they are the reasoning, and the code reads better against the argument it
|
||||
came from than against a summary of itself. Where the implementation chose
|
||||
differently the section says so, and it did so eight times worth reading:
|
||||
|
||||
| what the note said | what it turned out to be |
|
||||
|---|---|
|
||||
| the 30329 arm "gains a second branch" | a pure `SubgroupManager.classify` in front of it. The arm cannot tell its two callers apart and the decision wanted testing without a database, which `certifies` makes possible -- it checks a signature against a room id and needs no lookup |
|
||||
| nothing about a group certifying itself | **a real gap, found by a test.** A group can sign, truthfully by all six of `certifies`' questions, that it is its own parent. The self-parent guard began inside the offer branch, where it never ran: the child branch catches such a certificate first and reads it as an ordinary child, which would list a group as its own subgroup. It predates this work -- `subgroupsOf` would have shown it |
|
||||
| drop `ChatRoom.parentChatRoomId` | and the `parentChatRoomId` parameter threaded through four files to reach the write that was its only writer. The note counted the column and missed the thread |
|
||||
| `stateFrom` returns a state and a lineage | `LocalGroupKeyState`, a Room read model that is also a plain data class -- so the pure verifier builds one with no database near it, and `@Relation` serves the DAO |
|
||||
| the certificate's roster is the founding roster, frozen | true, and silent about adoption, where there is no founding. The adopting group's admins are asked about a group that *exists*, so the roster and name are its **current** ones -- still frozen the moment they are signed, still never read as live |
|
||||
| the summary says which parents are being dropped | not done, and it cannot be: a summary is pure and a disavowal is a difference against the row this device holds. It belongs to the screen, which has the room, and there is a comment where somebody will look |
|
||||
| `EmptyState` takes a supporting line | it does not. The reason folded into the message, which is the whole content of that absence anyway |
|
||||
| `inComparableGroups` moved to the key extensions | in the fork's tree, not this one, where it is still private to `ChatRoomDetailScreen`. A second copy to add three spaces to an id is not worth having |
|
||||
|
||||
Two things the note got right that are worth keeping if any of this is rewritten.
|
||||
Emitting the parentage tags **pairwise** rather than as two runs, which is the one
|
||||
decision here made entirely for builds that do not exist yet -- and which has a
|
||||
test asserting the tag order, not just the round trip. And `replace` rewriting a
|
||||
lineage rather than merging it, which is what makes a disavowal expressible at all
|
||||
and is the reason that method is a transaction.
|
||||
|
||||
**Not reachable yet:** `proposeDisavowal` is built and tested and has no button.
|
||||
Dropping a parent is a destructive-looking act that wants a confirmation step, and
|
||||
it belongs in its own change.
|
||||
|
||||
Written against the code as it stands at `e15d31b8`.
|
||||
|
||||
Read [subgroups.md](./subgroups.md) first. This extends it and contradicts it in
|
||||
one place — the assumption, never stated because it never had to be, that a
|
||||
@@ -757,15 +785,19 @@ description is baked into the epoch-0 group context and adoption happens after i
|
||||
|
||||
## Rough size
|
||||
|
||||
| | files | notes |
|
||||
|---|---|---|
|
||||
| wire | 1 | `GroupKeyStateEvent` — list instead of nullable single, in three functions |
|
||||
| lineage type | 2 | `SubgroupParentage` kept as one pair; a `SubgroupLineage` holding the set and its invariants |
|
||||
| schema | 5 | `GroupKeyState`, a new entity, `GroupKeyStateDao` (transactional `replace`), `ChatRoomDao`, `MantraDatabase` + migration |
|
||||
| managers | 2 | `GroupKeyStateManager` (`parentageOf`, `propose`), `SubgroupManager` (`parentsOf`, the two guard sets, adoption proposals) |
|
||||
| repositories | 4 | two interfaces, two implementations, mostly signature plurals |
|
||||
| text | 2 | `ProposedEvent`'s 30326 arm — the consent fix — and `ChatMessage`'s 30329 arm, for the offer line |
|
||||
| UI | 5 | `ChatRoomDetailUIState`, its view model and screen, a parent picker, strings |
|
||||
| tests | 8 | |
|
||||
What it estimated, against what it took:
|
||||
|
||||
Around two dozen files, and the founding flow is not among them.
|
||||
| | estimated | actual |
|
||||
|---|---|---|
|
||||
| wire | 1 | 2 — `GroupKeyStateEvent` plus the `SubgroupLineage` type beside it |
|
||||
| schema | 5 | 7 — two new files (`GroupKeyStateParent`, `LocalGroupKeyState`), the migration, two DAOs, `MantraDatabase`, the builder |
|
||||
| managers | 2 | 3 — `MarmotGroupCreation` as well, losing the parameter nothing fed any more |
|
||||
| repositories | 4 | 2 — one interface and one implementation; `DkgRepository`'s changes were return types the note did not count |
|
||||
| text | 2 | 2 |
|
||||
| UI | 5 | 10 — a route, a screen, a view model and a UI state for the picker, plus the detail screen's four and the strings |
|
||||
| tests | 8 | 7 files, 53 new cases |
|
||||
|
||||
39 files, and the founding flow is not among them: a subgroup is still founded with
|
||||
one parent, and the second arrives by adoption.
|
||||
|
||||
454 common tests, 962 jvm tests, `m3Audit` meets every budget.
|
||||
|
||||
Reference in New Issue
Block a user