feat: archive the translated text too, now that the group signs it
The merge brought in two commits that close the gap this feature was written around, so the allowlist grows from six kinds to eight. `feat: sign an artifact's first version with it, not derive it after` makes the version the second item of the artifact's own signing batch. `feat: ask the group to sign a chunk's translation, not just save it` puts a quorum behind the prose. Both were done for their own reasons and neither was about the archive, but they are exactly what the archive was missing: an archive can only carry what its recipient can check, so a derived version and a member-authored translation could not travel. A new member got the whole structure and none of the words. **30301 and 30309 do not go on the end of the list.** The order is the foreign keys: a version sits between its artifact and the chapters hanging off it, and a translated chunk hangs off both a source chunk and a translation chapter, so that one really is last. **`toArtifactVersionEvent` had the bug this predicted it would.** It emitted [artifactId, alt] where `build` emits [alt, artifactId], so the id did not round-trip -- the same fault fixed on `MantraArtifact.toArtifactEvent` in Phase 3, in the second of the three unused rebuilds, and for the same reason: nothing had ever called it, so the "tag order matches build" claim in its comment was never checked. `toTranslationChunkEvent` was already correct. Both now have a round-trip case, which is what makes the difference between a rebuild that is right and one that has not been contradicted yet. **`signedEventsOf` walks two steps further**, emitting each version and the translation chunks under each translation chapter. A retranslated passage archives once: the arm that applies a translation chunk drops the one it supersedes -- newest by the timestamp the group signed at, id breaking a tie -- so what a sender holds, and therefore what travels, is the group's current answer to each passage rather than its drafts. **The seeds had to change with it.** Both database tests derived the artifact's first version by applying the artifact, which is exactly what stopped happening; they now sign it through `ArtifactVersionEvent.initialVersionOf`, the way the batch does. That also removes the one exception in the end-to-end assertion: every archived row is now authored by the room and carries a signature, where the artifact version used to have to be excused for having neither. 480 tests pass. The plan's Phase 3 table, its built-vs-plan table and its "what this does not do" section are updated -- what an archive cannot do is down from two things to one, and the remaining one is that it still cannot make its recipient able to sign. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,12 +68,16 @@ data class MantraArtifactVersion(
|
||||
id = id,
|
||||
pubKey = publicKey,
|
||||
createdAt = createdAt.epochSeconds,
|
||||
// Tag order matches ArtifactVersionEvent.initialVersionOf, which puts
|
||||
// the alt first because `build` does, or the event id does not
|
||||
// round-trip. It was the other way round here for as long as nothing
|
||||
// called this -- see ArchiveRoundTripTest, which is what calls it now.
|
||||
tags = TagArrayBuilder<ArtifactVersionEvent>()
|
||||
.addUnique(
|
||||
ArtifactIdTag.assemble(artifactId)
|
||||
AltTag.assemble(ArtifactVersionEvent.ALT_DESCRIPTION)
|
||||
)
|
||||
.addUnique(
|
||||
AltTag.assemble(ArtifactVersionEvent.ALT_DESCRIPTION)
|
||||
ArtifactIdTag.assemble(artifactId)
|
||||
)
|
||||
.build(),
|
||||
content = versionLabel,
|
||||
|
||||
@@ -577,10 +577,12 @@ object ArchiveManager {
|
||||
.getArtifactVersionsByArtifactId(artifact.id)
|
||||
|
||||
// The version an artifact starts life with carries the label the
|
||||
// artifact was signed with, and `initialVersionOf` gives it the
|
||||
// artifact's own timestamp. A row with none of those is an artifact
|
||||
// this device cannot rebuild, which is a gap in the archive rather
|
||||
// than a reason to abandon it.
|
||||
// artifact was signed with, and `ArtifactVersionEvent.initialVersionOf`
|
||||
// gives it the artifact's own timestamp. The label is still not on the
|
||||
// artifact row -- `fromArtifactEvent` drops it -- so this is where it
|
||||
// comes back from. An artifact with no such version is one this device
|
||||
// cannot rebuild, which is a gap in the archive rather than a reason to
|
||||
// abandon it.
|
||||
val versionLabel = versions
|
||||
.firstOrNull { it.createdAt == artifact.createdAt }
|
||||
?.versionLabel
|
||||
@@ -592,6 +594,8 @@ object ArchiveManager {
|
||||
}
|
||||
|
||||
versions.forEach { version ->
|
||||
add(version.toArtifactVersionEvent())
|
||||
|
||||
database.mantraChapterDao()
|
||||
.getChaptersByArtifactVersionId(version.id)
|
||||
.forEach { chapter ->
|
||||
@@ -609,7 +613,20 @@ object ArchiveManager {
|
||||
|
||||
database.mantraTranslationChapterDao()
|
||||
.getTranslationChaptersByTranslationArtifactVersionId(translationVersion.id)
|
||||
.forEach { add(it.toTranslationChapterEvent()) }
|
||||
.forEach { translationChapter ->
|
||||
add(translationChapter.toTranslationChapterEvent())
|
||||
|
||||
// Only the translation that survived supersession
|
||||
// is here to be found: retranslating a passage
|
||||
// changes the text and so the event id, and the
|
||||
// arm that applies one drops what it replaces. So
|
||||
// an archive carries a group's current answer to
|
||||
// each passage rather than its drafts, which is
|
||||
// the same thing every other member holds.
|
||||
database.mantraTranslationChunkDao()
|
||||
.getTranslationChunksByTranslationChapterId(translationChapter.id)
|
||||
.forEach { add(it.toTranslationChunkEvent()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,13 @@ import press.mantra.compose.nostr.archive.tags.ArchiveIdTag
|
||||
import press.mantra.compose.nostr.archive.tags.ArchivePageTag
|
||||
import press.mantra.compose.nostr.archive.tags.ArchiveRecipientTag
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactEvent
|
||||
import press.mantra.compose.nostr.nip30303.ArtifactVersionEvent
|
||||
import press.mantra.compose.nostr.nip30303.ChapterEvent
|
||||
import press.mantra.compose.nostr.nip30303.ChunkEvent
|
||||
import press.mantra.compose.nostr.nip30303.DialectEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationArtifactVersionEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationChapterEvent
|
||||
import press.mantra.compose.nostr.nip30303.TranslationChunkEvent
|
||||
|
||||
/**
|
||||
* 30327
|
||||
@@ -170,35 +172,32 @@ class ArchiveEvent(
|
||||
* rumors -- empty signature, member author, vouched for by the MLS frame
|
||||
* they arrived in and by nothing that survives leaving it.
|
||||
*
|
||||
* Left out for that reason, and this is a real limit rather than an
|
||||
* oversight:
|
||||
* Left out for that reason:
|
||||
*
|
||||
* - `ArtifactVersionEvent` (30301). Never signed. The version an
|
||||
* artifact starts life with is *derived* from the signed artifact by
|
||||
* `MantraArtifactVersion.initialVersionOf`, which the artifact's own
|
||||
* arm in `applyInnerEvent` runs -- so archiving the artifact brings its
|
||||
* version along and the chapters hanging off it keep their foreign
|
||||
* key. Later versions go through `MantraDao.addArtifactVersion` as
|
||||
* rumors, and no screen calls it today.
|
||||
* - `TranslationChunkEvent` (30309) -- **the translated text itself**.
|
||||
* `MantraDao.saveTranslation` submits it as a member's rumor, because
|
||||
* a translation is one member's work rather than a group decision.
|
||||
* So an archive restores everything a translation hangs on and not the
|
||||
* translation. See docs/member-archive.md for what closing that would
|
||||
* take; it is not a line in this list.
|
||||
* - `TranslationEvent` (30311). Nothing builds one; the inbound arm
|
||||
* exists and no producer does.
|
||||
* - The contributor lists (30305, 30307, 30310). `applyInnerEvent` has
|
||||
* no arm that writes a row for any of them, so archiving them would
|
||||
* cost bytes and restore nothing.
|
||||
*
|
||||
* Two kinds were on that list and are not any more, because the app
|
||||
* stopped leaving them unsigned. `ArtifactVersionEvent` (30301) used to be
|
||||
* derived from the signed artifact on arrival, and is now the second item
|
||||
* of the batch that carries it. `TranslationChunkEvent` (30309) -- the
|
||||
* translated text itself -- used to be submitted as its author's rumor,
|
||||
* and is now proposed to the group like everything else. Both are
|
||||
* therefore checkable, and an archive that left them out would hand a new
|
||||
* member the whole structure and none of the prose.
|
||||
*/
|
||||
private val APPLY_ORDER: List<Kind> = listOf(
|
||||
DialectEvent.KIND,
|
||||
ArtifactEvent.KIND,
|
||||
ArtifactVersionEvent.KIND,
|
||||
ChapterEvent.KIND,
|
||||
ChunkEvent.KIND,
|
||||
TranslationArtifactVersionEvent.KIND,
|
||||
TranslationChapterEvent.KIND,
|
||||
TranslationChunkEvent.KIND,
|
||||
)
|
||||
|
||||
/** Every kind an archive may carry. */
|
||||
|
||||
Reference in New Issue
Block a user