Phase 5 of docs/subgroups.md. `SubgroupManager` orchestrates the four steps and reimplements none of them: step 1 is `ChillDkgRitualManager`, steps 2 and 3 are `FrostSigningManager` sessions, step 4 is Phase 6's. What lives here is the order, the guards, and the reading of what a parent has signed. **There is no table of subgroups, and there should not be.** The certificates *are* the record: the group's own signed statement, checkable without a lookup, and held by every device that followed the signing session rather than only by the signers -- `FrostSigningManager.complete` files a `GroupSignedEvent` everywhere. A table beside them would be a second copy that can disagree. So `subgroupsOf` reads the parent's kind-30329 rows, filters every one through `certifies`, groups by child and keeps the newest. That read has to include children this device has no room for. It is the normal position of a parent member who is not in the subgroup, and of everybody between the certificate being signed and the room being created -- so a list built from rooms would be empty exactly when it is most needed. `Subgroup` carries the room when there is one and null when there is not, and its `name` and `adminPublicKeys` are labelled in the type as the **founding** roster: what the parent approved, not who is in the room now. **Two certificates for one child is a normal outcome, not a conflict.** Two parent admins can press the button on the same admin set; the second gets the first's ceremony back but both may still propose, and both sessions can complete. `GroupSignedEvent` is keyed on the event id so the rows coexist, and because both say the same true thing about the same child, which wins does not matter. `certificateFor` takes the newest that verifies and the `d` tag makes them replacements rather than an accumulation. **`refuseCeremonyRoom` is where the collision from Phase 4 is caught.** A NIP-17 room's id is a pure function of its members, so one admin set gets one ceremony room forever. Four refusals, each phrased as what to do: fewer than three admins (the coordinator counts -- they hold a share whether or not anybody ticked them, so asking for three *others* would quietly build a group of four); somebody who is not in the parent; the whole group, which derives the parent's own ceremony room and would make the "child" this very group; and an admin set that already holds a non-failed ceremony, which is the same trap one step removed. **Two dispatch arms, both of the sort that fail silently if forgotten.** `ChatMessage.applyInnerEvent` gains a 30329 arm -- without it every parent member gets a raw-JSON chat bubble per subgroup, which is exactly the failure mode docs/member-chronicle.md reports for an old build meeting a new kind. It verifies rather than trusts, because it is reached both from a completed session, where the signature is checked, and from an arriving inner event, where a member could have sent a rumor of this kind, and it cannot tell which. Unlike the key-state arm it *does* write a line. A key state is standing state whose session already wrote the transcript; a subgroup being born is something that happened, and it happened on behalf of members of the parent room who are not in the child and will otherwise never learn it exists. `TYPE_SUBGROUP_CERTIFIED` joins the transcript's system-line dispatch and the chat-list preview, since a type missing from either renders as a bubble -- silently, and looking exactly like a member having said it. `ProposedEvent` gains a 30329 summary, or the parent's admins approve "Event of kind 30329" with the JSON underneath. It reads as the name and the member count, which are the two things a signer can actually weigh; the id is on the screen and their device has already checked it derives from the key the certificate names. **30329 is deliberately not chroniclable**, with a test saying so and why. Same shape as the key state: signed by the room, verifies perfectly, and standing rather than work. The argument for admitting it is better -- "P certified C" is a fixed historical fact -- and the cost of leaving it out is real, since a member added to a parent afterwards sees an empty subgroup list. It still needs an apply-order slot and a decision about whether a room's chronicle may carry an event its own key did not sign, which no chroniclable kind does. Thirteen tests in `SubgroupManagerJvmTest` over four real FROST groups, weighted to the negative cases the way the chronicle tests are: a certificate signed by another group, one nobody signed, and one whose id does not derive from the key it names are each filed in the parent's room and refused on read; `record` refuses a forgery and a certificate naming no subgroup; two certificates for one child collapse; two children stay two, newest first; and the four refusals each fire. 397 common tests, 701 jvm tests, and `m3Audit` meets every budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…