Merge branch 'mantra' into claude/rename-archive-chronicle-a4a8e0
The rebuild deprecation landed on mantra while the rename was in flight, and it touched the same files by their old names. Git matched the renames itself, so the only conflict was `ChronicleRoundTripTest`'s header, where both sides had rewritten the same paragraph: mantra's says this file is now the gate on a deprecated fallback rather than on the only path, which is the newer and truer claim, so it wins and the rename is applied on top of it. Everything the merge brought in went through the same substitution as the rest: the nine `@Deprecated` messages and the "Retiring the rebuild" checklist all name `ChronicleManager`, `ChronicleRoundTripTest` and docs/member-chronicle.md, which are the files that now exist. 797 tests pass -- 500 jvm, 297 android. The five new ones are the migration's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <room>: 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.
|
||||
|
||||
Reference in New Issue
Block a user