diff --git a/composeApp/src/commonTest/kotlin/press/mantra/compose/database/model/TranscriptRequestStateTest.kt b/composeApp/src/commonTest/kotlin/press/mantra/compose/database/model/TranscriptRequestStateTest.kt index 4dbf4788..3905f187 100644 --- a/composeApp/src/commonTest/kotlin/press/mantra/compose/database/model/TranscriptRequestStateTest.kt +++ b/composeApp/src/commonTest/kotlin/press/mantra/compose/database/model/TranscriptRequestStateTest.kt @@ -182,4 +182,42 @@ class TranscriptRequestStateTest { assertEquals(setOf(hostKey.id), ChatMessage.answeredRequests(transcript)) } + + @Test + fun `twelve open proposals each keep their own summons`() { + // An upload of twelve files opens twelve sessions at once, interleaved in + // one stretch of transcript. The clock fallback would have the first + // answer drop every later request, which is the failure two concurrent + // sessions already had once -- and twelve is where a path that still + // falls back to the clock would show. + val requests = (1..12).map { n -> + line(ChatMessage.TYPE_FROST_APPROVAL_NEEDED, at = 100L + n, session = "session-" + n) + } + + // Answered in the opposite order to the asking, so neither the clock nor + // the list order could produce the right answer by accident. + val answers = listOf(12, 7, 1).map { n -> + line(ChatMessage.TYPE_FROST_NONCE, at = 200L + n, session = "session-" + n) + } + + val answered = ChatMessage.answeredRequests(requests + answers) + + assertEquals( + setOf(requests[11].id, requests[6].id, requests[0].id), + answered, + ) + } + + @Test + fun `one proposal failing settles only itself`() { + val requests = (1..12).map { n -> + line(ChatMessage.TYPE_FROST_APPROVAL_NEEDED, at = 100L + n, session = "session-" + n) + } + val failure = line(ChatMessage.TYPE_FROST_FAILED, at = 300, session = "session-5") + + // The other eleven chapters are still in front of the group. A batch is + // all-or-nothing within a session and nothing across them, which is the + // whole reason an upload is N sessions rather than one. + assertEquals(setOf(requests[4].id), ChatMessage.settledRequests(requests + listOf(failure))) + } } diff --git a/docs/chapter-uploads.md b/docs/chapter-uploads.md index eee3aa51..d5695041 100644 --- a/docs/chapter-uploads.md +++ b/docs/chapter-uploads.md @@ -615,23 +615,39 @@ The transcript and the proposal list were built for two concurrent sessions — scaffolding were open together. Twelve is the same problem at a scale that will find whatever that fix missed. -What to check rather than assume: +What to check rather than assume — and **all three already held**, so this +phase is its tests and this paragraph: -- `ProposalListScreen` with twenty open sessions: the ones waiting on the reader - gather at the top, and `ProposedEvent.summarize` gives a chapter a name rather - than "Event of kind 30302". Chapters are summarised today; confirm the - summary is the chapter's *name*, since twelve rows reading "Chapter" are - twelve rows a member cannot choose between. -- Twelve "Review" lines in the transcript, each resolving its own session. - `answeredRequests`/`settledRequests` read `frostSigningSessionId` when both - rows have one; twelve is where a fallback-to-clock path would show. -- Twelve approval gates. No batching of the gate — that is the one thing the - batch-signing note refuses, and for the same reason: "a member who cannot - scroll the whole list should not be able to approve it". What can be done is - make the *list* cheap to work through, which is the point above. +- **The list already groups.** `ProposalListScreen` renders a "Waiting for you" + header over the sessions `FrostSigningManager.isAwaitingApproval` picks out and + an "Everything else" header under them, over a query that is already + `ORDER BY createdAt DESC`. Twelve proposals arrive sorted and the three that + need the reader are at the top. +- **The summary already names the chapter.** `ProposedEvent.summarize`'s chapter + branch is `name · N words · N chunks`, not "Chapter" — so twelve rows are + twelve distinguishable rows. +- **`isAbout` already matches on the session.** `answeredRequests` and + `settledRequests` compare `frostSigningSessionId` when both rows carry one and + fall back to the clock only when either does not. Now tested at twelve, with + the answers given in the opposite order to the asking so that neither the + clock nor the list order could produce the right answer by accident, plus one + failure settling only its own row. +- **Twelve approval gates, unbatched.** Unchanged by design: the gate shows every + event rather than a count, because "a member who cannot scroll the whole list + should not be able to approve it". No new `ChatMessage` type, no new set to add to. +### One thing N does make worse, and it is Phase 8's to fix + +`observeSessionsForChatRoom` carries every session's items, and a chapter item's +`unsignedEventJson` holds the whole markdown. Twenty open chapter proposals is +roughly 900 KB of JSON parsed and summarised **on every emission** — and +`summarize` then re-splits each chapter's text to count its chunks, which is the +derived count Phase 8 replaces with the session's own item count. Fixing it +there removes the re-split; the parse is inherent to the query and is the price +of the list showing what each proposal signs without one query per row. + --- ## Phase 7 — the splitter, which is a decision about translation