Docs

doc-sync

Source: crates/lvcs-doc/src/sync/mod.rs.

/// A peer device, by its Ed25519 public key (the bytes of
/// `lvcs_net::transport::Peer`), which keys the document actor's per-peer
/// Automerge `sync::State` (decision 30). Never a participant key.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct DeviceKey(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// The record every signed change carries as its Automerge message
/// (decision 125): the `ID_ENCODING` text of the canonical CBOR array
/// `[signer, grant, device or null, bundle or null]`, each a byte string.
/// `signer` and `grant` are the ones its [`ChangeSig`] must name; `device`
/// is the raw Ed25519 key of the daemon that committed it, and null in an
/// import or caps-seed change, so two devices importing identical bytes
/// produce one change (decision 69); `bundle` is set only by a
/// `BundleEdit`'s change, the merge bundle it belongs to (decision 48).
/// The change hash covers it, so a change has one valid signature. A
/// message decodes ([`ChangeMessage::decode`]) only when it is exactly that
/// encoding; genesis has no message. The document builds it in one place
/// from the commit's `Origin`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeMessage {
    pub signer: ParticipantKey,
    pub grant: GrantId,
    pub device: Option<DeviceKey>,
    pub bundle: Option<BundleId>,
}

/// The signature sidecar of one change (design §11, decision 50): `sig` is
/// `signer`'s `SIGNATURE` over [`ChangeSig::signing_input`], the canonical
/// CBOR array `(doc address, change_hash, grant)`, which blocks replaying
/// a signature onto another document or grant. `grant` names the grant the
/// signer holds for the change; a change by the owner, who needs no grant,
/// names `GrantId::OWNER`, the all-zero id (chosen at the M3 freeze), as
/// do the import changes `lvcs init` writes, which the owner signs. Both
/// must equal the change's own record ([`ChangeMessage`], decision 125). A
/// genesis change carries no `ChangeSig` (design §3). Each peer
/// stores signatures beside the document's saves ([`SigSave`]), keyed by
/// document and change hash, and sends them with the changes they cover.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeSig {
    pub doc: String,
    #[serde(with = "crate::cbor::hash")]
    pub change_hash: ChangeHash,
    pub signer: ParticipantKey,
    pub grant: GrantId,
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
}

/// The bytes one document actor sends another (`SyncOut.payload`,
/// `SyncIn.payload`, the `sync` wire payload's inner bytes), as canonical
/// CBOR: `message` is an encoded Automerge `sync::Message`, unchanged, and
/// `sigs` holds the [`ChangeSig`] of every change the message carries but
/// the canonical genesis, which has none, in the message's order (design
/// §11: sync carries the signature of every change it sends, inside the
/// same envelope). A change with no signature beside it is `Unsigned`; a
/// signature that names no carried change, or sits out of the message's
/// order, makes the payload `Malformed`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncPayload {
    #[serde(with = "crate::cbor::bytes")]
    pub message: Vec<u8>,
    pub sigs: Vec<ChangeSig>,
}

/// Start syncing with `peer` (the sync actor's `Join`): the actor makes a
/// fresh `sync::State` for it, replacing any it had, stores `tag` with it,
/// and sends its first message ([`SyncOut`]). `tag` is the sync actor's
/// (decision 110): the next value of a counter rising through one daemon
/// run, which the shell copies from `SyncOutput::Join`. The actor keeps the
/// last tag it had for each peer, from this or a later [`SyncIn`], and
/// stamps it on every [`SyncOut`] and [`SyncSettled`] for the peer, so the
/// sync actor can tell which of its inputs an output answers. A sync peer
/// keeps the actor from idling (design §2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerJoined {
    pub peer: DeviceKey,
    pub tag: u64,
}

/// Stop syncing with `peer`: its `sync::State` is dropped.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerLeft {
    pub peer: DeviceKey,
}

/// A sync message from `peer` (decision 68): `payload` is a
/// [`SyncPayload`]. Before Automerge sees it, the actor decodes it and
/// checks every change it carries: a [`ChangeSig`] by a signer whose
/// grant chain (from [`GrantsKnown`]) authorizes, at the change's time,
/// what the change concerns (decision 108: the document for a file or a
/// thread, an override's base, an index entry's, every document a merge
/// record lists; the caps document's changes keep its own rules), within
/// its quota by `seq` (decision 70); genesis only when it is the
/// canonical genesis. Any failure rejects the whole message
/// ([`SyncRejected`]) and applies nothing, and the actor sends the peer
/// nothing more; otherwise the actor applies it, stores the signatures,
/// and answers with [`SyncOut`] or [`SyncSettled`] (to every peer when the
/// document changed). A message whose change names a grant not known yet,
/// or that arrives before any `GrantsKnown` or before the project's root
/// is known, is deferred, with every later message from its peer (each
/// decoded as it arrives, so a malformed one is rejected at once), until
/// [`CapsSettled`] for its peer gives it its last check, rejecting it if
/// the grant is still unknown (decision 108); every `GrantsKnown` checks
/// the deferred messages again before that. A peer may have at most
/// `DEFER_MAX_MESSAGES` messages, and `DEFER_MAX_BYTES` of payload,
/// deferred in one document (`lvcs_vocab::limits`, decision 111): one more
/// is rejected `NoCapability`. `tag` is the sync actor's, copied by the
/// shell from `SyncOutput::Deliver` (decision 110): as the message
/// arrives, whatever its check finds, it replaces the tag stored with the
/// peer's sync state, and a message from a peer with no sync state (one
/// that never joined) creates a fresh one with it; a rejection then drops
/// that state. Only Automerge's V1 format is accepted (`Malformed`
/// otherwise): LVCS peers never advertise V2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncIn {
    pub peer: DeviceKey,
    pub tag: u64,
    #[serde(with = "crate::cbor::bytes")]
    pub payload: Vec<u8>,
}

/// The `.lvcs/caps` document settled with `peer` (its [`SyncSettled`];
/// decision 108): the shell sends it to every document actor, the caps
/// document's own included, right after the [`GrantsKnown`] of the caps
/// render that came before it, so it reaches each mailbox behind those
/// grants. Every message deferred from `peer` ([`SyncIn`]) gets its last
/// check against the grants known then: applied, or rejected if a grant it
/// names is still unknown; the other deferred messages are checked again.
/// Sent by `M3.daemon`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapsSettled {
    pub peer: DeviceKey,
}

/// The daemon's signature for a change the actor asked about
/// ([`SignRequest`]): the actor stores it beside the saves ([`SigSave`])
/// and resumes syncing the change (design §11). Taken only when it names
/// this document and one of its unsigned changes and its signature
/// verifies; any other is ignored, as is a second one for a change.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signed {
    pub sig: ChangeSig,
}

/// The grants every change is verified against (`kinds::caps::authorize`):
/// `project`, the project id this daemon runs (from `lvcs init`, or the
/// one `lvcs join` was given); `root`, the owner and project name (the root
/// of every chain), once this daemon's `.lvcs/caps` holds a seed whose
/// root's project id equals `project`, and `None` before (a joining daemon
/// that has not synced the caps document yet); and every signed grant the
/// caps document holds, by id. With `root` unknown, every change but the
/// caps document's seed is refused `NoCapability`, and the caps document
/// accepts as its seed only a change signed by the root's owner under
/// `GrantId::OWNER` whose root names `project`: with `root` unknown, the
/// caps document counts the root its seed would hold once the message
/// applies (read from a copy) when that root's project id is `project`.
/// `revoked` holds each revoked grant's revocation time `t` in Unix
/// seconds (decision 148): such a grant authorizes only changes whose own
/// time is before `t`.
/// Sent by the shell on spawn and whenever the caps document renders
/// ([`CapsRendered`]); each one checks the deferred messages ([`SyncIn`])
/// again, and [`CapsSettled`] gives a peer's its last check.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantsKnown {
    #[serde(with = "crate::cbor::fixed")]
    pub project: [u8; 32],
    pub root: Option<ProjectRoot>,
    pub grants: BTreeMap<GrantId, SignedGrant>,
    pub revoked: BTreeMap<GrantId, i64>,
}

/// Add a signed grant to the `.lvcs/caps` document (design §15: `grant`
/// issues every grant, a renewal included), committed by
/// `origin.author`. Answered like an edit: `Changed` with `req`, or
/// `Rejected` (a document of another kind answers `Failed`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssueGrant {
    pub req: RequestId,
    pub origin: Origin,
    pub grant: SignedGrant,
}

/// The `HASH` (SHA-256) of a string the secret scan flagged and its
/// author confirmed is no secret: the key of its signed allowlist entry in the
/// caps document (decisions 108 and 128), which every scanner built from
/// the caps render honors.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SecretHash(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// Add `hash` to the `.lvcs/caps` document's signed allowlist (decisions
/// 108, 109, and 128): the key `/allowlist/<hash>` in `ID_ENCODING`,
/// holding `true`, written once and committed by `origin`. Answered like
/// an edit: `Changed` with `req`, at the current heads and committing
/// nothing when the entry is already there, or `Rejected` (a document of
/// another kind, or a closed one, answers `Failed`). Every peer's verifier
/// takes it from any signer whose grant chain and window hold (`Write` on
/// `ALLOWLIST_ADDRESS`, decision 147), the owner always.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AllowSecret {
    pub req: RequestId,
    pub origin: Origin,
    pub hash: SecretHash,
}

/// Revoke `grants` in the `.lvcs/caps` document (decision 148): for each
/// one not revoked yet, the key `/revoked/<grant id>` in `ID_ENCODING`,
/// holding `ScalarValue::Int(t)`, where `t` is the change's own commit
/// time, written once and committed by `origin` in one change. Answered
/// like an edit: `Changed` with `req`, at the current heads and committing
/// nothing when every one is revoked already, or `Rejected` `Failed` when
/// one is not in `/grants` or `t` is before its `not_before_s`, which every
/// peer's verifier would refuse (a document of another kind, or a closed
/// one, answers `Failed` too). Every peer's verifier takes it from a
/// signer `authorize` lets write `CAPS_ADDRESS` (a grant naming
/// `Delegate`, or the owner) that is the grant's issuer or the owner, or
/// that also holds `Delegate` on each path probe of the grant
/// (`paths::probe`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevokeGrant {
    pub req: RequestId,
    pub origin: Origin,
    pub grants: BTreeSet<GrantId>,
}

/// The bundle visibility gate (design §8, decision 48): the registry holds
/// the merge record of `bundle`, which lists `target` as this document's
/// target heads. The document answers with [`BundleHeld`], and again
/// whenever sync completes them; it never changes what the document shows,
/// since a bundle's changes are hidden from when they arrive. Until [`BundleVisible`] arrives for it,
/// the document renders (and reports as its visible heads, in `Changed`,
/// `Rendered`, `AnchorsResolved`, a body `ReadDone`, and `Resolved`) the
/// heads that exclude every change whose record ([`ChangeMessage`]) names
/// `bundle` and every change built on one; a bundle sync brings changes
/// of before any record is hidden the same way, and so is every bundle a
/// change of the saves names when the document loads. A bundle
/// this incarnation committed changes of itself (`BundleEdit`, on the
/// merging peer) is never hidden, as in M2. The gate lasts one
/// incarnation (it is in the journaled fingerprint): the daemon sends
/// every record's `BundleTargets`, then `BundleVisible` for every bundle
/// complete everywhere, before a new incarnation's first `Render`, so a
/// reload shows every complete bundle, the merging peer's own included,
/// and keeps hiding one whose record or targets haven't all arrived.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleTargets {
    pub bundle: BundleId,
    #[serde(with = "crate::cbor::heads")]
    pub target: Vec<ChangeHash>,
}

/// Every document `bundle`'s record lists holds its target heads: this
/// document's visible frontier advances over the bundle, and, when that
/// moves what it shows, it emits `Changed` and renders.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleVisible {
    pub bundle: BundleId,
}

/// One side of a bundle-against-bundle check (decision 65): in this
/// document, the bundle's base heads (from its base's visible frontier),
/// its target heads, and the base ranges its merge record lists as
/// changed, anchored at `base` (`MergedDoc.ranges` in `lvcs-ws`). Empty
/// `ranges` is a `whole` entry (decision 108: the record lists the
/// document as whole, for a change with no range), which overlaps every
/// range of the other side: a bundle whose record lists a document always
/// changed something there.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleSide {
    pub bundle: BundleId,
    #[serde(with = "crate::cbor::heads")]
    pub base: Vec<ChangeHash>,
    #[serde(with = "crate::cbor::heads")]
    pub target: Vec<ChangeHash>,
    pub ranges: Vec<RangeAnchor>,
}

/// Check two complete bundles that both touch this document (decision 65):
/// they are concurrent here when neither's base heads contain the other's
/// target heads, and they overlap when a range of one shares an element
/// with a range of the other in the text's element order, deleted elements
/// included, at the sorted union of `a.target` and `b.target`: a range is
/// every element from its start anchor's to its end anchor's, inclusive.
/// That order never changes once both bundles are present, so every peer
/// holding both complete bundles decides alike whatever its current heads;
/// the document rebuilds it from the insert operations of its changes, as
/// Automerge orders a list.
/// Ranges are never resolved at the current heads: resolution moves a
/// deleted anchor (decisions 76 and 83), so two ranges that shared a
/// character a later edit deleted would no longer overlap there (chosen at
/// the M3 freeze). Answered with exactly one [`OverlapChecked`]; the shell
/// opens the conflict thread to both mergers and marks every frontier
/// holding both `needs-review`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckOverlap {
    pub req: RequestId,
    pub a: BundleSide,
    pub b: BundleSide,
}

/// Resolve cursor pairs against this document's current heads (design §5:
/// the coord actor decides overlap between notifications on resolved
/// spans). Answered with exactly one [`Resolved`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolveAnchors {
    pub req: RequestId,
    pub anchors: Vec<RangeAnchor>,
}

/// Anchor a notification's range (design §5; `M3.coord`, `M3.daemon`):
/// `start..end` are code points of `/content` at `at`, the heads the
/// notifier last read (`AnnounceParams.base`), holding at least one
/// character, or `0..0` when the text there is empty. Answered with exactly
/// one [`Anchored`]: the cursor pair on the range's first and last
/// characters at `at`, anchored as a claim is (decision 76), which the
/// daemon encodes as `Notification.range`; or `Whole` for an empty text,
/// which the daemon sends as an empty `range`, a whole-document
/// notification. It changes nothing in the document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorRange {
    pub req: RequestId,
    #[serde(with = "crate::cbor::heads")]
    pub at: Vec<ChangeHash>,
    pub start: usize,
    pub end: usize,
}

/// A sync message for `peer` (decision 30): `payload` is a
/// [`SyncPayload`] whose `message` is in Automerge's V1 format with no
/// flags. The actor sends none to any peer while a change it holds awaits
/// its signature, since Automerge can't leave one change out of a
/// message; the `Signed` that ends the wait sends to every peer. `tag` is
/// the one stored with the peer's sync state ([`PeerJoined`], [`SyncIn`];
/// decision 110), on every message, those the actor sends unprompted
/// after a local commit's `Signed` included; the shell copies it into
/// `SyncingEvents::DocOutgoing`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncOut {
    pub peer: DeviceKey,
    pub tag: u64,
    #[serde(with = "crate::cbor::bytes")]
    pub payload: Vec<u8>,
}

/// Automerge's sync has nothing more for `peer` until it hears from it
/// (decisions 110 and 112): emitted whenever the actor generates for the
/// peer and Automerge gives nothing, and also right after a [`SyncOut`]
/// the same input produced when that message is an acknowledgement: it
/// carries no changes, its `need` is empty, and the heads the peer last
/// reported (`their_heads`) equal this document's heads. So the opening
/// message (the peer has reported no heads yet) and a mid-round reply
/// that sends or asks for changes never settle, and the side that sends
/// the last message of a round still does. Never while a change awaits its
/// signature, since a message is then due. `tag` is as
/// for [`SyncOut`]; the shell copies it into `SyncingEvents::DocSettled`, and the
/// sync actor ends a peer's caps round (decision 68) only on a caps
/// `Settled` whose tag is its latest caps input's.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncSettled {
    pub peer: DeviceKey,
    pub tag: u64,
}

/// The revocations a peer holds (decision 326): the grant ids, in
/// `ID_ENCODING`, of the caps document's `/revoked` keys at the heads the
/// peer last reported (`their_heads`) that this document holds, sorted.
/// The caps document emits it after applying a message from the peer,
/// while it holds at least one revocation. A peer's sync message names
/// heads it holds, so each grant listed is one the peer holds the
/// revocation of; a head this document doesn't hold yet is left out, which
/// can only list fewer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerRevocations {
    pub peer: DeviceKey,
    pub grants: Vec<String>,
}

/// Why a whole sync message was rejected (decision 68). Each maps to a
/// `rejection` value; every reason but `Unavailable` disconnects the peer
/// with a lowered score.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncRejection {
    /// Not a canonical [`SyncPayload`], an Automerge sync message that
    /// doesn't decode, or a signature in `sigs` that names no change the
    /// message carries, in its order (`malformed`).
    Malformed,
    /// A change other than the canonical genesis has no [`ChangeSig`]
    /// (`unsigned`).
    Unsigned,
    /// A [`ChangeSig`] doesn't verify, or names another document; or the
    /// change's message is no [`ChangeMessage`], names another signer or
    /// grant than its `ChangeSig`, or its actor id isn't the derivation
    /// for its grant and record (`AuthorId::granted` under a grant whose
    /// terms name a quota, `AuthorId::participant` otherwise, or an
    /// import's or caps seed's, decisions 125 and 146) (`bad_signature`).
    BadSignature,
    /// No known grant chain authorizes the signer (`no_capability`).
    NoCapability,
    /// The grant's window doesn't contain the change's time
    /// (`grant_expired`).
    GrantExpired,
    /// The grant doesn't cover the document's path or the change
    /// (`out_of_scope`).
    OutOfScope,
    /// The change comes after `quota` among its actor's changes under
    /// `grant`, in `seq` order (decision 70; `quota_exceeded`).
    QuotaExceeded { grant: GrantId, quota: u64 },
    /// The document can't take sync now (closed, or a kind that doesn't
    /// sync yet); the shell drops the message and keeps the peer.
    Unavailable,
}

/// A sync message from `peer` was rejected whole: `change` is the first
/// change that failed, when one did.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncRejected {
    pub peer: DeviceKey,
    #[serde(with = "crate::cbor::opt_hash")]
    pub change: Option<ChangeHash>,
    pub reason: SyncRejection,
}

/// A change was committed and awaits its signature (design §11): the actor
/// sends no sync for it until [`Signed`] returns its [`ChangeSig`]. Emitted
/// right after the `Persist` that saves the change. A reload asks again,
/// on its incarnation's first `Render`, for every change but genesis that
/// the sidecar holds no signature for: a synced change's signature is
/// stored before its change, so each is an own change whose signature was
/// lost between this request and its `Signed` (a crash, a restart), and is
/// asked for again, never lost; the daemon signs those whose `signer` is
/// one of its participants. `req`
/// is the request that caused it, when one did; `author` the Automerge
/// actor it was committed under; `signer` and `grant` its record's
/// ([`ChangeMessage`], decision 125), exactly the pair the daemon signs
/// (the document takes a [`Signed`] only for that pair); `seq` its
/// Automerge sequence number, which the daemon's quota count follows
/// (decision 70); `time_s` its commit time. A change whose message is no
/// record is never asked about: no signature can verify for it. The
/// secret scan, the signing rate limit, and the daemon's own quota and
/// grant-expiry checks ran on the request before it was committed, with
/// the grant the shell put in its `Origin`, so the daemon always signs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignRequest {
    pub req: Option<RequestId>,
    pub author: AuthorId,
    pub signer: ParticipantKey,
    pub grant: GrantId,
    #[serde(with = "crate::cbor::hash")]
    pub change_hash: ChangeHash,
    pub seq: u64,
    pub time_s: i64,
}

/// The answer to [`BundleTargets`], and again whenever it changes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleHeld {
    pub bundle: BundleId,
    pub outcome: HeldOutcome,
}

/// Whether a document holds a bundle's target heads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HeldOutcome {
    Held(bool),
    /// `Failed` when the document can't take the gate (a kind no bundle
    /// changes, or closed).
    Refused(RejectReason),
}

/// The answer to a [`CheckOverlap`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlapChecked {
    pub req: RequestId,
    pub outcome: OverlapOutcome,
}

/// What a [`CheckOverlap`] found.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OverlapOutcome {
    Checked {
        concurrent: bool,
        overlapping: bool,
    },
    /// `UnknownBase` when the document lacks a side's base or target
    /// heads (a bundle not complete here); `Failed` when it can't check (a
    /// kind with no text, or closed).
    Refused(RejectReason),
}

/// The answer to an [`AnchorRange`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Anchored {
    pub req: RequestId,
    pub outcome: AnchorOutcome,
}

/// What an [`AnchorRange`] found.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AnchorOutcome {
    /// The range's cursor pair, anchored at `at`.
    Anchor(RangeAnchor),
    /// The text is empty at `at` and the range is `0..0`: the whole
    /// document.
    Whole,
    /// `UnknownBase` when the document lacks `at` (the `announce` error
    /// `UNKNOWN_BASE`), `OutOfBounds` when `start..end` is past the text
    /// there or empty in a text that isn't (`OUT_OF_BOUNDS`), `Failed`
    /// for a kind with no text, or a closed actor.
    Refused(RejectReason),
}

/// What a committed mirror save changed, as the render after it reports
/// it (`DocOutput::Rendered.touched`, decision 165): one range per save.
///
/// `start..end` are code points of the text at the render's heads. For a
/// text save they are the hull of the code points its splices changed there,
/// from the first to the last, snapped to whole lines by decision 160's rule
/// (a pure deletion names the line that holds its gap, and a save that
/// leaves the text empty names `0..0`); a save that creates the file, or
/// turns a binary file into text, names the whole text. `lines` names that
/// range there. `anchor` is the range anchored at the render's heads, as
/// `DocEvent::Anchor` anchors one, and is `None` for an empty range. A
/// removal and a binary save carry no anchor and name `0..0`, line 1 of an
/// empty text.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Touched {
    /// The range anchored at the render's heads; `None` for an empty one.
    pub anchor: Option<RangeAnchor>,
    /// The range's first code point.
    pub start: usize,
    /// The code point after its last.
    pub end: usize,
    /// The range's lines, as a reader is shown them.
    pub lines: LineName,
    /// What the save did there.
    pub op: TouchOp,
}

/// What a render of a derived document hid (row 180).
///
/// The document has two or more heads, and the state at the greatest of
/// them by `ChangeHash` bytes has `/meta/derived` true, so every reader is
/// shown the text at that head alone. Every peer computes the same value
/// from the document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Superseded {
    /// The head whose text is shown.
    #[serde(with = "crate::cbor::hash")]
    pub shown: ChangeHash,
    /// Each other head whose text differs from the shown text, in head
    /// order, with its change's signer (decision 125's records).
    pub hidden: Vec<HiddenHead>,
}

/// One head a derived document's render hid ([`Superseded`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HiddenHead {
    /// The hidden head.
    #[serde(with = "crate::cbor::hash")]
    pub head: ChangeHash,
    /// The signer its change's record names.
    pub author: ParticipantKey,
}

/// How a save changed its file (decision 165).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TouchOp {
    /// Every splice only inserted, or the save created the file.
    Adding,
    /// The splices inserted and deleted, a binary file became text, or a
    /// binary save.
    Changing,
    /// Every splice only deleted, or the file was removed.
    Removing,
}

/// The answer to a [`ResolveAnchors`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Resolved {
    pub req: RequestId,
    pub outcome: ResolvedOutcome,
}

/// What a [`ResolveAnchors`] found.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolvedOutcome {
    /// Each anchor's span at `heads`, in order; `None` while the document
    /// lacks an anchor's heads or the anchor names characters not visible
    /// at them. `lines` names each span, in the same order, as a reader is
    /// shown it, from the text at `heads` (`lines::name`; a collapsed span
    /// names the line its gap is in), and is `None` where the span is
    /// (decision 165).
    Spans {
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        spans: Vec<Option<AnchorSpan>>,
        /// Each span's name, in order.
        lines: Vec<Option<LineName>>,
    },
    /// `Failed` when the document has no text to resolve against, or is
    /// closed.
    Refused(RejectReason),
}

/// Bytes to append to this document's signature sidecar under
/// `.lvcs/docs/` (design §11), beside its saves: [`ChangeSig`]s, each one
/// canonical CBOR item, concatenated (a CBOR sequence; [`encode_sigs`]),
/// which a load reads back keyed by change hash (`LoadDoc::sigs`). The
/// actor emits it for every signature it learns: those of an applied sync
/// message before the `Persist` of the changes they cover, so a stored
/// remote change always has its signature beside it, and a [`Signed`] one
/// after its change's `Persist` (a reload asks again for any own change
/// the sidecar lacks, [`SignRequest`]). The sidecar is only appended to:
/// a signature is never superseded, a duplicate is harmless, and
/// compacting the saves leaves it as it is (chosen at the M3 freeze).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SigSave {
    #[serde(with = "crate::cbor::bytes")]
    pub bytes: Vec<u8>,
}

/// The participant behind each Automerge actor of the changes this
/// document holds a verified [`ChangeSig`] for: the signature's signer
/// (an actor whose changes carry several signers, which only a faulty
/// peer's can, keeps the signer of its first change in `seq` order).
/// Emitted after every applied change that adds a pair, and on `Render`.
/// Every verified change's actor id is bound to its signer (decision 125:
/// `AuthorId::participant` or `AuthorId::granted`, or an import's), so each
/// actor id has exactly one possible signer and the pairs are exact for
/// verified changes: the daemon merges every document's pairs into one
/// map, which names remote authors in `MergeRequest.authors` and so in
/// `Conflict.authors` (`lvcs-ws`), beside its own participants' actor ids
/// from its key store and their grants, and the protected-path gate reads
/// it (decision 122).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signers {
    pub signers: BTreeMap<AuthorId, ParticipantKey>,
}

/// One stored document as `lvcs audit` checks it (design §12, teardown
/// invariant 3): its spec, its saves (`LoadDoc::saves`), and its signature
/// sidecar (`LoadDoc::sigs`).
#[derive(Debug, Clone, Copy)]
pub struct StoredDoc<'a> {
    pub spec: &'a DocSpec,
    pub saves: &'a [u8],
    pub sigs: &'a [u8],
}

/// What [`verify_history`] found in a document that passed: how many
/// changes it checked, genesis included.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Audited {
    pub changes: u64,
}

/// The first change of a document, in the saves' order, that fails the
/// audit, and why; `change` is `None` when the saves or the sidecar don't
/// decode (`Malformed`), or when the audit can't run (`Unavailable`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuditViolation {
    pub change: Option<ChangeHash>,
    pub reason: SyncRejection,
}

/// One change of a `file` or override document's history, as
/// [`history_texts`] reads it: the change, and the text the document
/// renders at it (its heads being that change alone).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeText {
    pub change: ChangeHash,
    pub text: String,
}

/// The `.lvcs/caps` document's render, after every applied change and on
/// `Render`: the root its seed holds (`None` while it holds only genesis),
/// every signed grant whose signature verifies, by id, the signed
/// allowlist, every `/allowlist` key that is a hash in `ID_ENCODING`
/// (decision 128), and the revocations, every `/revoked` key that is a
/// grant id in `ID_ENCODING` with its revocation time (decision 148), the
/// smallest `Int` among concurrent values when revocations of one grant
/// conflict, so the earliest one counts on every peer. The shell feeds it
/// to every document actor ([`GrantsKnown`], `root` only when its project
/// id is this daemon's), summarized per signer, to the gossip actor, and
/// its allowlist to the secret scan, as `lvcs audit` does.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapsRendered {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub root: Option<ProjectRoot>,
    pub grants: BTreeMap<GrantId, SignedGrant>,
    pub allowlist: BTreeSet<SecretHash>,
    pub revoked: BTreeMap<GrantId, i64>,
}