Phase 2 of docs/subgroups.md. Somewhere to put a parent, now that Phase 1 can prove one. | table | column | filled from | trusted? | |---|---|---|---| | GroupKeyState | parentChatRoomId | the state's parent tag | yes -- the certificate was checked | | GroupKeyState | birthCertificateJson | the state's certificate tag | yes -- same | | ChatRoom | parentChatRoomId | the verified key state | yes | | DkgSession | parentChatRoomId | a tag on a ceremony proposal | **no** -- a screen's title | The trust column is the point of the table and is written into the KDoc of each one. Three of these are written only after a signature has been checked; the fourth is an unverified claim off a wire message, and a column that mixed the two would be a column no reader could act on. Nothing may be granted on the strength of `DkgSession.parentChatRoomId` that would not be granted without it. **None of the four is a foreign key, and that is the change most likely to be "fixed" by somebody later.** `GroupKeyState`, `DkgSession` and `GroupSignedEvent` all declare `ForeignKey(onDelete = CASCADE)` onto ChatRoom, so pointing a parent column at ChatRoom the same way is the obvious next move. It would mean deleting a parent room deletes every subgroup row beneath it -- and then, by their own cascades, each subgroup's messages, participants, key state, signing sessions and signed events. A user tidying away a group they had left would silently destroy a group they are still in. RESTRICT is no better: it would make a parent undeletable while any child row exists, which is a foreign key deciding a product question. And neither would work anyway, because a parent pointer routinely names a room this device does not have at all -- a member of a subgroup who was never in its parent holds the id off a certificate and nothing else. A dangling reference is the normal, expected state here, and readers resolve it with a lookup allowed to return null. **The certificate is stored whole, as JSON, rather than as its signature.** A signature plus a rule for rebuilding the event it covers is a rule that breaks silently the first time the certificate's shape changes: a rebuild differing by one byte hashes to an id whose signature fails, and is indistinguishable from a forgery. A few hundred bytes inside an encryption removes the class. The parent column beside it is an index into that event, never a second source of truth -- the two are written together or not at all. Four DAO reads, each with the limits of what it answers written down. `GroupKeyStateDao.getByParentChatRoomId`/`observe` list the children whose state this device holds, which is *verified* but not complete -- a certified child whose room was never created here leaves no state at all. `ChatRoomDao.observeByParentChatRoomId` lists the ones there is something to open, excluding soft-deleted rooms so a room the user cleared away does not reappear because its parent lists it. `DkgSessionDao.getByParentChatRoomId` is how a member gets back into a subgroup flow they closed the app during, since before the certificate is signed the ceremony is the only thing on the device that knows the flow was started. `AutoMigration(16, 17)`: nullable additions are a shape Room migrates itself, and 17.json exports with no new foreign key on any of the three tables. Nine tests in `SubgroupDaoJvmTest`, all on properties the compiler cannot see: a state and a room may each name a parent this device holds no room for; deleting a parent leaves its child, its child's key state and its child's lineage standing; the parent lists its children newest-first and filters on *which* parent rather than on having one; a soft-deleted subgroup drops out; and a ceremony round-trips the parent it was opened for. 386 common tests and 673 jvm tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
177 KiB
177 KiB