A chapter proposal now carries the chapter and a chunk per paragraph, and the group signs the lot at once. Every row a member ends up with is signed: a translation is of a chunk, and a chunk that carries the group's signature over its own words can be checked by anybody holding it, rather than only by re-deriving it from the chapter it came out of. This replaces the derivation two commits ago, which split the chunks out of the signed chapter's text on each device and left them as rumors. That was the right shape when a chunk could only have its own signature by having its own quorum. Batch signing removed that, and this is the other side of the trade `MantraChunk.chunksOf` was weighed against. **A batch whose items name each other.** A chunk carries its chapter's id, and that id is a hash over the group's key at the room's derivation path -- neither resolved until the proposal runs. A caller computing it would be recomputing `signingPath`, the one input in this protocol that must never come from a proposer, since the path decides which key the group signs as. So `proposeSigningBatch` gains a second form: a `lead` template, and a `dependents` builder handed the lead *after* it is authored, returning the events that reference it. Every id still comes out of `unsignedEventOf`, which makes an item naming a chapter nobody signed something that cannot be built rather than something to be tested for. `AddChapterViewModel` passes `ChunkEvent::splitOf` and nothing else. The lead is item 0. Items apply in `itemIndex` order and a chunk row whose chapter does not exist yet is a foreign key violation, so what is referenced is signed first as well as named first. **The cost, in front of whoever is typing.** `MAX_BATCH_SIZE` is 64 and the chapter takes one place, so a chapter is capped at 63 paragraphs and a longer one has to be split in two. That is a real limit on real prose. The form counts chunks against the cap as the text is typed, colours the count when it is past, says what to do about it, and will not propose -- because the alternative is an IllegalArgumentException after the fact. The manager still refuses independently; the screen is not what enforces it. **What went away.** `MantraChunk.chunksOf` and the derivation it did inside `ChatMessage.applyInnerEvent`. Chunks arrive as their own signed events now and go through the `ChunkEvent.KIND` branch that was always there. `ChapterEvent` still carries the whole text beside chunks that hold the same words: chunk boundaries are a decision about how to divide the work, and a chapter that kept only the pieces could never be divided differently again. **Tests.** `ChapterChunkSplitTest` covers the split as a pure function -- what each chunk names, counts and carries. `SignedChapterTest` signs a real batch, one FROST instance per item, and checks every chunk row is authored by the room and carries a signature over its own id. `ChapterBatchProposalJvmTest` runs the real proposal against a real database, which is where the sharp edge is: item order, the chunks naming the chapter as the group will author it, and both ends of the cap -- 63 paragraphs proposes, 64 is refused and leaves no session behind. Checked against broken implementations: putting the lead last, naming the wrong chapter, and stamping the chunks off the clock are each caught, in both suites. `jvmTest` runs on linux again as of the merge, which is what made the database-backed test possible. Dropped a nonce-reuse test that was in the first draft of this: it asserted over its own fixture, and `FrostSigningRoundTest` and `SignedGroupKeyStateTest` already hold the manager to giving every item its own nonce. 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…