Kgothatso Ngako 87ef129de5 fix: archive the translation that stands, not every draft of it
Follow-up to reading the archive out of `GroupSignedEvent` rather than rebuilding
it from rows. The two sources do not hold the same thing, and one place where
they differ reaches the archive.

`ChatMessage.applyInnerEvent` supersedes a translation chunk: retranslating a
passage changes the text and so the event id, so the arm drops the row it
replaces -- newest by the timestamp the group signed at, id breaking a tie. The
record does not, and should not: a signature is the group's statement and
discarding one is not that table's business. So a passage translated three times
leaves one row and three events.

While the archive was rebuilt from rows that difference was invisible, because a
sender simply had nothing but the group's current answer to each passage. Read
from the record it is not: measured on a seeded room, one retranslation leaves
one row and puts **two** payloads in the archive, and it compounds -- every draft
a group ever signed would travel in every archive it ever sends, for as long as
the room exists.

**The rule is the applying arm's, restated rather than approximated.** An archive
that shipped one translation as current while the recipient settled on another
would have both validly signed and nothing downstream to notice they disagree, so
`currentTranslationsOnly` groups by `(translationChapterId, chunkId)` and keeps
the maximum by `(createdAt, id)` -- the same comparison, spelled the same way.

Dropping the drafts is safe precisely *because* the recipient applies that rule
too. This is not what keeps them correct; it is what stops them being sent work
they would discard on arrival.

Grouped per passage rather than per chapter, or retranslating one passage would
take every other passage's translation with it. A translation naming no passage
is left alone rather than lumped in with the rest: it is unappliable either way,
and letting one stand in for a whole passage would let a malformed event suppress
a good one.

Three tests, and all three fail if the filter is removed: a retranslated passage
leaves one row, two recorded events and one payload; three translations signed in
the same second settle on the same id the row keeps, which is what pins the
tiebreak to the applying arm's; and two passages each keep their own, which is
what a group-by-chapter mistake would fail.

Two documentation corrections alongside it. docs/member-archive.md said "a
retranslated passage archives once" as a property of the rows, which stopped
being true the moment the record became the source -- it now says what makes it
true again. And `GroupSignedEvent.verifies()` said a false means the row's
columns have drifted from the event they came from. That is the reading worth
acting on and it is not the only one: a room never derived from its group's key
signs as the bare threshold key rather than as its own id, so a perfectly good
event there fails and cannot be made to pass, the key it would need being absent
from the row and unreachable from one.

498 jvm tests and 297 android unit tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:19:12 +02:00
2026-07-05 23:21:02 +02:00
2026-07-28 09:10:20 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

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 thats 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 Apples 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 youre 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 IDEs 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 IDEs 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 IDEs toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 8.2 MiB
Languages
Kotlin 100%