Files
mantra-kmp/composeApp
Kgothatso Ngako 39e5df8253 feat: sign a translation into the artifact instead of submitting one
Starting a translation no longer creates one. It opens a signing session over a
TranslationArtifactVersionEvent and a TranslationChapterEvent per chapter, and
the translation appears -- on every member's device at once, authored by the
room's shared key rather than by whoever picked the dialect -- when enough
members have signed. The same trade the dialects, artifacts and chapters made:
a submission says "I am putting this in front of the group" and the group's
only recourse afterwards is social, while a signature is the group saying it
and it takes a quorum to say. A translation is what the group's readers will
read the work as, so the second is the honest one.

**The chapters, in the same batch.** A translation with no chapters is one
nobody can start: a translated chunk hangs off a translation chapter, which
hangs off the translation. They travel as their own signed events for the same
reason the chapter's chunks do since 2e133dd -- a row that carries the group's
signature over its own id can be checked by anybody holding it, rather than
only by whoever re-derives it.

That makes this the second caller of `proposeSigningBatch`'s lead/dependents
form, and for exactly the reason the form exists. A translation chapter carries
the id of the translation it belongs to, 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. So the translation is built
first and handed to `AddTranslationArtifactVersionViewModel.translationChaptersOf`,
which lays the chapters out against it. An item naming a translation nobody
signed is not a mistake that can be made rather than one to be tested for.

The lead is item 0 and items apply in `itemIndex` order, which is what
MantraTranslationChapter's foreign key to MantraTranslationArtifactVersion
needs: what is referenced is signed first as well as named first.

**Only existing dialects.** The form's "New dialect" chip and its three fields
are gone, and with them the path that created a dialect on the way to using it.
A dialect is the group's too -- AddDialectScreen has proposed one for signing
since the dialects made this trade -- so minting one as a side effect of
translating would have put a dialect nobody agreed to underneath a translation
the group did. Nothing is selected to begin with, because picking a default
would be choosing the language of the work; the room's own detail screen is
where a missing dialect is asked for, and the form says so when there are none.

**The cost, in front of whoever is looking.** MAX_BATCH_SIZE is 64 and the
translation takes one place, so MAX_CHAPTERS_PER_TRANSLATION is 63. Unlike a
chapter's paragraphs this is not something the person at the screen can shorten
by splitting anything, so it is stated rather than advised: the count is shown
against the cap as chapters load, coloured when past it, and the form will not
propose -- because the alternative is an IllegalArgumentException after the
fact. The view model refuses independently; the screen is not what enforces it.

**What a chapter signed later does not reach.** A translation covers the
chapters that existed when it was proposed, and nothing scaffolds a translation
chapter for one signed into the artifact afterwards. That is not new -- the DAO
this replaces scaffolded once too -- but it is now said where somebody can act
on it, in the form's own line, rather than discovered as a chapter that cannot
be translated. Fixing it properly is its own change.

**What went away.** MantraDao.addTranslationArtifactVersion and its way up
through the repository, including the commented-out chunk scaffolding it had
been carrying. Nothing called it once the screen proposed instead, and leaving
a path that authors a translation under a member's key while the UI insists on
a quorum is the trap eb34c8e removed for chapters.

MantraRepository.addDialect and DatabaseMantraRepository.addDialect went with
it: the new-dialect path was their last caller, so what remained was a
member-authored dialect reachable from any screen that holds a MantraRepository.
MantraDao.addDialect stays, because MantraDaoJvmTest uses it as the worked
example for the `rumorOf` seam that addArtifact, addArtifactVersion and
saveTranslation still run through.

**The screens.** AddTranslationArtifactVersionScreen loads the artifact's
latest version, its chapters and canSign up front and disables the FAB when any
of them is missing, the way the dialect, artifact and chapter screens do; on
success it lands on the session rather than on an artifact the translation is
not in yet. FrostSigningScreen described both new kinds as "Event of kind
30306" and a run of "Event of kind 30308", which is a member being asked to
sign a translation they cannot read; it now reads the dialect name, visibility
and licence for the translation and the position for each chapter.

**Tests.** TranslationBatchProposalJvmTest runs the real proposal against a
real database over a real ceremony, which is where the sharp edge is: item
order, every chapter naming the translation as the group will author it, the
source chapter and position each stands in for, one timestamp across the batch,
and both ends of the cap -- 63 chapters proposes, 64 is refused and leaves no
session behind. Checked against broken implementations rather than only against
a working one: naming the artifact version instead of the signed translation,
taking the index from list position, and stamping the chapters off their own
clock are each caught.

If the first of those came apart the translation would still be signed and
every chapter would still verify -- against a translation id nobody has. They
would fail a foreign key on the way in and the translation would simply arrive
empty, which is the failure worth a database to catch.

392 jvmTest and 244 testDebugUnitTest pass, none of the existing ones edited.

Not covered: applyInnerEvent's upserts, which need a database no test here
stands up, and addTranslation itself, which is plumbing across two dispatchers
over a template and a builder the tests already pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 11:07:49 +02:00
..