Docs

doc-events

Source: crates/lvcs-doc/src/actor/events.rs.

/// The Automerge actor id a change is written under (design §3), bound to
/// its signer (decisions 125 and 146): [`AuthorId::granted`] under a grant
/// whose terms name a quota, [`AuthorId::participant`] for every other
/// change, the owner's included, each with one implementation, here. Import and
/// caps-seed changes keep their `IMPORT_DOMAIN` actor ids. The verifier
/// refuses a change whose actor id isn't the derivation its record names,
/// so each actor id has exactly one possible signer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct AuthorId(#[serde(with = "crate::cbor::fixed")] pub [u8; 16]);

/// A participant's Ed25519 public key (`lvcs_vocab::ids::SIGNATURE`),
/// written in `ID_ENCODING` on the API. Workspace ids, override ids, bundle
/// ids, and thread messages name participants by this key, never by
/// [`AuthorId`] (design §8, §9).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ParticipantKey(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// Who caused a change and when: event data, never read inside an action
/// (design §3). `time_ms` becomes the Automerge commit time, which grant
/// windows are checked against (§15). The shell builds it at request time
/// with the live grant it checked the write against (decision 125):
/// `signer` and `grant` are what the change's record names and its
/// `ChangeSig` must sign, `device` the raw Ed25519 key of this daemon, and
/// `author` the actor id their derivation gives ([`AuthorId`]). The
/// document writes the record ([`ChangeMessage`]) from it in one place.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Origin {
    pub author: AuthorId,
    pub signer: ParticipantKey,
    pub grant: GrantId,
    pub device: DeviceKey,
    /// Unix time in milliseconds, read by the shell when it accepted the
    /// request. The commit time is `time_ms / 1000`, floored, because
    /// Automerge commit times are in seconds (design §3).
    pub time_ms: i64,
}

/// Correlates a request with its output; chosen by the daemon shell, unique
/// per connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RequestId(pub u64);

/// Replace `delete` code points at `pos` with `insert`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Splice {
    pub pos: usize,
    pub delete: usize,
    pub insert: String,
}

/// How a save treats the file's `/meta/derived` flag (row 180).
///
/// The saving daemon sets it from its classification of the path. The text
/// kind treats a [`DiskEdit`] or [`Edit`] as derived when it is `Mark`, or
/// `Keep` while `/meta/derived` is true at the document's live heads, and a
/// [`BundleEdit`] as derived when it is `Mark`. A derived text save commits
/// one whole-text replacement, based where its author last looked (never
/// at the live heads), and puts `/meta/derived` true beside `/meta/format`
/// and `/meta/deleted`. `Unmark` puts it false and saves as a text save
/// always does; `Keep` on a document that isn't derived saves the same
/// way, so a peer with a stale view never re-enables text merging. A
/// binary save is whole already: it commits as it always does and puts
/// the flag as a text save would. An executable-bit change or a removal
/// puts no flag.
///
/// [`BundleEdit`]: crate::actor::BundleEdit
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DerivedFlag {
    /// No rule applies: the document's own flag decides.
    Keep,
    /// A positive rule marks the path derived.
    Mark,
    /// An explicit negation marks it not derived.
    Unmark,
}

/// Splice `/content`. Every splice's positions are Unicode code points in
/// the text at `base` (the heads the client last saw); splices are sorted by
/// `pos` and don't overlap. The document actor applies them on the author's
/// own branch (design §3, decisions 75, 78, and 125): it forks at `base`
/// plus the author's latest change of any kind (the heads of the last
/// change under every actor whose record names `origin.signer`, since the
/// actor id under a grant with a quota changes at each renewal and a
/// participant's differs per device), with the fork's actor id set to `origin.author`, translates the
/// splices from the text at `base`
/// onto the fork through element-id anchors (decision 76), commits there at
/// `origin.time_ms / 1000` seconds, and merges the fork into the live
/// document. When `base` plus the author's latest change already is the live
/// heads, it commits on the live document directly. The translation crosses
/// only the changes since `base` on the author's branch: each deleted base
/// character is deleted only if still visible, and each insert is anchored
/// on the base character before it (`MoveCursor::Before`: the nearest
/// earlier base character still visible) or on the start. The same change
/// deletes and re-puts `/meta/format` (`text`) and `/meta/deleted` (false),
/// so both are always recorded (decision 76). An unknown or empty `base`
/// answers [`DocOutput::Rejected`] with [`RejectReason::UnknownBase`];
/// splices that are unsorted, overlapping, or out of bounds at a known
/// `base` answer it with [`RejectReason::OutOfBounds`] (decision 81).
///
/// A derived edit ([`DerivedFlag`]) applies its splices to the text the
/// document renders at `base` and commits the result whole there, after
/// the same bounds check against that text; one that changes nothing
/// there commits nothing and answers `Changed` with no splices.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Edit {
    pub req: RequestId,
    pub origin: Origin,
    #[serde(with = "crate::cbor::heads")]
    pub base: Vec<ChangeHash>,
    pub splices: Vec<Splice>,
    /// How the edit treats `/meta/derived`.
    pub derived: DerivedFlag,
}

/// Read the rendered body, or the value at `pointer` (RFC 6901) into the
/// document's Automerge structure. A thread renders no file: with no
/// pointer it reads its whole Automerge structure (design §4). Reads go
/// through the mailbox and are journaled like any event. A pointer that
/// names nothing answers [`DocOutput::Rejected`] with
/// [`RejectReason::Failed`] (decision 81).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Read {
    pub req: RequestId,
    pub pointer: Option<String>,
}

/// Release the document; the actor stops.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Close;

/// A change from the mirrored directory (decisions 33, 47, 54, 63, 75, and
/// 78), authored by the daemon's default participant (decision 64). Every
/// kind of change commits on the author's own branch, at its base plus the
/// author's latest change of any kind (decision 78).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiskEdit {
    pub origin: Origin,
    /// Assigned by the mirror, increasing per mirror. Handling this edit
    /// emits exactly one [`DocOutput::Rendered`], with this stamp as
    /// `after`, whether it committed, changed nothing, or faulted before
    /// rendering, so the mirror can tell which save a render follows and
    /// its pending-save rule always completes (decisions 75 and 78).
    pub stamp: u64,
    pub change: DiskChange,
    /// How the save treats `/meta/derived`: a derived text save commits
    /// the whole `new_text` at the base the rule on [`DiskChange::Text`]
    /// chooses ([`DerivedFlag`]).
    pub derived: DerivedFlag,
}

/// What changed on disk. A change that leaves the file as it was where it
/// would commit (see each variant) makes no commit, so a save reported
/// twice is absorbed (decision 75), unless it is a text or binary save and
/// the file is deleted at the live heads: then it commits, and its re-put
/// `/meta/deleted` (false) restores the file, so a recreate after another
/// participant's removal is never lost (decision 78). An executable or
/// removal change puts no content and restores nothing, so it has no such
/// exception.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiskChange {
    /// A text save (design §8, decisions 47, 66, 75, 78, 79, and 131). When
    /// the file renders as exactly `new_text` at the live heads (not deleted,
    /// `/meta/format` not `binary`, `/content` equal to `new_text`), the
    /// save makes no commit and is not reported stale, whatever base it
    /// would choose, and only renders (decision 79): a rescan of a file the
    /// mirror has no disk base for, such as a freshly imported one, never
    /// inserts its content a second time. A file deleted or rendering as
    /// binary at the live heads goes through the base choice below even
    /// when its `/content` is `new_text`. Otherwise the document actor
    /// puts the heads of the author's latest change in the document, of
    /// any kind (any actor whose record names `origin.signer`, decision
    /// 125), ahead of `candidates` (dropping a later
    /// candidate with the same heads, compared as sets): every change the
    /// author makes forks at its base plus the author's latest change, so
    /// the text there is what the author last saw plus what it wrote. It
    /// skips candidates it doesn't have, and picks the one whose text
    /// differs least from `new_text` (inserted plus deleted code points),
    /// ties to the earliest unless a later one's version is older (the
    /// earlier one's history holds it), which then wins: a stale save is
    /// never taken as made from the newer version (decision 296); an empty
    /// `new_text` takes the first, the
    /// newest, since against it each one's distance is its own length and
    /// the oldest, shortest one would win (decision 131). With none usable
    /// (the author has no change in the document, and the mirror no disk
    /// base the document has, as for a new file) the base is the genesis
    /// heads, since the author then saw nothing, so content another
    /// participant created concurrently is kept and merges as concurrent
    /// text (decision 78). When the base's text is
    /// `new_text` and the file renders as text (not deleted, `/meta/format`
    /// not `binary`) where the save would commit, at the base plus the
    /// author's latest change, nothing changes relative to the base on the
    /// author's branch, and no commit is made unless the file is deleted at
    /// the live heads: another participant's later binary save stays in
    /// place. Otherwise it diffs the base's text against `new_text` and applies the
    /// splices like an [`Edit`] with that base, on the author's own branch,
    /// so the save lands as a concurrent change; that change deletes and
    /// re-puts `/meta/format` (`text`) and `/meta/deleted` (false)
    /// (decision 76), which restores a file the author removed or saved as
    /// binary since the base, or anyone removed (decision 78).
    /// A save that reaches the base choice is reported with
    /// [`DocOutput::StaleSave`], changed or not, when the base is a
    /// candidate other than the first, the first candidate while its
    /// history lacks the author's latest change, the author's latest
    /// change while `candidates` is non-empty and the history of its first
    /// entry holds a change that is not part of the latest change's
    /// history, or the genesis heads while the document held changes
    /// beyond genesis when the save arrived. Before it commits, a save
    /// whose base is not the genesis heads, made from an older version
    /// than the text the document shows, is refused when the lines it
    /// changes overlap lines changed since that version, measured against
    /// the base plus the author's latest change: no commit,
    /// [`DocOutput::SaveRefused`], and a render of the document as it is
    /// (decision 296).
    Text {
        new_text: String,
        /// Recent disk bases for the file, newest first: the heads of the
        /// mirror's writes still in flight, then those of the last
        /// `DISK_BASE_RING` confirmed writes (decision 75).
        #[serde(with = "crate::cbor::head_lists")]
        candidates: Vec<Vec<ChangeHash>>,
        /// The heads of the version a hook read the file at (`track_read`,
        /// decision 117), which the mirror recorded and this save takes.
        /// When the document holds them, they equal or descend from the
        /// author's latest change, and the save is not strictly nearer the
        /// first candidate's text than theirs (decision 241: a non-empty
        /// save only), they are the save's base, chosen before any other
        /// candidate and never stale: the save is exactly what the author
        /// read plus what it wrote. Otherwise the rule above applies as if
        /// there were none: a tool that read the disk again before it
        /// wrote saved the renders since the read, and a diff against the
        /// read would apply them a second time.
        #[serde(with = "crate::cbor::opt_heads")]
        loaded: Option<Vec<ChangeHash>>,
    },
    /// A save of bytes that are not valid UTF-8: replaces `/bytes`
    /// (last writer wins), deleting and re-putting `/meta/format`
    /// (`binary`) and `/meta/deleted` (false) (decision 76), committed on
    /// `origin.author`'s own branch at `base` (decision 78). No commit when
    /// the file already renders exactly these bytes there and is not
    /// deleted at the live heads.
    Binary {
        #[serde(with = "crate::cbor::bytes")]
        bytes: Vec<u8>,
        /// The mirror's newest known disk state of the file (decision 78);
        /// empty when it has none. An empty base, or one the document
        /// doesn't have, is the genesis heads: the author then saw nothing.
        #[serde(with = "crate::cbor::heads")]
        base: Vec<ChangeHash>,
    },
    /// The executable bit changed: sets `/meta/executable`, committed on
    /// `origin.author`'s own branch at `base` (decision 78). No commit when
    /// the bit already has this value there.
    Executable {
        executable: bool,
        /// As for [`DiskChange::Binary`].
        #[serde(with = "crate::cbor::heads")]
        base: Vec<ChangeHash>,
    },
    /// The file was removed: sets `/meta/deleted`, committed on
    /// `origin.author`'s own branch at `base` (decision 78), so another
    /// participant's edit the author never saw keeps the file. No commit
    /// when the file is already deleted there.
    Removed {
        /// As for [`DiskChange::Binary`].
        #[serde(with = "crate::cbor::heads")]
        base: Vec<ChangeHash>,
    },
}

/// A file as a document renders it. `/meta/format` selects `/content` or
/// `/bytes` (last writer wins between them).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileBody {
    Text(String),
    Binary(#[serde(with = "crate::cbor::bytes")] Vec<u8>),
    /// Every concurrent `/meta/deleted` value is true; a genesis-only
    /// document is deleted.
    Deleted,
}

/// Names a daemon-wide fact in the local journal (decision 233): ids rise
/// within a daemon run, and each daemon run starts them again at 0.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FactId(pub u64);

/// A daemon-wide fact the shell hands to many documents (decision 233):
/// the journal holds it once, as a record no actor's, and each
/// recipient's entry names `id` in place of the event. The actor reads
/// `value` alone, so the id never reaches a context or a fingerprint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Fact<T> {
    /// The journal's name for it.
    pub id: FactId,
    /// The fact.
    pub value: T,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocEvent {
    Edit(Edit),
    Read(Read),
    DiskEdit(DiskEdit),
    /// Emit [`DocOutput::Rendered`] for the current heads, for example to a
    /// mirror that restarted (design §2).
    Render,
    /// The compaction tick, every `COMPACT_SECS`: emit a compacted
    /// [`DocOutput::Persist`] when incremental saves were emitted since the
    /// last one (design §3).
    Compact,
    Close,
    WatchAnchors(WatchAnchors),
    Claim(Claim),
    ReadAt(ReadAt),
    BundleEdit(BundleEdit),
    /// Boxed: the claim it carries is much larger than any other event.
    OverrideInit(Box<OverrideInit>),
    IndexInit(IndexInit),
    IndexChange(IndexChange),
    RegisterWorkspace(RegisterWorkspace),
    RecordBundle(RecordBundle),
    /// Boxed, like [`DocEvent::OverrideInit`]: its anchor, addressees, and
    /// first message make it much larger than most events.
    ThreadOpen(Box<ThreadOpen>),
    ThreadAppend(ThreadAppend),
    /// A write to an issue (decision 304).
    IssueWrite(IssueWrite),
    /// Start syncing with a peer (decision 30; `M3.doc`, `M3.sync`).
    PeerJoined(PeerJoined),
    /// Stop syncing with a peer; a daemon-wide fact (decision 233).
    PeerLeft(Fact<PeerLeft>),
    /// A sync message from a peer, verified whole before Automerge sees it
    /// (decision 68).
    SyncIn(SyncIn),
    /// The daemon's signature for a committed change (design §11).
    Signed(Signed),
    /// The grants every change is verified against (from `.lvcs/caps`);
    /// a daemon-wide fact (decision 233).
    Grants(Fact<GrantsKnown>),
    /// The caps document settled with a peer: that peer's deferred sync
    /// messages get their last check (decision 108); a daemon-wide fact
    /// (decision 233).
    CapsSettled(Fact<CapsSettled>),
    /// Add a signed grant to the `.lvcs/caps` document. Boxed, like
    /// [`DocEvent::OverrideInit`].
    IssueGrant(Box<IssueGrant>),
    /// A bundle's target heads in this document (decision 48).
    BundleTargets(BundleTargets),
    /// A bundle is complete everywhere: render past it (decision 48).
    BundleVisible(BundleVisible),
    /// Show a derived file at a counted generation's heads (row 180).
    Pin(Pin),
    /// Check two concurrent bundles for overlap here (decision 65). Boxed:
    /// its two sides are much larger than most events.
    CheckOverlap(Box<CheckOverlap>),
    /// Resolve cursor pairs at the current heads (design §5).
    Resolve(ResolveAnchors),
    /// Anchor a notification's range at past heads (design §5; `M3.doc`).
    Anchor(AnchorRange),
    /// Does one set of heads descend from another (decision 120)?
    Descends(DescendsQuery),
    /// Where a protected file's approved heads start (decision 120).
    Baseline(BaselineQuery),
    /// Put an entry into a channel document (decision 119). Boxed, like
    /// [`DocEvent::OverrideInit`]: an attestation carries a frontier.
    ChannelPut(Box<ChannelPut>),
    /// Add a hash to the `.lvcs/caps` document's signed allowlist
    /// (decision 128).
    AllowSecret(AllowSecret),
    /// Revoke grants in the `.lvcs/caps` document (decision 148). Boxed,
    /// like [`DocEvent::IssueGrant`].
    RevokeGrant(Box<RevokeGrant>),
}

impl DocEvent {
    /// The daemon-wide fact this event delivers, when it is one (decision
    /// 233): the journal names it in place of the event.
    #[must_use]
    pub const fn fact(&self) -> Option<FactId> {
        if let Self::Grants(Fact { id, .. })
        | Self::CapsSettled(Fact { id, .. })
        | Self::PeerLeft(Fact { id, .. }) = self
        {
            Some(*id)
        } else {
            None
        }
    }
}

/// Outputs a document actor hands to its effect sink. An applied change
/// emits, in order: `Persist`, one `SignRequest` per change it committed
/// (design §11), `Changed`, `StaleSave` (stale text saves only),
/// `Rendered`. A [`DiskEdit`] that changes nothing emits only
/// `StaleSave` (stale text saves only) and `Rendered`. A [`DiskEdit`] whose
/// handling faults emits what it emitted before the fault (at most
/// `Persist`, `Changed`, and `StaleSave`, in that order), then `Rendered`;
/// only a render that faults itself has no body to send (decision 78).
/// An override document emits `OverrideRendered` wherever a file document
/// emits `Rendered`. Every render is preceded by one `AnchorsResolved` per
/// watcher, in watcher order, at the render's heads (decision 84).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocOutput {
    /// A change was applied. `splices` turn the live text at the previous
    /// heads into the text at `heads`, computed from the two (a
    /// deterministic diff, decision 75) rather than echoed from the request,
    /// so subscribers can patch their buffers; `req` is set when the change
    /// answers a request.
    Changed {
        req: Option<RequestId>,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        splices: Vec<Splice>,
    },
    /// A request was refused; `reason` says why, so the daemon can answer
    /// `UNKNOWN_BASE` and `OUT_OF_BOUNDS` correctly (decision 81).
    Rejected {
        req: RequestId,
        reason: RejectReason,
    },
    /// Bytes to append to this document's store under `.lvcs/docs/`
    /// (decision 30): an incremental save, or a full compacted save that
    /// replaces earlier ones.
    Persist {
        #[serde(with = "crate::cbor::bytes")]
        bytes: Vec<u8>,
        compacted: bool,
    },
    /// The whole file at `heads`, emitted after every applied change, once
    /// for every [`DiskEdit`] (also one that changes nothing or faults,
    /// decision 78), and on [`DocEvent::Render`], for the mirror actor
    /// (decisions 33, 63, and 75).
    /// `after` is the `stamp` of the [`DiskEdit`] whose handling produced
    /// this render, and `None` for any other render (decision 75).
    /// `touched` is what that edit changed when it committed a text save,
    /// a binary save, or a removal, at `heads`, and `None` for every other
    /// render, an executable-bit change included (decision 165).
    Rendered {
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        body: FileBody,
        executable: bool,
        after: Option<u64>,
        touched: Option<Touched>,
        /// Set when the render shows one head of a derived document alone
        /// (row 180's winner rule): the head shown, and each other head
        /// whose text differs, with its signer. `None` for every other
        /// render.
        superseded: Option<Superseded>,
    },
    /// The answer to a [`Read`].
    ReadDone {
        req: RequestId,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        value: ReadValue,
    },
    /// A text save was applied, or found unchanged, on a possibly stale base
    /// (design §8, decisions 66 and 75; the rule is on [`DiskChange::Text`]),
    /// for the daemon's `stale_save` push. `chosen_base` is the base the
    /// save was diffed against; `newest_base` is the heads of the author's
    /// latest change when `chosen_base` is the save's first candidate,
    /// otherwise the save's first candidate (the mirror's newest disk base),
    /// or empty when the save had none; `heads` is the document's heads
    /// after the save.
    StaleSave {
        #[serde(with = "crate::cbor::heads")]
        chosen_base: Vec<ChangeHash>,
        #[serde(with = "crate::cbor::heads")]
        newest_base: Vec<ChangeHash>,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
    },
    /// A text save was refused, uncommitted (the stale-overlap row
    /// revising decision 49): it was made from an older version of the
    /// file than the one the document shows, and the lines it changes
    /// overlap lines changed since. Emitted just before the save's
    /// `Rendered`, which renders the document as it is, for the daemon's
    /// `refused_save` notification.
    SaveRefused(StaleOverlap),
    /// An [`Edit`] to a `file` document was refused, uncommitted (decision
    /// 310): its base is older than the text the document shows, and the
    /// lines it changes overlap lines another participant changed since
    /// that base, measured as a stale save is (decision 296). It answers
    /// the edit's request in place of `Changed` or `Rejected`.
    EditRefused {
        req: RequestId,
        overlap: StaleOverlap,
    },
    AnchorsResolved(AnchorsResolved),
    Claimed(Claimed),
    ReadAtDone(ReadAtDone),
    /// An override document's render, where a file document emits
    /// [`DocOutput::Rendered`] (decision 84).
    OverrideRendered(OverrideRendered),
    /// A workspace index document's render, after every applied change and
    /// on [`DocEvent::Render`] (it emits no `Changed`: it has no text).
    IndexRendered(IndexRendered),
    /// The registry document's render, after every applied change and on
    /// [`DocEvent::Render`] (it emits no `Changed`: it has no text).
    RegistryRendered(RegistryRendered),
    /// A sync message for a peer (decision 30; `M3.doc`, `M3.sync`).
    SyncOut(SyncOut),
    /// Nothing more to send a peer until it answers (decision 68).
    SyncSettled(SyncSettled),
    /// A peer's whole sync message was rejected (decision 68).
    SyncRejected(SyncRejected),
    /// A committed change awaits its signature (design §11).
    SignRequest(SignRequest),
    /// The answer to [`DocEvent::BundleTargets`] (decision 48).
    BundleHeld(BundleHeld),
    /// The answer to [`DocEvent::CheckOverlap`] (decision 65).
    OverlapChecked(OverlapChecked),
    /// The answer to [`DocEvent::Resolve`].
    Resolved(Resolved),
    /// The answer to [`DocEvent::Anchor`].
    Anchored(Anchored),
    /// The `.lvcs/caps` document's render (design §11).
    CapsRendered(CapsRendered),
    /// Signatures to append to the document's signature sidecar (design
    /// §11; `M3.doc`).
    SaveSigs(SigSave),
    /// The signer behind each Automerge actor of the document's verified
    /// changes, for attribution (`M3.doc`, `M3.daemon`).
    Signers(Signers),
    /// The answer to [`DocEvent::Descends`] (decision 120).
    DescendsDone(DescendsDone),
    /// The answer to [`DocEvent::Baseline`] (decision 120).
    BaselineDone(BaselineDone),
    /// A channel document's render (decision 119).
    ChannelRendered(ChannelRendered),
    /// The revocations a peer of the `.lvcs/caps` document holds
    /// (decision 326).
    PeerRevocations(PeerRevocations),
}

/// Why a request answered [`DocOutput::Rejected`] (decision 81). Only an
/// [`Edit`] is rejected for its base or splices; a [`DiskEdit`] is never
/// rejected (one with an unknown base commits at the genesis heads,
/// decision 78).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RejectReason {
    /// The edit's `base` is empty or names heads the document doesn't have.
    UnknownBase,
    /// The edit's splices are unsorted, overlapping, or out of bounds at its
    /// known `base`.
    OutOfBounds,
    /// Anything else: Automerge or another callback faulted while answering
    /// the request, a read pointer names nothing, or the actor was closed.
    Failed,
}

/// What a [`Read`] returns.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadValue {
    /// No pointer, in a file or override document: the rendered file.
    Body(FileBody),
    /// The value at the pointer, as JSON text; with no pointer, in a
    /// thread, the whole document.
    Json(String),
}

/// The journaled fingerprint of a document actor, the one content check the
/// journal keeps beside each entry's trace (decision 286).
///
/// It holds the document's heads, which replay loads an incarnation at,
/// how much of the signature sidecar the incarnation loaded, which replay
/// loads the same prefix of, and a digest of the durable context
/// (decisions 32, 84, 222, and 286).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocFingerprint {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// The bytes of the signature sidecar the incarnation loaded (after
    /// its format header). The sidecar is only ever appended to, so replay
    /// loads this prefix of it: the signatures the incarnation started
    /// with, not ones a later `SaveSigs` appended, which its trace would
    /// otherwise show as signatures no longer awaited (decision 286).
    pub sigs: u64,
    /// The digest covers exactly the heads, the watch map, the grants
    /// known, the deferred sync messages, the bundle gate, and the pin
    /// (decisions 32, 84, and 111, row 180): the `HASH` of the canonical
    /// CBOR array of five items. First the heads, each a byte string, in Automerge's sorted
    /// order. Then the watch map: every [`WatchAnchors`] set in force, a
    /// map from watcher id to a map from override id to its anchor. Then
    /// the sync, a map of two keys: `known`, the `HASH` of the last
    /// `GrantsKnown`'s canonical CBOR (null before the first), and
    /// `deferred`, an array holding, for each `SyncIn` held deferred
    /// (decision 108) in arrival order, a map whose one key `digest` is
    /// the `HASH` of that event's canonical CBOR, its tag included; each
    /// digest is taken when its event arrives, so no state re-encodes
    /// them. Last the bundle gate (decision 48), a map of the bundles
    /// `hidden`, made `visible`, registered (`targets`, each with its
    /// target `heads` and whether it is `held`), and `merged` here in this
    /// incarnation. Then the [`Pin`]'s heads, an array of byte strings, or
    /// null with none. A control document has no watches, an open gate,
    /// and no pin, so those items are always empty there. So the journal covers anchor
    /// watches, what sync waits on, and what the gate shows as well as
    /// content. Nothing else of the context is covered: the signature
    /// sidecar (`sigs`, the changes still `unsigned` and those `asked`
    /// for, and the `signers` pairs), which a load reads from the sidecar
    /// and replay loads the same prefix of (`sigs` above); each peer's Automerge sync
    /// state with its tag (decision 110), which has no canonical full
    /// encoding and is rebuilt from the incarnation's own events; and the
    /// save bookkeeping (heads saved, increments since a compaction) and
    /// the outputs, which the heads and the events already decide.
    #[serde(with = "crate::cbor::fixed")]
    pub digest: [u8; 32],
}

/// Show a derived `file` document at a counted generation's heads (row 180).
///
/// `heads` are the heads of the counted generation main's visible
/// frontier shows, or `None` for no generation. While set, every render, read, and read at heads of a
/// version that includes `heads` shows the file at them (the greatest of
/// them alone when several), hiding every other head as the winner rule
/// does and naming each in `Rendered.superseded`; a version that doesn't
/// include them, or a document that doesn't hold them, is shown by the
/// winner rule. A pin that changes what the live document shows renders.
/// Every other kind ignores it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pin {
    #[serde(with = "crate::cbor::opt_heads")]
    pub heads: Option<Vec<ChangeHash>>,
}

/// The state label of the machine a document actor runs, which its kind's
/// layout selects (decision 82): `file` and `override` documents run the
/// text machine, a workspace index, the registry, a thread, the caps
/// document, and a channel their own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DocLabel {
    Text(DocLifecycleStates),
    Index(IndexLifecycleStates),
    Registry(RegistryLifecycleStates),
    Thread(ThreadLifecycleStates),
    Issue(IssueLifecycleStates),
    Caps(CapsLifecycleStates),
    Channel(ChannelLifecycleStates),
}

impl sml::StateName for DocLabel {
    fn name(&self) -> &'static str {
        match self {
            Self::Text(label) => label.name(),
            Self::Index(label) => label.name(),
            Self::Registry(label) => label.name(),
            Self::Thread(label) => label.name(),
            Self::Issue(label) => label.name(),
            Self::Caps(label) => label.name(),
            Self::Channel(label) => label.name(),
        }
    }
}

/// Does `heads`, a set of this document's heads, equal or descend from
/// `from` (decision 120)? The `channels` actor asks it to decide
/// supersession and approved heads, which only a document can tell. File,
/// override, workspace index, and registry documents answer with exactly
/// one [`DescendsDone`]: [`Descent::Yes`] when every change in `from` is in
/// the history of `heads`, `No` when one is not, and `Unknown` when either
/// set is empty or names a change the document doesn't hold. Every other
/// kind, and a closed actor, answers `Unknown`. `req` is the shell's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescendsQuery {
    pub req: RequestId,
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    #[serde(with = "crate::cbor::heads")]
    pub from: Vec<ChangeHash>,
}

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

/// Where a protected file's approved heads start (decisions 120 and 122):
/// a `file` document answers with exactly one [`BaselineDone`] holding the
/// heads after its genesis and its owner-signed import change (the record
/// `[owner, GrantId::OWNER, null, null]` at time 0 on top of genesis,
/// decision 125), or its genesis heads when it has no import. Every other
/// kind, and a closed actor, answers `heads: None`. `req` is the shell's.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaselineQuery {
    pub req: RequestId,
}

/// The answer to a [`BaselineQuery`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaselineDone {
    pub req: RequestId,
    #[serde(with = "crate::cbor::opt_heads")]
    pub heads: Option<Vec<ChangeHash>>,
}