The refusal `679d5249` added is gone, and with it the last reason it existed.
**The per-viewer field had to go first.** `ChatMessage.isUserMessage` was a boolean
about one reader stored on a row that belongs to the group: a transcript line is
deduped by the group event behind it, and `reconcileMarmotLine` lets the fresher row
win, so whichever profile indexed the event last decided whose message it looked
like. A member's own messages would draw as somebody else's and flip back when the
other profile caught up -- on every line in the room.
It is `senderIdentity` now, and `ChatMessage.isFrom(userPublicKey)` asks the
question. The identity is the fact about the message; whether it is yours is a
question only a reader can ask. Not derivable from `senderPublicKey`, which for a
Marmot line is the kind:445's ephemeral signer -- the same trap the comment on
`fromGroupEventResult` already records someone falling into. `answeredRequests` takes
the asking profile for the same reason: the other profile's approval is not this
one's to be credited with.
The backfill is exact, and exact for the reason the bug could not happen yet: the
boolean was only safe to store because a room belonged to one profile, so a line
marked as the user's was sent by that room's one member, and `ChatRoomMembership`
says who. `isUserMessage` is named by no index or constraint, so `DROP COLUMN` takes
it and `ChatMessage`'s three cascading children are untouched.
**Removing the refusal was not enough.** The Welcome branch keyed on the *room*, so a
Welcome that found one fell through and did nothing -- the accidental silent refusal
that `679d5249` was written to make legible in the first place. It asks about this
profile's membership now, and the body it guards already does the right thing for a
joiner: the room's upsert is idempotent, the bundle consumed is their own, the roster
is the group's, and `adopt` files a statement the group already signed.
`TYPE_WELCOME_REFUSED_OTHER_PROFILE` stays, written by nothing and still rendered.
Devices that ran the older build hold lines of that type, and a transcript that
cannot draw a line it is holding is worse than one drawing an obsolete notice.
`WelcomeForOtherProfileJvmTest` becomes `SecondProfileJoinsGroupJvmTest` and asserts
the opposite of what it used to: two memberships of one room, two distinct ratchet
states, the holder's untouched, no refusal line, and the key package spent like any
other join's.
1216 tests, 0 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Takes the projects line, the chapter uploads, the tablet room selection and the
adoption fixes -- thirty-eight commits, 204 tests.
**Renumbered, for the third time in this window.** `mantra` reached 22 with the
projects tables, having itself renumbered from 21 after the parentage table took
that; this branch had already written 22 and 23. So the ritual keying becomes 22->23
and the membership split 23->24, by the rule in docs/curated-to-mantra-profiles.md
that a version belongs to whichever tree lands first. Nothing had shipped on either.
Schema 23 had to be regenerated rather than kept: the old 22.json described
`mantra@21 + the ritual keying`, and the intermediate shape is now
`mantra@22 + the ritual keying`, which includes the three project tables. Exported by
putting the entities briefly back to that shape, since Room only ever emits the
current version.
**Projects added three more cascading children, so ChatRoom now has twenty-two.** All
three of `MantraProject`, `MantraProjectArtifactVersion` and
`MantraProjectTranslationArtifactVersion` reference the room `ON DELETE CASCADE`. The
split's migration never enumerated them -- it depends on Room disabling foreign keys,
which is asserted rather than assumed -- so it needed no change, but every count in
its prose did.
`ChatRoomSplitThroughRoomJvmTest` is better for it: starting from a v22 file now runs
both of this branch's migrations in one upgrade, which is what a device coming off the
projects release will actually do.
Conflicts resolved toward mantra's shape with this branch's change applied inside it:
`roomDerivedBy` keeps its new extracted helper and reads the ceremony's own threshold
key through `getCeremonyThresholdPublicKey`, and `ChatRoomDetailViewModel` keeps the
`parents` rename with the per-profile MLS read. The new projects and chapters fixtures
needed the same membership seeding as the rest.
1212 tests, 0 failures.