feat: archive the events the group signed, not rebuilds of its rows
`assemble` read the archive out of `Mantra*` rows, rebuilding each payload with `toXEvent()` and standing or falling on that rebuild being byte-identical to what was signed. It had to: nothing kept the events. `GroupSignedEvent` keeps them now, so `signedEventsOf` reads the record first and rebuilds only what the record does not hold. **The rebuild stays, as the fallback, keyed by id.** A room whose work predates v13 has no events on file, and dropping the walk would silently empty its archive -- the failure mode being that a member asks for the history, a member answers, and nobody notices the answer was blank. So both sources are read and unioned by event id, which is also what a half-upgraded room needs: older work only the rows remember, newer work on file, and neither half complete on its own. The fallback can go once no install still carries pre-v13 work, and `ArchiveRoundTripTest` is what holds it up until then. **The allowlist does real work on the way out now, and this is the part that would have bitten.** The rebuild could only ever produce document kinds, because those are the only rows it walks. The record holds every kind the group has ever signed -- and every room signs a `GroupKeyStateEvent` as its first act, so one is on file in every room that has signed anything at all. `ArchiveEvent.build` refuses a non-archivable kind with `require`, so an unfiltered read does not quietly ship a key state: it throws, and the room's entire archive fails on the one event every room has. `signedEventsOf` therefore filters on `isArchivable` before anything else, which is the same rule `applyPage` applies on the way in. Removing that one line fails two tests with exactly that exception, which is how I know they are load-bearing rather than passing for the reason I expected. **An artifact whose initial version row is missing now archives.** The rebuild has to recover the version label from that row -- `fromArtifactEvent` drops it, so it is not on the artifact -- and logs and gives up without it, which is a hole in the archive for any device that applied half a batch. Read from the record there is nothing to recover: the label never left the event. That is the case that makes the record the better source rather than merely the faster one, and it has a test of its own. **One verify filter over both sources**, because the rule is per event and not per source: nothing leaves that the recipient could not check for themselves. A drop still means different things on each side -- a member's own rumor sitting in the same table as the group's work, versus a row that has drifted from the event it recorded -- and the comment now says so, since the log line cannot. **Ordering is unchanged where it matters and looser where it does not.** `inApplyOrder` is a stable sort by dependency rank, so the union only affects order *within* a rank: a room holding some work both ways can order two chapters differently from a member holding one way only. Pages are idempotent and applied payload by payload, and two members already differed by the order their rows were written in, so this costs nothing. `rebuiltEventsOf` still runs on every archive even where it contributes nothing, because there is no way to tell a complete record from a partial one without doing the walk, and it is a handful of indexed queries against a room's own rows. 495 jvm tests and 297 android unit tests pass. Five new cases in `ArchiveAssemblyJvmTest`, which seeds through the real inbound path and now records the same batch the way `FrostSigningManager.complete` does: payloads compared byte-for-byte against what was signed, work held both ways travelling exactly once, a genuinely room-signed key state left behind, a signed kind the archive has no arm for left behind, and the artifact the rebuild has to leave out archiving from the record. The existing assembly and end-to-end tests seed without recording, so they go on covering the rebuild fallback unchanged -- which is why they all still pass, and why that is evidence rather than luck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -296,10 +296,23 @@ keeps the decision about transcript lines next to the decision about triggers.
|
||||
> **Since the `GroupSignedEvent` table landed**, a signed event *is* stored as an
|
||||
> event -- `FrostSigningManager` files one per batch it completes, and
|
||||
> `ArchiveManager.applyPage` files one per payload it accepts, each with the
|
||||
> derivation path its author was reached at. Assembly still rebuilds from rows,
|
||||
> because a room whose work predates the table has nothing else to rebuild from.
|
||||
> The rest of this section is the reasoning as it stood then, and the round-trip
|
||||
> gate it describes is still what holds those rooms up.
|
||||
> derivation path its author was reached at. `assemble` reads that table first
|
||||
> and rebuilds only what it does not hold, which is work signed before the table
|
||||
> existed. So the rest of this section describes the *fallback*: the round-trip
|
||||
> gate it argues for is what holds those older rooms up, and it can go once no
|
||||
> install still carries pre-v13 work.
|
||||
>
|
||||
> Two things changed with the source, both worth knowing before reading on:
|
||||
>
|
||||
> - **The allowlist now does real work on the way out.** The rebuild could only
|
||||
> ever produce document kinds; the table holds everything the group has signed,
|
||||
> and every room signs a `GroupKeyStateEvent` as its first act. `assemble`
|
||||
> filters on `ArchiveEvent.isArchivable` before anything else -- without it
|
||||
> `ArchiveEvent.build` refuses the page and a room's whole archive fails on the
|
||||
> one event every room has.
|
||||
> - **An artifact whose initial version row is missing now archives.** The
|
||||
> rebuild has to recover the version label from that row and logs and gives up
|
||||
> without it; on file as an event, the label never left.
|
||||
|
||||
Signed events are not stored as events; they are stored as rows. So the archive
|
||||
is rebuilt from `Mantra*` rows via each entity's `toXEvent()`, which is exactly
|
||||
|
||||
Reference in New Issue
Block a user