diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifact.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifact.kt index 7620de16..a8194481 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifact.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifact.kt @@ -87,6 +87,11 @@ data class MantraArtifact( * wrong here until a chronicle needed to rebuild an artifact and nothing had * ever called this. */ + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toArtifactEvent(versionLabel: String): ArtifactEvent { return ArtifactEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifactVersion.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifactVersion.kt index e645ebf3..fc37fc1a 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifactVersion.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifactVersion.kt @@ -66,6 +66,11 @@ data class MantraArtifactVersion( override val createdAt: Instant = Clock.System.now(), override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toArtifactVersionEvent(): ArtifactVersionEvent { return ArtifactVersionEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChapter.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChapter.kt index a12f015e..328441ef 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChapter.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChapter.kt @@ -71,6 +71,11 @@ data class MantraChapter( override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toChapterEvent(): ChapterEvent { return ChapterEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChunk.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChunk.kt index ab7439cf..b294bf31 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChunk.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChunk.kt @@ -67,6 +67,11 @@ data class MantraChunk( override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toChunkEvent(): ChunkEvent { return ChunkEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraDialect.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraDialect.kt index a7fef77a..0bc37ba8 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraDialect.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraDialect.kt @@ -56,6 +56,11 @@ data class MantraDialect( override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toDialectEvent(): DialectEvent { return DialectEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationArtifactVersion.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationArtifactVersion.kt index 9b9be98c..06f05451 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationArtifactVersion.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationArtifactVersion.kt @@ -75,6 +75,11 @@ data class MantraTranslationArtifactVersion( override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toTranslationArtifactVersionEvent(): TranslationArtifactVersionEvent { return TranslationArtifactVersionEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChapter.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChapter.kt index 0cd2df48..d9f3c737 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChapter.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChapter.kt @@ -70,6 +70,11 @@ data class MantraTranslationChapter( override val createdAt: Instant = Clock.System.now(), override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toTranslationChapterEvent(): TranslationChapterEvent { return TranslationChapterEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChunk.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChunk.kt index b74380b5..c5ec03ca 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChunk.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChunk.kt @@ -72,6 +72,11 @@ data class MantraTranslationChunk( override val updatedAt: Instant = createdAt ): OptionalNostrEventEntity, TimestampedEntity { + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. Read the " + + "event off that table instead; this rebuild goes when the last pre-v13 install " + + "does -- see the removal checklist in docs/member-chronicle.md." + ) fun toTranslationChunkEvent(): TranslationChunkEvent { return TranslationChunkEvent( id = id, diff --git a/composeApp/src/commonMain/kotlin/press/mantra/compose/managers/ChronicleManager.kt b/composeApp/src/commonMain/kotlin/press/mantra/compose/managers/ChronicleManager.kt index 03c4a873..7c9f5e19 100644 --- a/composeApp/src/commonMain/kotlin/press/mantra/compose/managers/ChronicleManager.kt +++ b/composeApp/src/commonMain/kotlin/press/mantra/compose/managers/ChronicleManager.kt @@ -42,8 +42,10 @@ import press.mantra.compose.nostr.frost.GroupKeyStateEvent * signed; `ChronicleRoundTripTest` is what says it is, per kind, against a real * quorum, and it found two faults in the artifact's rebuild the first time it * ran -- both of which would have shipped payloads that every receiver drops as - * forgeries without a word. The fallback can go once no install still holds - * pre-v13 work. + * forgeries without a word. Both it and everything that exists only to hold it + * up are marked `@Deprecated`; the fallback can go once no install still holds + * pre-v13 work, and docs/member-chronicle.md's "Retiring the rebuild" is the list + * of what goes with it. * * **The allowlist does real work on the way out now.** The rebuild could only * ever produce document kinds; the table holds everything the group has ever @@ -662,7 +664,18 @@ object ChronicleManager { * * It returns everything it can rebuild and lets [signedEventsOf] and the * verify filter decide what can travel. + * + * Deprecated rather than merely legacy: it is a whole mechanism kept alive + * for a shrinking set of rows, and it takes eight `toXEvent()` methods and a + * ten-case round-trip suite with it. **docs/member-chronicle.md, "Retiring the + * rebuild", is the checklist** -- what goes, what only looks like it goes, + * and the one thing that has to be true before any of it can. */ + @Deprecated( + "Chronicle fallback for work signed before the GroupSignedEvent table. " + + "Goes when the last pre-v13 install does -- see the removal checklist " + + "in docs/member-chronicle.md." + ) private suspend fun rebuiltEventsOf( database: MantraDatabase, chatRoomId: String, diff --git a/composeApp/src/commonTest/kotlin/press/mantra/compose/nostr/chronicle/ChronicleRoundTripTest.kt b/composeApp/src/commonTest/kotlin/press/mantra/compose/nostr/chronicle/ChronicleRoundTripTest.kt index c6d13145..7d53214a 100644 --- a/composeApp/src/commonTest/kotlin/press/mantra/compose/nostr/chronicle/ChronicleRoundTripTest.kt +++ b/composeApp/src/commonTest/kotlin/press/mantra/compose/nostr/chronicle/ChronicleRoundTripTest.kt @@ -39,22 +39,30 @@ import press.mantra.compose.nostr.nip30303.TranslationChapterEvent import press.mantra.compose.nostr.nip30303.TranslationChunkEvent /** - * The assumption the whole chronicle rests on: a row can be turned back into the - * event the group signed. + * The assumption the chronicle's *fallback* rests on: a row can be turned back + * into the event the group signed. * - * Signed events are not stored as events. `FrostSigningManager.complete` applies - * them and what survives is a `Mantra*` row, so a chronicle has to rebuild each - * one with `toXEvent()` and hope it comes out byte-identical. If it does not, - * the id changes, the signature no longer covers it, and every receiver drops - * the payload as a forgery -- silently, one kind at a time. + * Signed events used to be stored only as rows. `FrostSigningManager.complete` + * applied one and what survived was a `Mantra*` row, so a chronicle had to + * rebuild each payload with `toXEvent()` and hope it came out byte-identical -- + * and if it did not, the id changed, the signature no longer covered it, and + * every receiver dropped the payload as a forgery, silently, one kind at a time. * * That is what the round-trip note on each `toXEvent` claims and what nothing - * asserted until now. Every kind in `ChronicleEvent.CHRONICLABLE_KINDS` is checked - * here against a real FROST quorum, because a signature that verifies is the - * only evidence that the rebuild is faithful -- comparing fields would only + * asserted until this ran. Every kind in `ChronicleEvent.CHRONICLABLE_KINDS` is + * checked here against a real FROST quorum, because a signature that verifies is + * the only evidence that the rebuild is faithful -- comparing fields would only * prove the test and the code agree about which fields matter. * - * A kind that fails here cannot be chronicled at all, whatever the allowlist says. + * ### Deprecated, along with what it guards + * + * `GroupSignedEvent` keeps the events now, and `ChronicleManager.assemble` reads + * them; the rebuild survives only for work signed before that table existed, and + * so does this. Nothing else covers the `toXEvent()` methods, so **this whole + * file goes when they do** -- see "Retiring the rebuild" in + * docs/member-chronicle.md. Until then a kind that fails here cannot be chronicled + * *from a row*, whatever the allowlist says, which for a pre-v13 room is the + * same thing as not at all. */ class ChronicleRoundTripTest { private val participants = 3 diff --git a/docs/member-chronicle.md b/docs/member-chronicle.md index 2c8f146e..dd6d07e5 100644 --- a/docs/member-chronicle.md +++ b/docs/member-chronicle.md @@ -717,6 +717,102 @@ content. It buys graceful degradation and costs everything listed under --- +## Retiring the rebuild + +`assemble` reads `GroupSignedEvent` and rebuilds from `Mantra*` rows only what +that table does not hold, which by now is work signed before the table existed. +Everything on the rebuild side is marked `@Deprecated` so the compiler names it +at every call site, and it comes out in one piece rather than a method at a time +-- a half-removed rebuild is a rebuild that covers some kinds and silently drops +others. + +### The one precondition + +**No install still holds work signed before schema v13.** Nothing in the app can +check this, and no query answers it: a device that upgraded is indistinguishable +from one that never had pre-v13 work, and the rows that need rebuilding are on +*other people's* devices. It is a judgement about the installed base, not a +condition to test for. What can be checked, on any given device, is that the +rebuild is contributing nothing: + +``` +Chronicling : N event(s) as the group signed them, M rebuilt from rows that predate the record +``` + +`ChronicleManager.signedEventsOf` logs that line only when `M > 0`. Silence across +the fleet is the evidence; it is not proof. + +A member whose device still needs it and does not get it is not broken loudly. +They keep their own rows and read the room normally. What they lose is the +ability to *answer* a chronicle request with the older half of the group's work, +so a newer member asks, is answered, and receives a chronicle that is quietly +short. That is the failure mode to weigh -- it looks like success on both ends. + +### What goes + +| what | where | +|---|---| +| `rebuiltEventsOf` | [ChronicleManager.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/managers/ChronicleManager.kt) -- the tree walk, and the version-label recovery inside it | +| the union in `signedEventsOf` | same file -- it collapses to the `GroupSignedEvent` read plus the `isChroniclable` filter, which **stays**: see below | +| `MantraDialect.toDialectEvent` | [MantraDialect.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraDialect.kt) | +| `MantraArtifact.toArtifactEvent` | [MantraArtifact.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifact.kt) -- and with it the `versionLabel` parameter that exists only because the label is not on the row | +| `MantraArtifactVersion.toArtifactVersionEvent` | [MantraArtifactVersion.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraArtifactVersion.kt) | +| `MantraChapter.toChapterEvent` | [MantraChapter.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChapter.kt) | +| `MantraChunk.toChunkEvent` | [MantraChunk.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraChunk.kt) | +| `MantraTranslationArtifactVersion.toTranslationArtifactVersionEvent` | [MantraTranslationArtifactVersion.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationArtifactVersion.kt) | +| `MantraTranslationChapter.toTranslationChapterEvent` | [MantraTranslationChapter.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChapter.kt) | +| `MantraTranslationChunk.toTranslationChunkEvent` | [MantraTranslationChunk.kt](../composeApp/src/commonMain/kotlin/press/mantra/compose/database/model/MantraTranslationChunk.kt) | +| `ChronicleRoundTripTest`, all ten cases | [ChronicleRoundTripTest.kt](../composeApp/src/commonTest/kotlin/press/mantra/compose/nostr/chronicle/ChronicleRoundTripTest.kt) -- it exists to hold the rebuild up and covers nothing else | +| the "Where the events come from" reasoning above | this file | + +**Two already-dead cousins to sweep at the same time**, neither of which is part +of the rebuild and both of which will look like it to whoever does the removal: +`MantraTranslation.toTranslationEvent` (nothing has ever called it -- 30311 is +not chroniclable and nothing builds one) and +`MantraTranslationChunkProposal.toTranslationChunkEvent` (on a model that is not +even a `@Database` entity). See [dead-code.md](./dead-code.md) for the house +style on writing those down rather than deleting them blind. + +**The tests that seed without recording go too**, or they go on proving a path +that no longer exists. In `ChronicleAssemblyJvmTest` the `apply`-only seeding is +the rebuild path and `recordEverythingApplied` is the real one; the cases named +*work held both ways travels exactly once* and *an artifact the rebuild has to +leave out still chronicles from the record* are about the union specifically and +have no meaning without it. `ChronicleApplyJvmTest` seeds the sender the same way, +so it needs the recording call added rather than removed -- it is testing +delivery, not assembly, and would otherwise start asserting against an empty +chronicle. + +### What only looks like it goes + +**The `isChroniclable` filter in `signedEventsOf` stays, and becomes the only +thing standing.** It is not part of the rebuild; it is there *because* of the +record. The rebuild could only ever produce document kinds, so nothing needed +filtering while it was the source. The table holds every kind the group has +signed, and every room signs a `GroupKeyStateEvent` as its first act -- so +removing that filter along with the walk turns every room's chronicle into an +`IllegalArgumentException` from `ChronicleEvent.build`. Two cases in +`ChronicleAssemblyJvmTest` fail with exactly that if it is dropped, which is the +guard against removing it by association. + +**The verify filter in `assemble` stays.** With the rebuild gone it is checking +events that were verified before they were recorded, so it can never fail in +practice -- which is an argument for keeping it, not for dropping it. It is one +signature check standing between a corrupted row and a payload every receiver +reads as a forgery, and "cannot happen" is the state it is meant to preserve. + +**`Mantra*.signature` and `Mantra*.publicKey` are not obviously removable, and +are a separate decision.** They were what made a row rebuildable, but they are +also what `SignedArtifactTest`, `SignedChapterTest`, `SignedGroupKeyStateTest` +and `ChronicleApplyJvmTest.rowFingerprints` assert on, and +`MantraTranslationContributor` builds a contributor list out of one. Since v13, +`groupSignedEventId` says whether the group signed a row and points at the proof, +so the columns are arguably redundant -- but that is a schema migration across +twelve tables with its own tests to rewrite, and it should not ride along with +this. + +--- + ## What this does not do Each of these will be reported as a bug. None of them is.