Kgothatso Ngako eba973331b feat(subgroups): schema 21 -- a parentage is a row, so a room can have two
Phase 2 of docs/subgroups-multiple-parents.md, the storage. Still dark: a lineage of
two now verifies and is filed, and nothing in the app creates one until the adoption
flow lands.

`GroupKeyState` carried `parentChatRoomId` and `birthCertificateJson` because a group
could only be founded with a parent, never adopted by a second. Those are
`GroupKeyStateParent` now, one row per parent, and nothing about what they mean
changed in the move -- including that the parent side is never a foreign key onto
`ChatRoom`, for the three reasons that entity's comment now carries.

**The primary key is the rule.** `(chatRoomId, parentChatRoomId)` is "one certificate
per child per parent", which `SubgroupLineage.of` already refuses to break; the key is
what makes it a property of the store rather than of a reader remembering. The cascade
points at `GroupKeyState`, which is the one cascade here that is wanted: a lineage must
not outlive the state that claimed it, or a room recreated at the same derived id would
inherit a parentage nobody signed for it.

**`replace` rewrites the lineage rather than merging it, and that is why it is a
transaction.** A key state is replaceable and the newest wins *wholesale*, so the set
of parents it names is the set. That is what makes a disavowal expressible at all -- a
room drops a parent by re-announcing without it -- and merged rows would leave the
dropped parent surviving the statement that dropped it. Deleting before inserting
inside one transaction also stops a reader seeing a state beside somebody else's
parents: were the writes separate, a concurrent read could land between them and get a
state with the old lineage, which looks exactly like a true answer. An older state
still changes nothing, now including its parents.

**`stateFrom` returns `LocalGroupKeyState`, because a state and its lineage are one
signed statement.** `lineageOf` checks every parent and drops the whole state if any
one certificate fails -- the all-or-nothing rule generalised, for the reason it was
written: keeping the provable half would file a group as having one parent when its own
members see two. Each link is checked on its own, which is what lets a lineage be
assembled a parent at a time, and there is a comment where somebody will look for the
check that is deliberately absent: nothing requires the certificates to agree on the
set. An earlier certificate cannot name a later co-parent, so requiring agreement would
make every adoption cost a fresh quorum in every group that had already certified the
room. The child's key state is the sole authority.

**`ChatRoom.parentChatRoomId` is dropped, unread.** It was a cache of the verified
value, written by `MarmotGroupCreation` and read in production by nothing at all -- the
live answer went through `GroupKeyState`. Keeping it would mean a column holding *one*
of several parents, which is worse than unused: a cache that is wrong. Its one query,
`ChatRoomDao.observeByParentChatRoomId`, had only tests for callers and goes with it;
the comment left in its place says where the two live answers are. `create`'s
`parentChatRoomId` parameter threaded through four files to reach the write that is now
gone, so it goes too rather than sitting dead.

`MIGRATION_20_21` creates, copies, then drops -- the shape MIGRATION_9_10 established,
and for the same reason it gives: `DROP TABLE` fires cascades and would delete the rows
the migration had just written, so `ALTER TABLE ... DROP COLUMN` is used instead.
`BundledSQLiteDriver` is pinned on every platform, so 3.35 is ours rather than the
host's. The `WHERE` takes both halves of a parentage: a half row should not exist, since
`stateFrom` has always dropped such a state rather than filing it, but this is the one
moment the data is reshaped rather than re-derived and a half row copied forward would
become a parent whose certificate is the string "null". Better to leave it behind -- the
state keeps saying what the room signs with, and loses a lineage it could never prove.

`parentOf` is `parentsOf` and returns a list, through the repository to
`ChatRoomDetailUIState`; the parent card becomes one card per parent, keyed, because
they are a set rather than a path -- neither is "the" parent and the order says nothing
about which came first.

Thirteen tests in `SubgroupDaoJvmTest`, rewritten for the table: the two cascades in
both directions, a child with two parents appearing in both their lists, a
re-announcement that drops a parent and one that clears the lineage entirely, an older
state changing neither the state nor its parents, and the primary key collapsing a
repeat to one row. In `GroupKeyStateTest`, Phase 1's "two parents are dropped" becomes
"two parents are kept, each with its own certificate" -- it needed a third real group,
since the earlier draft's second parent was the child certifying itself -- beside a new
case where one bad certificate of two drops the whole state.

444 common tests, 937 jvm tests, `m3Audit` meets every budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 17:50:57 +02:00
2026-09-09 17:04:09 +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 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…

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