LVCS

Docs · generated from code

Contracts

Copied verbatim from source regions marked // docgen:begin <name> / // docgen:end.

inbox

Source: crates/lvcs/src/adapter/inbox.rs.

/// The inbox actor's events, all journaled.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum InboxEvent {
    /// What `participant` is interested in: sent first for each local
    /// participant and again when the caps render changes.
    Interest(Interest),
    /// A candidate notification for `participant`, forwarded by the shell
    /// (boxed: it is most of the event's size).
    Item(Box<Item>),
    /// Sent once at start: `participant`'s open asks from thread documents
    /// and its stale saves since each file's last clean save.
    Refill(Refill),
    /// List `participant`'s notifications, answered by one
    /// [`InboxOutput::Listed`].
    List(List),
    /// Read the content of `participant`'s notifications `ids`, answered
    /// by one [`InboxOutput::Contents`].
    Read(Read),
    /// A check notice for `participant`, which opens, replaces, or closes
    /// its streak on the check (decision 193); boxed, since it carries the
    /// notice's notification.
    Check(Box<Check>),
    /// List `participant`'s open check streaks, answered by one
    /// [`InboxOutput::Checked`].
    Checks(Checks),
    /// `participant`'s own notification as this daemon announced it,
    /// which is never offered to it (decision 239) but closes its open
    /// entries as an own `Item` does; boxed, since it carries the
    /// notification.
    Sent(Box<Sent>),
}

/// `patterns` is `**` for the owner and for a participant holding a live
/// grant whose paths include `**`, and otherwise the paths of the
/// participant's live `Write` grants, matched with lvcs-doc's `paths`
/// matcher, compiled once per `Interest` and kept out of the fingerprint.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Interest {
    pub(crate) participant: ParticipantKey,
    pub(crate) patterns: Vec<String>,
}

/// One notification as the shell offers it: its metadata and its content.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Received {
    /// What `notifications` lists of it.
    pub(crate) notification: Notification,
    /// What `notifications.read` returns of it.
    pub(crate) content: NotificationContent,
}

/// A candidate notification for one participant.
///
/// Kept only when it names `participant` directly or its address matches
/// the participant's interest, and when its signed `at_ms` lies in
/// [`now_ms` − `BULLETIN_SECS`, `now_ms` + `INBOX_SKEW_MS`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Item {
    /// The local participant it is offered to.
    pub(crate) participant: ParticipantKey,
    /// The notification.
    pub(crate) received: Received,
    /// Its arrival time, which orders listing and expires holds.
    pub(crate) now_ms: i64,
}

/// `participant`'s open asks and stale saves at start, from the thread
/// documents and `.lvcs/stale-saves.json`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Refill {
    /// The local participant.
    pub(crate) participant: ParticipantKey,
    /// Its open asks from the thread documents.
    pub(crate) asks: Vec<Received>,
    /// Its stale saves from `.lvcs/stale-saves.json`.
    pub(crate) stale_saves: Vec<Received>,
    /// The shell's clock at start.
    pub(crate) now_ms: i64,
}

/// List a participant's notifications.
///
/// Its open ones, then its unread ones, as the module docs order them.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct List {
    /// The `notifications` or `track_write` request it answers.
    pub(crate) req: RequestId,
    /// The participant.
    pub(crate) participant: ParticipantKey,
    /// Only unread notifications sent at or after this time (every one
    /// when `None`).
    pub(crate) since: Option<i64>,
    /// The most bytes of their JSON listed (no bound when `None`).
    pub(crate) max_bytes: Option<u32>,
    /// Whether it is a hook's hint (decision 242): after it, every
    /// notification the participant holds counts as shown.
    pub(crate) hint: bool,
    /// The shell's clock when the request came.
    pub(crate) now_ms: i64,
}

/// Read the content of `participant`'s notifications `ids` for the
/// `notifications.read` request `req`, marking them read.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Read {
    /// The `notifications.read` request it answers.
    pub(crate) req: RequestId,
    /// The participant.
    pub(crate) participant: ParticipantKey,
    /// The notification ids to read.
    pub(crate) ids: Vec<String>,
    /// The shell's clock when the request came.
    pub(crate) now_ms: i64,
}

/// A runner's check notice for `participant` (decision 193): a
/// `check_failed` when `failed`, else a `check_passed`, on check `check`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Check {
    /// The local participant it names.
    pub(crate) participant: ParticipantKey,
    /// The check's id, in `ID_ENCODING`.
    pub(crate) id: String,
    /// Whether it is a `check_failed`.
    pub(crate) failed: bool,
    /// Its notification, as the `Item` that follows offers it.
    pub(crate) notification: Notification,
}

/// `participant`'s own notification `notification`, announced by this
/// daemon and offered to every other local participant (decision 239).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Sent {
    /// The local participant that sent it.
    pub(crate) participant: ParticipantKey,
    /// Its metadata.
    pub(crate) notification: Notification,
}

/// List `participant`'s open check streaks for the request `req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Checks {
    /// The request it answers.
    pub(crate) req: RequestId,
    /// The participant.
    pub(crate) participant: ParticipantKey,
}

/// What the inbox actor asks the shell to do.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum InboxOutput {
    /// The answer to `List` `req`: `open` first, then the unread
    /// `notifications` that fit, and how many didn't (`omitted`).
    Listed {
        /// The request.
        req: RequestId,
        /// The unread notifications not open that fit.
        notifications: Vec<Notification>,
        /// The open asks and holds.
        open: Vec<Notification>,
        /// How many unread notifications didn't fit.
        omitted: u32,
        /// The participant's open check streaks, newest first.
        checks: Vec<Notification>,
        /// The ids listed, open ones included, that arrived after the
        /// participant's last hinting `List` (decision 242).
        fresh: Vec<String>,
    },
    /// The answer to `Checks` `req`: the participant's open check streaks,
    /// newest first.
    Checked {
        /// The request.
        req: RequestId,
        /// The participant.
        participant: ParticipantKey,
        /// The open streaks.
        checks: Vec<Notification>,
    },
    /// A `check_failed` opened or replaced `participant`'s streak on a
    /// check.
    StreakOpened {
        /// The participant.
        participant: ParticipantKey,
    },
    /// The answer to `Read` `req`: the content of each named notification
    /// held, and the ids not held.
    Contents {
        /// The request.
        req: RequestId,
        /// The content of each named notification held, in order.
        contents: Vec<NotificationContent>,
        /// The ids not held.
        missing: Vec<String>,
    },
}

adapter

Source: crates/lvcs/src/adapter/mod.rs.

/// The Claude Code hook events `lvcs hook` handles, named as Claude Code
/// names them.
///
/// One op per hook (decision 117): `SessionStart`,
/// `UserPromptSubmit`, and `Stop` call `notifications` (`Stop` reads `open`),
/// `PreToolUse` calls `track_edit`, and `PostToolUse` calls `track_read`
/// after a `Read` and `track_write` after a `Write` or `Edit`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum HookEvent {
    #[value(name = "SessionStart")]
    SessionStart,
    #[value(name = "PreToolUse")]
    PreToolUse,
    #[value(name = "PostToolUse")]
    PostToolUse,
    #[value(name = "UserPromptSubmit")]
    UserPromptSubmit,
    #[value(name = "Stop")]
    Stop,
}

/// One `lvcs hook` run: the event, the participant (`--as`, else
/// `LVCS_PARTICIPANT`; with neither the hook prints nothing), and the
/// project root (`--root`, else `CLAUDE_PROJECT_DIR`, else the first
/// ancestor of the working directory holding `.lvcs/`). The hook connects
/// only when `<root>/SOCKET_PATH` is a socket whose server, by `peer_cred`,
/// is the uid that owns `<root>/.lvcs/`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Hook {
    pub(crate) event: HookEvent,
    pub(crate) participant: Option<String>,
    pub(crate) root: Option<PathBuf>,
}

/// Run `hook`: read the hook JSON on stdin, call its op, and print the hook
/// output JSON or nothing. Always exits 0.
pub(crate) fn run_hook(hook: &Hook) -> ExitCode {
    hook::run(hook);
    ExitCode::SUCCESS
}

api-mcp

Source: crates/lvcs/src/api/mcp/mod.rs.

/// One `lvcs mcp` run: the participant it says `hello` as (`--as`, else
/// `LVCS_PARTICIPANT`, never an owner default; with neither, `lvcs mcp`
/// exits 2 with `lvcs: no participant`) and the project root (`--root`,
/// else the current directory).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct McpOptions {
    pub(crate) participant: String,
    pub(crate) root: Option<PathBuf>,
}

/// Serve MCP on stdin and stdout until stdin ends, then exit 0. With no
/// daemon at the root's `SOCKET_PATH`, or a `hello` the daemon refuses, it
/// prints `lvcs: <reason>` on stderr and exits 1 before reading stdin; it
/// does the same when stdin or stdout fails.
pub(crate) fn serve(options: &McpOptions) -> ExitCode {
    match run(options) {
        Ok(()) => ExitCode::SUCCESS,
        Err(err) => {
            let _ = writeln!(std::io::stderr(), "lvcs: {err}");
            ExitCode::FAILURE
        }
    }
}

api

Source: crates/lvcs/src/api/mod.rs.

/// `hello`: must be the first request on a connection. Every later request
/// on the connection acts as `participant` (a public key in `ID_ENCODING`),
/// whose key the daemon holds under `KEYS_DIR`. Without `participant`, it
/// acts as the one key bound to the connection's uid, else, from the
/// daemon's own user, as the owner; several keys bound to the uid, or none
/// from another uid, are refused `NO_PARTICIPANT` (decision 242). It is decided by custody,
/// the client's OS user (decision 149): from the daemon's own user any
/// held key is accepted; from any other uid, or when either uid is
/// unknown, only a key bound to that uid by `lvcs key new --uid`, and the
/// owner, whose key is never bound, and every other key are refused
/// `NO_PARTICIPANT`. A bound key with no live grant is accepted, and its
/// signed requests are refused `REJECTED` until a grant renders. Custody
/// is fixed when a key is created, so it is never checked again on the
/// connection, and a connection from another uid creates no mirror
/// (decision 134).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HelloParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub participant: Option<String>,
}

/// `open`: start (or find) the document actor for `address`, as
/// `workspace` (a workspace name or id; `main` when omitted) sees it. The
/// same `workspace` field on `read`, `edit`, `subscribe`, and the pushes
/// names the workspace view they act on (design §8).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OpenParams {
    pub address: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `open` result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OpenResult {
    pub kind: String,
    pub heads: Vec<String>,
}

/// `read`: the rendered body, or the value at `pointer`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ReadParams {
    pub address: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub pointer: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `read` result: exactly one of `text`, `bytes_base64`, or `json` is
/// present; `deleted` is true when the file is deleted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadResult {
    pub heads: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bytes_base64: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub json: Option<String>,
    pub deleted: bool,
}

/// One splice against the text at `base`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SpliceParams {
    pub pos: usize,
    pub delete: usize,
    pub insert: String,
}

/// `edit`: splices against the text at `base`, sorted by `pos`, disjoint.
/// In a forked workspace, `base` is a view frontier (the heads a `read` or
/// push of that view returned) and the splices are positions in the view
/// text; the edit lands in the workspace's overrides, never in the base.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct EditParams {
    pub address: String,
    pub base: Vec<String>,
    pub splices: Vec<SpliceParams>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `edit` result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EditResult {
    pub heads: Vec<String>,
}

/// `subscribe`: an exact address or a `**` prefix pattern.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SubscribeParams {
    pub pattern: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `subscribe` result; pushes carry the same id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscribeResult {
    pub sub: u64,
}

/// Server push, JSON-RPC notification method `changed`; `workspace` is
/// the subscription's (omitted for `main`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangedPush {
    pub sub: u64,
    pub address: String,
    pub heads: Vec<String>,
    pub splices: Vec<SpliceParams>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// Server push, JSON-RPC notification method `stale_save`: a mirror save
/// was applied against an older disk base (design §8). Sent to every client
/// connected as the mirror's author; clients show it until dismissed.
/// `workspace` is the mirror's (omitted for `main`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StaleSavePush {
    pub address: String,
    pub chosen_base: Vec<String>,
    pub newest_base: Vec<String>,
    pub heads: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `snapshot`: the visible frontier of `workspace` (`main` when omitted).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SnapshotParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// `snapshot` result: the frontier id and its documents, as a JSON array of
/// `[address, heads]` pairs sorted by address.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotResult {
    pub frontier: String,
    pub docs: Vec<(String, Vec<String>)>,
}

/// `ws.fork`: fork a workspace named `name` from `from` (`main`, or a
/// workspace name or id), optionally only the subtree under the path
/// `subtree`, mirrored to `dir` (by default `.lvcs/ws/<name>/`, or
/// `.lvcs/ws/<name>-<first 6 characters of its id>/` when another workspace
/// in the registry has the name; design §8). The caller authors the new
/// workspace's mirror (decision 137 B): its disk saves are checked against
/// the caller's grants, metered at its signing rate, scanned for secrets,
/// and signed as it, the hook ops attribute through it, and its stale
/// saves reach the caller. A connection from another OS user than the
/// daemon's forks with no mirror, so with no author, and one naming `dir`
/// is refused `REJECTED` (decision 134).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WsForkParams {
    pub name: String,
    pub from: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub subtree: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dir: Option<String>,
}

/// `ws.fork` result: the new workspace's id (`ID_ENCODING`) and its mirror
/// directory, relative to the project root unless `dir` was absolute;
/// empty for a workspace a connection from another OS user than the
/// daemon's forked, which has no mirror (decision 134).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WsForkResult {
    pub workspace: String,
    pub dir: String,
}

/// `ws.merge`: merge `workspace` (a name or id) back into `into` (`main`,
/// or a workspace name or id; the workspace's base when omitted, and it
/// must be the workspace's base, else invalid params, -32602) as one
/// `WorkspaceMerged` bundle (design §8). A workspace forked from another
/// workspace merges into it, which absorbs the bundle copy-on-write into
/// its own overrides and writes no registry record (decision 107); it
/// reaches `main` only through that workspace's own merge-back. With
/// `require_validated` (false when omitted) the merge would wait until
/// checks pass on a preview of the merged state; that is deferred past the
/// prototype demo (decisions 101 and 126), so such a merge is refused
/// `REJECTED` and nothing is merged.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct WsMergeParams {
    pub workspace: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub into: Option<String>,
    #[serde(default)]
    pub require_validated: bool,
}

/// `ws.merge` result: the bundle id and the visible frontier id of `into`
/// after the bundle (both `ID_ENCODING`), and every conflict the merge-back
/// left, each with the thread it opened. A merge-back a base document
/// refused writes no record and opens no thread, and `ws.merge` answers an
/// error instead (decision 88).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WsMergeResult {
    pub bundle: String,
    pub frontier: String,
    pub conflicts: Vec<WsConflict>,
}

/// One merge-back conflict.
///
/// It names the base document, why it conflicts, the conflicting range as code points at the merged heads (`start == end`
/// when that range is empty; both absent for an `anchor_lost` or
/// `whiteout` conflict, whose override or whiteout has no range in the
/// merged text), the participants whose edits overlap (`ID_ENCODING`,
/// sorted; authors the daemon can't map to a participant are omitted), and
/// the conflict thread's address: a thread anchored on `address` and
/// addressed to `authors`. `lines` names the range's lines in the merged
/// text, as the merge plan named them for this conflict alone (decisions
/// 160, 165, and 197); it is absent for an empty range, a document that
/// holds no text after the merge, and an `anchor_lost` or `whiteout`
/// conflict.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WsConflict {
    pub address: String,
    pub cause: WsConflictCause,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub start: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub end: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lines: Option<LineName>,
    pub authors: Vec<String>,
    pub thread: String,
}

/// Why a merge-back conflict opened (decision 92), a JSON string: the
/// workspace's edits and the base's genuinely overlap and the base's text
/// was kept (`overlap`), the base deleted an override's whole range
/// (`anchor_lost`), or the base changed a file the workspace deleted, which
/// was kept (`whiteout`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WsConflictCause {
    Overlap,
    AnchorLost,
    Whiteout,
}

/// What a notification says its sender will do to the range (design §5),
/// a JSON string: `adding`, `changing`, or `removing`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AnnounceOpParam {
    Adding,
    Changing,
    Removing,
}

/// `announce` (design §5, decision 51): publish a notification for the range
/// `start..end` of `address` as `workspace` sees it (`main` when omitted),
/// code points in the text at `base` (the heads the client last read; the
/// document anchors the range there as a cursor pair, `DocEvent::Anchor`),
/// then wait. The result
/// arrives when the wait ends: at once when no other participant is
/// present on the document, otherwise after the wait window
/// (`WAIT_WINDOW_MS`, or `lvcs.toml`'s value up to `WAIT_WINDOW_MAX_MS`),
/// or as soon as a `hold` arrives. A range must hold at least one
/// character, except in an empty document, where the notification covers
/// the whole document. `ref` optionally names more detail (`kad://<key>`
/// or `lvcs://<doc>#<pointer>`). Errors: `UNKNOWN_BASE`, `OUT_OF_BOUNDS`,
/// `RATE_LIMITED` (the gossip bucket), `REJECTED` (no `message`
/// capability).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AnnounceParams {
    pub address: String,
    pub base: Vec<String>,
    pub start: usize,
    pub end: usize,
    pub op: AnnounceOpParam,
    pub what: String,
    #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// How an `announce` wait ended (design §5, decision 51), a JSON string:
/// proceed with the edit (`proceed`), pause it because someone objected
/// (`hold`), or yield to an earlier overlapping notification (`yield`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnnounceOutcome {
    Proceed,
    Hold,
    Yield,
}

/// One `note` or `hold` a notification received: who sent it
/// (`ID_ENCODING`), its text, the thread a hold opened, and its sender's
/// Unix time in milliseconds.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponseItem {
    pub by: String,
    pub text: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread: Option<String>,
    pub ts: i64,
}

/// `announce` result, when the wait ends.
///
/// The notification's id (`ID_ENCODING`), how the wait ended, and the holds
/// and notes received during it. On `yield`,
/// `yielded_to` is the earlier notification's id, and the daemon has
/// turned the edit into an `ask` to its sender in the range's thread, with
/// the notification's `what` as its text (chosen at the M3 freeze), named
/// by `ask`; the client does not edit.
/// On `hold`, `recover` is `wait`, and on `yield` it is `yield`, each with
/// `next` `notifications {}`; on `proceed` both are absent (decision 165).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnnounceResult {
    pub notification: String,
    pub outcome: AnnounceOutcome,
    pub holds: Vec<ResponseItem>,
    pub notes: Vec<ResponseItem>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub yielded_to: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ask: Option<AskResult>,
    /// How to go on: a `recover` value, absent on `proceed`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recover: Option<String>,
    /// The action that goes on, absent on `proceed`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<Next>,
}

/// `note` and `hold` (design §5): respond to the notification
/// `notification` (its id). A `note` is information only; a `hold` is an
/// objection, and the daemon opens a thread on the notification's range
/// addressed to its sender, with `text` as the first message.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RespondParams {
    pub notification: String,
    pub text: String,
}

/// `note` result: an empty object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoteResult {}

/// `hold` result: the address of the thread the hold opened.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HoldResult {
    pub thread: String,
}

/// `ask` (design §9): post an `ask` to the thread `thread`, or open a new
/// thread anchored on `address` (in `workspace`'s view), on `workspace`,
/// or, with neither, on the participants `to`, and post it there. `to`
/// lists the participants it expects an answer from (`ID_ENCODING`); empty
/// means anyone subscribed. A new thread's `to` is fixed when it opens.
/// The ask stays open until answered.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AskParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
    #[serde(default)]
    pub to: Vec<String>,
    pub text: String,
}

/// `ask` result: the thread's address and the ask's message id
/// (`ID_ENCODING`), which an `answer` names.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AskResult {
    pub thread: String,
    pub message: String,
}

/// `answer` (design §9): reply to the ask `ask` (its message id) in the
/// thread `thread`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct AnswerParams {
    pub thread: String,
    pub ask: String,
    pub text: String,
}

/// `answer` result: the answer's message id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnswerResult {
    pub message: String,
}

/// A grant's terms (design §15, decision 146), as the `caps` region's
/// `GrantTerms`, each optional and recorded as given: `harness` and
/// `model`, what the grantee runs, for attribution only, each at most
/// `GRANT_TERM_MAX_BYTES` (decision 178); `quota`, its
/// change quota per document, enforced only when set; and `sign_per_sec`,
/// `sign_burst`, and `gossip_per_sec`, this grant's values for the
/// grantee's buckets, the `[limits]` default when unset, the grantee being
/// metered at the largest over its live grants (decision 178). An issuer
/// may set any value. `alias` names the grantee (decision 243), for
/// display and addressing only: at most `ALIAS_MAX_BYTES` of lowercase
/// ASCII letters, digits, and `-`, starting with a letter; only the project
/// owner gives one, so any other issuer's is refused `REJECTED`
/// `out_of_scope` with an ask to the owner in `next` (decision 247); and
/// it is refused `INVALID_PARAMS`, with a free alias in `next`, when
/// another participant was ever granted it; it stays the grantee's for the
/// life of the project, revoked or not.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GrantTermsParam {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub quota: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sign_per_sec: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sign_burst: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gossip_per_sec: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub alias: Option<String>,
}

/// One capability (design §11), as the `caps` region's `Capability`: a
/// JSON string `write`, `message`, `approve`, or `delegate`, or
/// `{ "attest": <channel> }`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CapabilityParam {
    Write,
    Message,
    Attest(String),
    Approve,
    Delegate,
}

/// `grant` (design §11, §15): the calling participant signs a grant to
/// `grantee` (`ID_ENCODING`) for `capabilities` on `paths` (gitignore
/// syntax), on `terms` (none when omitted), starting now (the request's
/// time, in seconds). The grant's window ends `ttl_secs` after it starts,
/// and it has no expiry when `ttl_secs` is omitted; a `ttl_secs` below
/// `GRANT_TTL_MIN_SECS` is refused `INVALID_PARAMS`. A grant with a window
/// is renewed by its issuer's daemon, unattended, once half its window has
/// passed: a new grant with the same terms and a window of the same length
/// starting then, until it is revoked (`revoke`, decision 148). Refused `REJECTED` only when
/// the caller is neither the owner nor a holder of a live `delegate` grant
/// covering each capability the request names on each of its paths
/// (decisions 109 and 146): no rule reads the terms, and any grant may
/// name any capability on any paths. Refused `INVALID_PARAMS` when
/// `harness` or `model` is longer than `GRANT_TERM_MAX_BYTES`, which every
/// peer refuses in a grant (decision 178), and `REJECTED` when the caps
/// change would pass the change quota of the grant the caller issues it
/// under.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct GrantParams {
    pub grantee: String,
    pub paths: Vec<String>,
    pub capabilities: Vec<CapabilityParam>,
    #[serde(default)]
    pub terms: GrantTermsParam,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ttl_secs: Option<u64>,
}

/// `grant` result: the grant's id (`ID_ENCODING`) and its window in Unix
/// seconds (`not_after_s` omitted when it has no expiry).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantResult {
    pub grant: String,
    pub not_before_s: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub not_after_s: Option<i64>,
}

/// `revoke` (decision 148): revoke the grant `grant` (its id, in
/// `ID_ENCODING`) and every unrevoked grant of its lineage, the grants
/// with the same issuer, grantee, paths, capabilities, terms, and window
/// length (its renewals, since a renewal leaves its predecessor live for
/// half a window), in one signed caps change: each revocation is
/// `/revoked/<grant id>` holding the change's own time `t`, the start of
/// the next whole second, after which no change at or past `t` is within
/// that grant; a daemon signs nothing more under a grant once its
/// revocation renders, and a revoked grant is never renewed. Refused
/// `INVALID_PARAMS` when `grant` names no grant the
/// caps document holds, and `REJECTED`, with nothing committed, unless
/// the caller is the owner, or holds a live `delegate` grant (the one a
/// caps change is signed under) and is the grant's issuer or that grant
/// also covers `delegate` on each of the grant's paths, as every peer's
/// verifier requires, or when the change would pass that grant's change
/// quota (decision 178).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct RevokeParams {
    pub grant: String,
}

/// `revoke` result: the grants it revoked, in `ID_ENCODING`, sorted as
/// strings; empty when the grant and every grant of its lineage were
/// revoked already.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevokeResult {
    pub revoked: Vec<String>,
}

/// `secret.confirm` (design §15, decision 147): commit a held edit.
///
/// Commit the caller's edit the secret scan held (`held`, from the
/// error's `secret`) and record a signed
/// allowlist entry for each flagged string's hash. Refused `REJECTED`
/// unless the caller is the participant whose edit was held and is the
/// owner or holds a live grant, which lets it add allowlist entries
/// (`Write` on `ALLOWLIST_ADDRESS`), whatever its paths and capabilities,
/// and when an allowlist entry, or the edit signed under a grant other
/// than the one it was held under, would pass the change quota of the
/// grant it is signed under (decision 178). A refused confirm leaves the
/// edit held.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SecretConfirmParams {
    pub held: String,
}

/// `secret.confirm` result: as for `edit`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretConfirmResult {
    pub heads: Vec<String>,
}

/// Server push, JSON-RPC notification method `notification`.
///
/// Another participant's range announcement on a subscribed address
/// (design §5), as metadata only (decisions 173 and 176): it names its
/// sender, its attribution, its document, and its range, never what it
/// says; its content (`what`, `ref`, `op`, and its lines' `near` line) is
/// read with `notifications.read`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationPush {
    /// The subscription it is pushed for.
    pub sub: u64,
    /// The announcement, an `announce` notification whose `start` and `end`
    /// are resolved against this daemon's copy at `heads` (both absent
    /// while the anchors haven't synced here).
    pub notification: Notification,
    /// The heads its range is resolved at.
    pub heads: Vec<String>,
    /// The subscription's workspace (omitted for `main`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// Whether a response push is a note or a hold, a JSON string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseKindParam {
    Note,
    Hold,
}

/// Server push, JSON-RPC notification method `response`: a `note` or
/// `hold` on one of the client's participant's notifications (design §5),
/// including those that arrive after its `announce` answered; a hold names
/// its thread. `tier`, `harness`, and `model` are the responder's
/// attribution, as for `notification`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResponsePush {
    pub notification: String,
    pub kind: ResponseKindParam,
    pub response: ResponseItem,
    pub tier: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
}

/// Server push, JSON-RPC notification method `thread_message`: a message
/// in a thread whose anchor the client subscribes to, or that is addressed
/// to its participant (design §9). `kind` is `say`, `ask`, or `answer` (an
/// open set: a client skips a kind it doesn't know); `ask` names the ask
/// an answer replies to; `author` its author, and `tier`, `harness`, and
/// `model` the author's attribution, as for `notification`; `time_ms` the
/// sending request's time.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadMessagePush {
    pub thread: String,
    pub message: String,
    pub author: String,
    pub tier: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    pub kind: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ask: Option<String>,
    pub text: String,
    pub time_ms: i64,
}

/// `track_read` (decision 117), the PostToolUse `Read` hook's op: the
/// tool read `path` (the absolute path it used, in one of this daemon's
/// mirrors), whose bytes had the sha-256 `hash` (lowercase hex). The
/// mirror records the heads those bytes were rendered at, and the file's
/// next save is based on them when the document holds them and they are
/// at or after the author's latest change there. Refused `REJECTED` when
/// `path` is in no mirror of this daemon, or the mirror's author isn't the
/// caller.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TrackReadParams {
    pub path: String,
    pub hash: String,
}

/// `track_read` result: an empty object.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackReadResult {}

/// `track_edit` (decisions 117 and 134), the PreToolUse hook's op: the
/// tool is about to replace `old` (`None` for a whole-file write) in
/// `path` (as for `track_read`). The daemon locates `old` at its first
/// occurrence in the text at the heads the tool read (the whole document
/// when it can't), then notifies that range with `op` and `what` and
/// waits, answering [`TrackEditResult`] when the wait ends, as `announce`
/// does. Refused `REJECTED` when `path` is in no mirror of this daemon,
/// or the mirror's author isn't the caller.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TrackEditParams {
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub old: Option<String>,
    pub op: AnnounceOpParam,
    pub what: String,
}

/// `track_edit` result (decision 134): as [`AnnounceResult`], but for holds.
///
/// Each hold is the [`Notification`] the caller's `open` list holds for it
/// (`kind` `hold`, its sender's tier, harness, and model from the caps
/// render, the notified range, its thread), metadata only; its words are
/// read with `notifications.read` (decision 173).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackEditResult {
    pub notification: String,
    pub outcome: AnnounceOutcome,
    /// The holds received during the wait, as the caller's `open` list
    /// holds them.
    pub holds: Vec<Notification>,
    pub notes: Vec<ResponseItem>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub yielded_to: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ask: Option<AskResult>,
    /// How to go on, as for `announce`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recover: Option<String>,
    /// The action that goes on, as for `announce`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<Next>,
    /// How to name each participant the result names (decision 243), by
    /// id.
    #[serde(default)]
    pub names: BTreeMap<String, Named>,
}

/// `track_write` (decision 117), the `PostToolUse` `Write` and `Edit` hook's op.
///
/// The tool wrote `path` (as for `track_read`); lists the caller's unread
/// notifications as `notifications` does, up to `max_bytes` of their JSON
/// with its `open` notifications counted first (no bound when omitted).
/// Refused `REJECTED` when `path` is in no mirror of this daemon.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct TrackWriteParams {
    /// The absolute path the tool wrote.
    pub path: String,
    /// The most bytes of notification JSON listed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_bytes: Option<u32>,
}

/// `track_write` result. Listing marks nothing read; it is a hinting
/// listing (decision 242), so it moves the caller's hint watermark.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrackWriteResult {
    /// Whether the mirror's author isn't the caller (§5's warning: the
    /// write is attributed to the author).
    pub author_mismatch: bool,
    /// The caller's unread notifications that fit, not open.
    pub notifications: Vec<Notification>,
    /// The caller's open asks and holds, as `notifications` lists them.
    pub open: Vec<Notification>,
    /// How many didn't fit.
    pub omitted: u32,
    /// The ids listed, open ones included, that arrived after the caller's
    /// last hinting listing (decision 242).
    pub fresh: Vec<String>,
    /// How to name each participant the result names (decision 243), by
    /// id.
    #[serde(default)]
    pub names: BTreeMap<String, Named>,
}

/// `stop_gate` (decisions 214 and 217), the `Stop` hook's op: whether the
/// caller may stop now. Read-only, and never retried.
///
/// The daemon keeps the caller's newest change to each file it changed
/// through this daemon, and to each workspace document it changed there,
/// since the daemon started. The answer is `failed` while the caller has
/// an open `check_failed` streak (decision 193) and changed nothing since
/// the notice that opened it, once per set of open streaks: a later
/// `stop_gate` with the same set and nothing changed answers `pending`,
/// naming no file and those streaks, until a streak closes or opens, the
/// caller changes something, or `STOP_WAIT_MAX_MS` passed since the
/// `failed`, which then answers `failed` again (decision 230); else `merge` while one of those changes is in
/// a forked workspace no merge took since (decision 217); else `pending`
/// while a streak of its is open, or, for one of those files, no frontier
/// of `ci`'s value holds its newest change, unless the project
/// configures no check; and `passed` otherwise. A restart forgets the
/// changes, as it forgets `status.checks`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct StopGateParams {}

/// What the `Stop` gate decided (decisions 214 and 217).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopGate {
    /// The caller may stop: `ci` holds its newest changes, and
    /// no check of its fails.
    Passed,
    /// The runners haven't passed its newest changes yet, its fix of a
    /// failed check included: ask again after `retry_after_ms`.
    Pending,
    /// Its newest change is in a workspace it hasn't merged: merge it with
    /// `ws.merge`.
    Merge,
    /// A check failed on its changes: read the `check_failed` with
    /// `notifications.read` and fix the change.
    Failed,
}

/// `stop_gate` result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StopGateResult {
    /// The decision.
    pub gate: StopGate,
    /// `failed`: the caller's open failing streaks, as `status.checks`
    /// lists them (metadata only); `pending` with no file: those streaks,
    /// already shown at a `Stop` (decision 230); empty otherwise.
    pub failed: Vec<Notification>,
    /// `merge`: the workspaces holding its unmerged changes, each by its
    /// name when this daemon runs it, else its id in `ID_ENCODING`; empty
    /// otherwise.
    pub workspaces: Vec<String>,
    /// `pending`: the files whose newest change of the caller's no frontier
    /// of `ci` holds yet, sorted; empty otherwise.
    pub pending: Vec<String>,
    /// `pending`: how long to wait before asking again (`STOP_RETRY_MS`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub retry_after_ms: Option<u64>,
}

/// `notifications` (decisions 118 and 176): list the caller's notifications.
///
/// Metadata only. Decision 118's bounds on what the daemon keeps bound the
/// list.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct NotificationsParams {
    /// Only unread notifications sent at or after this Unix time in
    /// milliseconds (every one when omitted).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub since: Option<i64>,
    /// Whether this listing is a hook's hint (decision 242): after it, the
    /// notifications held count as shown, and a later listing's `fresh`
    /// leaves them out. Nothing is marked read.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub hint: bool,
}

/// How hints and `lvcs status` name a participant (decision 243): its
/// alias when one is bound to it, else the shortest prefix of its id, at
/// least `SHORT_ID_MIN` characters, that no other participant the daemon
/// knows starts with and that is no alias. `revoked` when it holds a
/// revoked grant and no live one. Every `lvcs` command takes `name` in
/// place of the id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Named {
    pub name: String,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub revoked: bool,
}

/// `notifications` result (decision 176). Listing marks nothing read.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationsResult {
    /// The caller's other unread notifications: stale saves first, then
    /// asks, holds, notes, says, and answers, then range announcements,
    /// each group newest arrival first.
    pub notifications: Vec<Notification>,
    /// The caller's open asks (asks naming it that it hasn't answered) and
    /// active holds, which reading never clears, all of them whatever
    /// `since` says.
    pub open: Vec<Notification>,
    /// The ids listed, open ones included, that arrived after the caller's
    /// last hinting listing (decision 242).
    #[serde(default)]
    pub fresh: Vec<String>,
    /// How to name each participant the result names (decision 243), by
    /// id.
    #[serde(default)]
    pub names: BTreeMap<String, Named>,
}

/// `notifications.read` (decisions 173 and 176): read notifications' content.
///
/// The caller's notifications named are marked read. An open ask or hold
/// stays open, and readable, until it closes, and the `check_failed` of an
/// open streak stays readable until the streak closes (decision 230).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct NotificationsReadParams {
    /// The notifications' ids, in `ID_ENCODING`.
    pub ids: Vec<String>,
}

/// `notifications.read` result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationsReadResult {
    /// The content of each named notification the caller holds, in the
    /// order named.
    pub contents: Vec<NotificationContent>,
    /// The ids it holds none for: never sent to it, read already, or
    /// dropped past decision 118's bounds.
    pub missing: Vec<String>,
}

/// `status` (decision 164): where the caller stands. Read-only, and never
/// retried.
///
/// It shows the caller only what is its own (its grants, notifications,
/// mirrors, stale saves, and errors) and a peer count. `workspace` (`main`,
/// or a forked workspace's name or id) limits `mirrors` and `stale_saves`
/// to that workspace's, and is refused as `read` refuses a workspace this
/// daemon doesn't run. `path` asks how this daemon classifies one path
/// now (row 180), answered as `path`; it is refused when it is no valid
/// address.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct StatusParams {
    /// The one workspace whose mirror and stale saves to show (every one
    /// when omitted).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
    /// The one path to classify, as `lvcs explain <path>` asks (row 180).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
}

/// `status` result (decision 164).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusResult {
    /// The caller, in `ID_ENCODING`.
    pub participant: String,
    /// The caller's `trust_tier` value now, as other participants' daemons
    /// compute it from its grants: `owner`, or `participant` (also when it
    /// holds no live grant).
    pub tier: String,
    /// The harness its grant names, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    /// The model its grant names, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    /// The mirrored directories this daemon runs that the caller authors
    /// (decision 137 B): `main`'s for the daemon's default participant, a
    /// forked workspace's for the participant that forked it.
    pub mirrors: Vec<StatusMirror>,
    /// The caller's live grants, newest first: those whose window holds
    /// now and whose revocation the caps document doesn't render, which
    /// the daemon signs under.
    pub grants: Vec<StatusGrant>,
    /// The caller's grants that ended in the last `RETRY_WINDOW_SECS`,
    /// latest end first: by their window, or once their revocation renders
    /// (decision 148).
    pub ended: Vec<StatusEnded>,
    /// The caller's open asks and holds, as `notifications` lists them.
    pub open: Vec<Notification>,
    /// How many of the caller's other notifications wait unread, with no
    /// filter applied.
    pub pending: u32,
    /// The stale saves of the mirrors the caller authors since each file's
    /// last clean save, oldest first.
    pub stale_saves: Vec<StatusStale>,
    /// How many peers this daemon syncs a document with now.
    pub peers: u32,
    /// The last error the daemon answered the caller, on any connection,
    /// since the daemon started; a restart forgets it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<LastError>,
    /// The caller's open failing streaks (decision 193): the latest
    /// `check_failed` it received for each check no `check_passed` has
    /// ended since, newest first; a restart forgets them.
    pub checks: Vec<Notification>,
    /// On a `ci` runner's daemon, the actions failing here now (decision
    /// 193), each with the authors its runner told; empty when this daemon
    /// doesn't serve `ci`.
    pub red: Vec<StatusRed>,
    /// The channels this daemon's runner serves, comma-separated, with why
    /// it runs nothing when the sandbox can't run here (decision 245);
    /// `None` when its participant's grants name no channel it can run.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runner: Option<String>,
    /// One line per GitHub workflow file at main's approved heads
    /// (decision 245, stage C): `actions <file>: <n> mapped, <n> skipped,
    /// <n> ignored`, or `actions <file>: refused <reason> <name>: <next>`
    /// for each job it refused, which holds `ci`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<String>,
    /// At most `DERIVED_STATUS_MAX` of the derived files the caller's
    /// mirrors hold that are stale or failed, or that were superseded for
    /// the caller in the last `RETRY_WINDOW_SECS`, stale and failed first,
    /// then by address (row 180).
    pub derived: Vec<DerivedView>,
    /// How many more such files `derived` leaves out.
    pub derived_omitted: u32,
    /// The path a `status { path }` asked about, as this daemon classifies
    /// it now (row 180); absent when none was asked.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<DerivedView>,
    /// How to name each participant the result names, the caller
    /// included (decision 243), by id.
    #[serde(default)]
    pub names: BTreeMap<String, Named>,
    /// The aliases a grant gave the caller that were first granted to
    /// another participant, so they name that one, not the caller
    /// (decision 243).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub alias_conflicts: Vec<String>,
}

/// One path as row 179's classifier sees it, with where its derived output
/// stands (row 180), as `status` and `lvcs explain <path>` show it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DerivedView {
    /// The path.
    pub address: String,
    /// Whether it is derived.
    pub derived: bool,
    /// The `derived_rule` value of the rule that decided: `none` when no
    /// rule applies, and for a path that is never derived.
    pub rule: String,
    /// What matched, cut to `RANGE_CONTEXT_MAX_CHARS` code points: the
    /// `.gitattributes` line as `<file>:<line>: <text>`, the `lvcs.toml`
    /// pattern, the vocabulary pattern, or the marker; empty for `none`
    /// with no pattern.
    pub detail: String,
    /// the generator's command at main's approved heads, for a derived
    /// path of a project that has one: its argv when it is one `Exec`
    /// step, else one word describing its steps (decision 248).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub generator: Option<Vec<String>>,
    /// Where its output stands under the generator; absent without one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<DerivedState>,
    /// For a `stale` path, how far its shown output is behind its inputs.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub behind: Option<Behind>,
    /// For a `failed` path, why its derivation failed and how to recover.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<ErrorData>,
}

/// Where a derived path's output stands under the generator (row 180).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DerivedState {
    /// A counted generation covers its inputs' newest change.
    Current,
    /// Its inputs changed after the shown generation, or its new output
    /// waits for approval.
    Stale,
    /// Its derivation for the newest inputs failed or diverged.
    Failed,
}

/// How far a stale derived path's shown output is behind its inputs (row
/// 180).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Behind {
    /// The input documents whose heads moved past the shown generation's
    /// frontier.
    pub inputs: u32,
    /// When the first of them moved, in Unix milliseconds by the daemon's
    /// clock.
    pub since_ms: i64,
}

/// A check failing on this `ci` runner's daemon (decision 193).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusRed {
    /// The check's id, in `ID_ENCODING`.
    pub check: String,
    /// Why it is red: a `red_kind` value (decision 230). `timed_out` with
    /// no one told is a first timeout, which reruns at the next quiet;
    /// with authors told, a second consecutive timeout at the same inputs
    /// opened its streak.
    #[serde(serialize_with = "vocab_to", deserialize_with = "vocab_from")]
    pub kind: RedKind,
    /// The id of the frontier where it last passed on `ci`;
    /// absent when it never did.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub passed_at: Option<String>,
    /// The id of the frontier where its failing streak began, or where it
    /// timed out.
    pub failed_at: String,
    /// The participants told with a `check_failed`: the authors of changes
    /// in the window to files the check reads. Empty when no change there
    /// touches one (a toolchain or environment change, or a flaky check):
    /// the check is red with no cause.
    pub told: Vec<String>,
}

/// One mirrored directory the caller authors (decision 137 B).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusMirror {
    /// `main`, or the forked workspace's name.
    pub workspace: String,
    /// The forked workspace's id, in `ID_ENCODING`; absent for `main`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    /// The directory, absolute.
    pub dir: String,
    /// What the forked workspace was forked from: `main`, or a workspace
    /// id in `ID_ENCODING`; absent for `main`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub base: Option<String>,
}

/// One live grant of the caller's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusGrant {
    /// The grant's id, in `ID_ENCODING`.
    pub id: String,
    /// Its issuer, in `ID_ENCODING`.
    pub issuer: String,
    /// The paths it covers (gitignore syntax).
    pub paths: Vec<String>,
    /// The capabilities it gives on them.
    pub capabilities: Vec<CapabilityParam>,
    /// The end of its window, in Unix seconds; absent when it has none.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub not_after_s: Option<i64>,
    /// Whether this daemon, holding its issuer's key, renews it before its
    /// window ends (decision 148): never for a grant with no window, nor
    /// for a lineage with a revoked grant or one a revocation this daemon
    /// sent names (decision 185).
    pub renews: bool,
}

/// One grant of the caller's that ended lately.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusEnded {
    /// The grant's id, in `ID_ENCODING`.
    pub id: String,
    /// When it ended, in Unix seconds: its window's end, or the time its
    /// revocation names when that comes first.
    pub ended_s: i64,
    /// Whether it is revoked.
    pub revoked: bool,
}

/// One stale save of a mirror the caller authors (design §8, decision 66).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusStale {
    /// The file.
    pub address: String,
    /// The forked workspace whose mirror saved it, in `ID_ENCODING`;
    /// absent for `main`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

/// The last error the daemon answered a participant (decision 164).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LastError {
    /// The op of the request it answered, when the request named one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op: Option<String>,
    /// Its JSON-RPC error code.
    pub code: i64,
    /// Its `data`, as the answer carried it.
    pub data: ErrorData,
    /// When it was answered, in Unix milliseconds.
    pub ts: i64,
}

/// One notification (decisions 118, 173, and 176), metadata only.
///
/// It says who sent it, what kind it is, where, when, and how long its
/// text is, never what it says. `tier`, `harness`, and `model` are computed
/// by this daemon from the grant `authorize` found for the sender in the
/// caps render, never copied from a message (decision 147). `lines` names
/// its range's lines as this daemon's document named them where it
/// resolved the range, never with a `near` line, which is content
/// (decisions 165 and 184); `auto` says the sender's daemon announced a
/// save through a mirror on its behalf (decision 165).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Notification {
    /// The `HASH` of the signed object it carries, in `ID_ENCODING`, so a
    /// copy that arrives twice is held once; for a `check_failed` or
    /// `check_passed`, the `HASH` of its kind, check, frontiers, and
    /// recipient, so every runner's copy is held once (decision 193).
    pub id: String,
    /// A `notification_kind` value.
    #[serde(serialize_with = "vocab_to", deserialize_with = "vocab_from")]
    pub kind: NotificationKind,
    /// Its sender, in `ID_ENCODING`.
    pub from: String,
    /// Its sender's `trust_tier` value, `owner` or `participant`.
    pub tier: String,
    /// The harness its sender's grant names, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    /// The model its sender's grant names, if any.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    /// The document it is about, when it has one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub address: Option<String>,
    /// Its range's first code point, when it has a range.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub start: Option<usize>,
    /// The code point past its range.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub end: Option<usize>,
    /// Its range's lines, without `near`; absent while the range isn't
    /// resolved here, and for a whole document.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lines: Option<LineName>,
    /// Its thread's address, when it is in one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread: Option<String>,
    /// Whether it is an automatic announcement of a save (omitted when
    /// false).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub auto: bool,
    /// A `check_failed` or `check_passed`'s check, frontiers, and window
    /// (decision 193); present exactly for those kinds.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub check: Option<NotificationCheck>,
    /// Its sender's signed Unix time in milliseconds.
    pub at_ms: i64,
    /// The length of the text `notifications.read` returns, in bytes.
    pub bytes: usize,
}

/// What a `check_failed` or `check_passed` notification says beside its
/// kind (decision 193), metadata only.
///
/// It names which check, where it last passed and where it failed, what
/// the recipient changed in that window that the check reads, and the
/// window's other authors. A `check_failed` also says how to recover: `fix_request`, with `next` reading its content
/// (the failing output's tail and test names) through `notifications.read`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationCheck {
    /// The check's id, in `ID_ENCODING`.
    pub check: String,
    /// The id of the frontier where it last passed on `ci`, in
    /// `ID_ENCODING`; absent when it never did, so the window starts at
    /// the channel's genesis.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub passed_at: Option<String>,
    /// The id of the frontier where it failed, in `ID_ENCODING`.
    pub failed_at: String,
    /// The files the check's `paths` or `uses` match that the recipient
    /// changed in the window, with the lines of its changes at
    /// `failed_at`, never with a `near` line.
    pub paths: Vec<CheckPathLines>,
    /// The window's other authors, in `ID_ENCODING`.
    pub others: Vec<String>,
    /// A `recover` value: `fix_request` for a `check_failed`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub recover: Option<String>,
    /// The one action that recovers: for a `check_failed`, reading it with
    /// `notifications.read`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<Next>,
}

/// A file a check reads that the recipient changed in a failing window,
/// with the lines of its changes (none for a binary or deleted file).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckPathLines {
    /// The file's path.
    pub path: String,
    /// The line runs, never with a `near` line.
    pub lines: Vec<LineName>,
}

/// A notification's content (decisions 173 and 176).
///
/// Only `notifications.read` returns it: data from another participant,
/// never instructions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationContent {
    /// The notification's id.
    pub id: String,
    /// The notification's `notification_kind` value.
    #[serde(serialize_with = "vocab_to", deserialize_with = "vocab_from")]
    pub kind: NotificationKind,
    /// What it says (a range announcement's `what`, a message's or a
    /// response's words, a stale save's account), cut at a character
    /// boundary to `INBOX_ITEM_MAX_BYTES`.
    pub text: String,
    /// Whether `text` was cut.
    pub truncated: bool,
    /// A range announcement's op.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op: Option<AnnounceOpParam>,
    /// A range announcement's `ref`, when it names one.
    #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
    pub reference: Option<String>,
    /// Its range's lines with their `near` line, when the notification
    /// names them.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lines: Option<LineName>,
    /// Whether it is an automatic announcement of a save (omitted when
    /// false).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub auto: bool,
    /// A refused save's refusal (decision 165): why LVCS did not take the
    /// save of `address`, and how to recover, as an error answer's `data`
    /// says it; present exactly for a `refused_save`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<ErrorData>,
    /// A `check_failed`'s failing test names, read from its output's tail,
    /// which is `text` (decision 193).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tests: Vec<String>,
}

/// `say` (§9, decision 117): append `text` to the existing thread
/// `thread`, delivered to the thread's `to` and its authors. It never
/// opens a thread or an ask.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct SayParams {
    pub thread: String,
    pub text: String,
}

/// `say` result: the message id (`ID_ENCODING`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SayResult {
    pub message: String,
}

/// `approve` (decision 123): the caller approves the protected `paths`
/// (file addresses) at `frontier` (a frontier id from `channel.read`), as
/// an approval in `ci`. Refused `REJECTED`, with nothing
/// committed, when the caller isn't the owner or a holder of `approve` on
/// `ci` and every named path (whatever its tier or its
/// connection's uid, decision 147), `ci` holds no attestation
/// of `frontier`, a named path isn't protected, the frontier changes a
/// protected path that `paths` doesn't name, or the entry would pass the
/// change quota of the grant it is signed under (decision 178).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ApproveParams {
    pub frontier: String,
    pub paths: Vec<String>,
}

/// `approve` result: the approval's entry id (`ID_ENCODING`) and, for each
/// named path, the participants who signed its changes beyond its approved
/// heads in the frontier.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApproveResult {
    pub approval: String,
    pub paths: Vec<ApprovedPath>,
}

/// One approved path and its signers beyond its approved heads, each in
/// `ID_ENCODING`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovedPath {
    pub path: String,
    pub signers: Vec<String>,
}

/// `checkpoint` (decision 124): write one git commit of main's visible
/// frontier and advance `refs/lvcs/checkpoints/main`. Refused `REJECTED`
/// when the project root is not a git repository.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CheckpointParams {}

/// `checkpoint` result: the commit's git object id (hex), the frontier id
/// it holds (`ID_ENCODING`), and the ref it advanced.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckpointResult {
    pub commit: String,
    pub frontier: String,
    pub reference: String,
}

/// `channel.read` (decision 120): the value of channel `channel` (a
/// channel name: `ci` or a promote action's channel).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ChannelReadParams {
    pub channel: String,
}

/// `channel.read` result: the channel's value, its counted frontiers that
/// no other counted frontier supersedes, sorted by id (none for a
/// promote channel).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelReadResult {
    pub frontiers: Vec<ChannelFrontier>,
}

/// One frontier of a channel's value: its id (`ID_ENCODING`), the size of
/// its largest counted set of attestations, and the approvals naming it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelFrontier {
    pub frontier: String,
    pub attestations: u32,
    pub approvals: u32,
}

/// `promote` (decisions 141 and 204): move a promote channel to a
/// frontier.
///
/// CLI: `lvcs promote <channel> <frontier-id>`. The caller signs the
/// promotion under a grant holding `Approve` on the channel's address. A
/// rollback is a promote of an earlier frontier the channel held. Retried
/// under `request` (decision 167).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct PromoteParams {
    /// The channel's name, `<env>`.
    pub channel: String,
    /// A frontier id (`ID_ENCODING`), as [`ChannelFrontier`] names one.
    pub frontier: String,
}

/// `promote` result: the promotion's entry id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromoteResult {
    /// The promotion's entry id (`ID_ENCODING`).
    pub promotion: String,
}

/// The JSON Schema of `op`'s params (decision 116).
///
/// What `lvcs mcp` lists as the `inputSchema` of `op`'s tool. Exhaustive
/// over `Op`, so a new op value doesn't compile without its params. A
/// [`retried`] op's params also take the optional [`REQUEST_PARAM`] string
/// (decision 163).
pub fn params_schema(op: Op) -> Schema {
    let mut schema = op_schema(op);
    if retried(op) {
        let field = schema
            .ensure_object()
            .entry("properties")
            .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
        if let Some(properties) = field.as_object_mut() {
            properties.insert(
                REQUEST_PARAM.to_owned(),
                serde_json::json!({
                    "type": "string",
                    "maxLength": REQUEST_ID_MAX_BYTES,
                    "description": REQUEST_DESCRIPTION,
                }),
            );
        }
    }
    schema
}

/// The params key of a retried request's id (decision 163): the shell
/// takes it out of the params before the op parses them.
pub const REQUEST_PARAM: &str = "request";

/// What a retried op's `request` param is for (decision 163). At most
/// `REQUEST_ID_MAX_BYTES` bytes.
pub const REQUEST_DESCRIPTION: &str = "a fresh id for this call; send the same id again only to retry a call whose answer you did not get";

/// Whether `op` is retried safely under a `request` id (decision 163).
///
/// True for every op that signs, publishes, appends, or delivers, so
/// sending one again with the same `request` gets the first answer and
/// applies once. `notifications` lists and consumes nothing, so it is not
/// (decision 187). Exhaustive over `Op`.
pub const fn retried(op: Op) -> bool {
    match op {
        Op::Edit
        | Op::Announce
        | Op::Note
        | Op::Hold
        | Op::Ask
        | Op::Answer
        | Op::Say
        | Op::WsFork
        | Op::WsMerge
        | Op::Grant
        | Op::Revoke
        | Op::Approve
        | Op::Checkpoint
        | Op::SecretConfirm
        | Op::TrackEdit
        | Op::TrackWrite
        | Op::NotificationsRead
        | Op::Promote => true,
        Op::Hello
        | Op::Subscribe
        | Op::Open
        | Op::Read
        | Op::Snapshot
        | Op::ChannelRead
        | Op::TrackRead
        | Op::Notifications
        | Op::StopGate
        | Op::Status => false,
    }
}

/// The JSON Schema of `op`'s own params type.
fn op_schema(op: Op) -> Schema {
    match op {
        Op::Hello => schema_for!(HelloParams),
        Op::Open => schema_for!(OpenParams),
        Op::Read => schema_for!(ReadParams),
        Op::Edit => schema_for!(EditParams),
        Op::Subscribe => schema_for!(SubscribeParams),
        Op::Announce => schema_for!(AnnounceParams),
        Op::Ask => schema_for!(AskParams),
        Op::Answer => schema_for!(AnswerParams),
        Op::Snapshot => schema_for!(SnapshotParams),
        Op::WsFork => schema_for!(WsForkParams),
        Op::WsMerge => schema_for!(WsMergeParams),
        Op::Note | Op::Hold => schema_for!(RespondParams),
        Op::Grant => schema_for!(GrantParams),
        Op::Revoke => schema_for!(RevokeParams),
        Op::SecretConfirm => schema_for!(SecretConfirmParams),
        Op::TrackRead => schema_for!(TrackReadParams),
        Op::TrackEdit => schema_for!(TrackEditParams),
        Op::TrackWrite => schema_for!(TrackWriteParams),
        Op::StopGate => schema_for!(StopGateParams),
        Op::Notifications => schema_for!(NotificationsParams),
        Op::NotificationsRead => schema_for!(NotificationsReadParams),
        Op::Status => schema_for!(StatusParams),
        Op::Say => schema_for!(SayParams),
        Op::Approve => schema_for!(ApproveParams),
        Op::Checkpoint => schema_for!(CheckpointParams),
        Op::ChannelRead => schema_for!(ChannelReadParams),
        Op::Promote => schema_for!(PromoteParams),
    }
}

/// Whether `lvcs mcp` lists a tool for `op` (decisions 116 and 147):
/// every op but the connection mechanics, `hello` (sent once by `lvcs mcp`
/// itself) and `subscribe` (pushes have no MCP form). Authority comes
/// from capabilities alone, so `grant`, `revoke`, `secret.confirm`, and
/// `approve` are tools like any other op (decisions 147 and 148).
/// Exhaustive over `Op`.
pub const fn mcp_tool(op: Op) -> bool {
    match op {
        Op::Hello | Op::Subscribe => false,
        Op::Open
        | Op::Read
        | Op::Edit
        | Op::Announce
        | Op::Ask
        | Op::Answer
        | Op::Snapshot
        | Op::WsFork
        | Op::WsMerge
        | Op::Note
        | Op::Hold
        | Op::Grant
        | Op::Revoke
        | Op::SecretConfirm
        | Op::TrackRead
        | Op::TrackEdit
        | Op::TrackWrite
        | Op::StopGate
        | Op::Notifications
        | Op::NotificationsRead
        | Op::Status
        | Op::Say
        | Op::Approve
        | Op::Checkpoint
        | Op::ChannelRead
        | Op::Promote => true,
    }
}

/// `data` of every JSON-RPC error the daemon or `lvcs mcp` answers.
///
/// Decision 163: what went wrong, said for a reader who has never seen
/// LVCS, and how to recover from it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorData {
    /// What went wrong, in plain words.
    pub reason: String,
    /// How to recover: a `recover` value among the error code's.
    pub recover: String,
    /// The one action that recovers, present exactly when `recover` isn't
    /// `none`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next: Option<Next>,
    /// The `rule` value that refused the request, present exactly when the
    /// code is `rejected`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rule: Option<String>,
    /// How long to wait before sending the same request again, present
    /// exactly when `recover` is `retry_after`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub retry_after_ms: Option<u64>,
    /// What the secret scan flagged, present exactly when the code is
    /// `secret_detected`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub secret: Option<SecretHit>,
}

/// The one action that recovers from an error (decision 163).
///
/// `op` with `params` is a whole request to send as given; `op` alone is
/// the same request sent again, with the same params; `cli` is a command
/// line, starting `lvcs `, to run in the project root; `candidates`
/// alone (decision 243) are the ids a prefix or alias named more than one
/// of, at most `CANDIDATES_MAX`: send the request again naming one.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Next {
    /// An `op` value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op: Option<String>,
    /// The params to send `op` with.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
    /// A command line to run instead.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cli: Option<String>,
    /// The ids an ambiguous prefix named, one of which to name instead.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub candidates: Vec<String>,
}

/// What the secret scan flagged (design §15, decisions 147 and 163).
///
/// `start..end` are code points of the request's text at its base. An edit
/// is held uncommitted, whoever its author is, and `held` is what
/// `secret.confirm` takes; a message's text or a mirror save has no edit
/// to hold, and `held` is absent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SecretHit {
    /// The document.
    pub address: String,
    /// The flagged text's first code point.
    pub start: usize,
    /// The code point past it.
    pub end: usize,
    /// The scan pattern that flagged it: a known token format's name, or
    /// `entropy`.
    pub pattern: String,
    /// The held edit's id, for `secret.confirm`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub held: Option<String>,
}

config

Source: crates/lvcs/src/config.rs.

/// The `lvcs.toml` sections the daemon reads (design §5, §10, §15). Every
/// section is a table whose missing keys keep their defaults, so an absent
/// file or section is the protocol defaults. Each section's type belongs
/// to the lane that reads it: `[coord]` to `M3.coord`
/// (`lvcs_net::coord::CoordConfig`), `[limits]` to `M3.grants`
/// (`lvcs_net::grants::Limits`), `[secrets]` to `M3.secrets`
/// (`SecretsConfig`), `[telemetry]` to the daemon, `[[actions]]` and
/// `[runner]` to `M5.validator` (`lvcs_action::config::ActionEntry` and
/// `RunnerTable`, decisions 161 and 245, whose canonical form,
/// `lvcs_action::config::Action`, `map` gives; `[workflows]`,
/// `lvcs_action::workflow::WorkflowsTable`, for the GitHub workflows
/// mapped beside them, stage C), and `[git]` to `M5.git`
/// (`crate::git::GitConfig`, decision 124). Other tables (`[deps]`,
/// `[[navigators]]`) are ignored until their lanes add them. `lvcs.toml`
/// is a protected path (`DEFAULT_PROTECTED`), so a change to it is not
/// materialized until approved (§15, decision 122), and no participant
/// changes the `[limits]` every signer is metered by alone (decision 146).
/// Owner: the daemon lanes.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub(crate) struct LvcsToml {
    pub(crate) coord: CoordConfig,
    pub(crate) limits: Limits,
    pub(crate) secrets: SecretsConfig,
    pub(crate) telemetry: TelemetryConfig,
    /// `[[actions]]` as written, read only at approved heads like every
    /// section (decisions 127 and 245); a list `map` refuses refuses the
    /// file.
    #[serde(rename = "actions")]
    pub(crate) entries: Vec<ActionEntry>,
    /// The canonical actions `map` gives of `entries`.
    #[serde(skip)]
    pub(crate) actions: Actions,
    /// `[runner]`: a `ci` runner's quiet period, applied at its next start
    /// (decision 127).
    pub(crate) runner: RunnerTable,
    pub(crate) git: GitConfig,
    /// `[workflows]`: how the GitHub workflows map (decision 245).
    pub(crate) workflows: WorkflowsTable,
    /// What each workflow file came to, once [`LvcsToml::add_workflows`]
    /// mapped them.
    #[serde(skip)]
    pub(crate) sources: Vec<FileMapped>,
}

/// `lvcs.toml` `[telemetry]` (design §6): `subtrees`, the directory
/// prefixes the `subtree` attribute may name (at most `SUBTREE_CAP`), and
/// `ws_names`, the workspace names the `ws_name` attribute may name (at
/// most `WS_NAME_CAP`); entries past a cap are ignored, so both attributes
/// stay bounded whatever the file says. Owner: `M3.daemon`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub(crate) struct TelemetryConfig {
    pub(crate) subtrees: Vec<String>,
    pub(crate) ws_names: Vec<String>,
}

/// Why `lvcs.toml` could not be loaded: it could not be read, or it is not
/// TOML of these sections' shapes.
#[derive(Debug)]
pub(crate) enum ConfigError {
    Read(io::Error),
    Parse(String),
}

derive-classify

Source: crates/lvcs/src/derive.rs.

/// How row 179's classifier classified one path (decisions 172, 179, and
/// 180).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Classification {
    /// Whether the file is derived.
    pub(crate) derived: bool,
    /// The rule that decided: `none` when no rule applies, and for a
    /// `NEVER_DERIVED` path.
    pub(crate) rule: DerivedRule,
    /// Whether the rule was an explicit negation (a `.gitattributes` unset
    /// or a the generator's `writes` `!` pattern), the one case a save carries
    /// `Unmark` (row 180).
    pub(crate) negated: bool,
    /// What matched, cut to `RANGE_CONTEXT_MAX_CHARS` code points: the
    /// `.gitattributes` line as `<file>:<line>: <text>`, the `lvcs.toml`
    /// pattern, the `DERIVED_PATTERNS` or `NEVER_DERIVED` pattern, or the
    /// marker; empty when no rule applies.
    pub(crate) detail: String,
}

git

Source: crates/lvcs/src/git/mod.rs.

/// The trailer every checkpoint commit carries, naming the frontier it
/// holds in `ID_ENCODING`: `LVCS-Frontier: <frontier id>`.
pub(crate) const FRONTIER_TRAILER: &str = "LVCS-Frontier";

/// The prefix of the refs checkpoints advance: `refs/lvcs/checkpoints/main`
/// for `main`, and `refs/lvcs/checkpoints/<workspace id>` for a workspace.
/// `lvcs init` creates `refs/lvcs/checkpoints/main` at `HEAD`, a git ref and
/// never an LVCS document.
pub(crate) const CHECKPOINT_REF_PREFIX: &str = "refs/lvcs/checkpoints/";

/// `lvcs.toml` `[git]`, read only from main's `lvcs.toml` at its approved
/// heads (decision 127): `branches` maps a channel name, which must be
/// as `ci`, to the git ref it moves, a branch under
/// `refs/heads/`. Empty by default, so no branch moves.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub(crate) struct GitConfig {
    pub(crate) branches: BTreeMap<String, String>,
}

/// A commit a ref names: its SHA-1 object id.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GitHead {
    pub(crate) commit: [u8; 20],
}

/// Why git refused what LVCS asked.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GitRefusal {
    /// The project root is not a git repository.
    NotARepository,
    /// Tracked files differ from `HEAD`, or an untracked file isn't
    /// ignored; the text says which.
    Dirty(String),
    /// `git` could not run, or answered with an error; the text says
    /// which. Also a repository whose work tree is not the project root or
    /// whose object format is not SHA-1, a `git` older than 2.31 (it can't
    /// be kept from running the repository's hooks), a malformed ref name,
    /// a branch outside `refs/heads/`, and a ref that moved concurrently
    /// (every ref move is a compare-and-swap).
    Failed(String),
}

/// One file of a checkpoint's tree: its bytes and executable bit.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct TreeFile {
    pub(crate) bytes: Vec<u8>,
    pub(crate) executable: bool,
}

/// Whether the git repository at `root` is clean, before `lvcs init`
/// imports anything: `Ok(None)` when `root` holds no `.git` (then no `git`
/// runs), `git` finds no repository there, or the repository has no commit yet, `Ok(Some(HEAD))` when no
/// tracked file differs from `HEAD` and every untracked file is ignored,
/// and `Dirty` otherwise, so `lvcs init` exits 1 with nothing written. It
/// takes no lock, so it writes nothing to the repository either.
pub(crate) fn check_clean(root: &Path) -> Result<Option<GitHead>, GitRefusal> {
    let span = tracing::info_span!("git_clean", outcome = tracing::field::Empty);
    traced(&span, || setup::check_clean(root))
}

/// Add `.lvcs/` to the repository's `.git/info/exclude` (a linked
/// worktree's common one), once, so git never tracks LVCS's own state.
/// Does nothing when `root` holds no `.git` or `git` finds no repository
/// there, or when a line of the file
/// already excludes `.lvcs`.
pub(crate) fn exclude_lvcs(root: &Path) -> Result<(), GitRefusal> {
    let span = tracing::info_span!("git_exclude", outcome = tracing::field::Empty);
    traced(&span, || setup::exclude_lvcs(root))
}

/// Record `head`, the `HEAD` [`check_clean`] answered, as `lvcs init`'s
/// starting state (design §2): set `refs/lvcs/checkpoints/main` to it,
/// keeping any earlier value (from an LVCS state removed before this init)
/// in that ref's reflog. Does nothing when `root` holds no `.git` or `git`
/// finds no repository there.
pub(crate) fn record_head(root: &Path, head: &GitHead) -> Result<(), GitRefusal> {
    let span = tracing::info_span!("git_record_head", outcome = tracing::field::Empty);
    traced(&span, || setup::record_head(root, head))
}

/// Write one checkpoint commit of `tree`, with the `FRONTIER_TRAILER`
/// naming `frontier`, and advance
/// `CHECKPOINT_REF_PREFIX` + `name` to it. Its parent is that ref, else
/// `HEAD`, else none. `tree` holds every path the caller read (decision
/// 136): the frontier's `file` documents, and every protected path at its
/// approved heads, each `Some` with its bytes and executable bit, or
/// `None` when it is absent at the heads read (a deleted file, or a
/// protected path deleted at its approved heads), which the commit leaves
/// out. A protected path with no approved heads is not in `tree`. The
/// commit's tree also keeps the parent tree's gitlinks, and its entries at
/// paths `tree` lacks that are protected or that LVCS never syncs: symbolic links, never-sync paths (by the root's
/// reserved and disk matchers), names that are not UTF-8 or break the
/// address rules, and case and normalization collisions with a path of
/// `tree`. A path of `tree` at, beneath, or above a kept gitlink or
/// protected entry is left out, and one colliding with another kept entry
/// replaces it. `NotARepository` when `root` holds no `.git`, or `git`
/// finds no repository there.
pub(crate) fn checkpoint(
    root: &Path,
    name: &str,
    frontier: &FrontierId,
    tree: &BTreeMap<String, Option<TreeFile>>,
) -> Result<GitHead, GitRefusal> {
    let span = tracing::info_span!("git_checkpoint", outcome = tracing::field::Empty);
    traced(&span, || commits::checkpoint(root, name, frontier, tree))
}

/// Whether `root` is a git repository (decision 239).
///
/// False only where every operation here refuses `NotARepository`: `root`
/// holds no `.git`, or `git` finds no repository there. A `.git` that
/// `git` can't use counts as one, so its operations fail (`Failed`).
pub(crate) fn is_repository(root: &Path) -> bool {
    !matches!(repo::Repo::open(root), Ok(None))
}

/// Move the mapped `branch` (a ref under `refs/heads/`) to a commit of
/// `tree`, the tree of `frontier` (a channel value of exactly one
/// frontier, every protected path at its approved heads; each path read
/// `Some` or `None` as for [`checkpoint`]), built as [`checkpoint`] builds
/// one and with its trailer, whose parent is the
/// branch's current commit, or none when the branch doesn't exist yet; a
/// branch checked out in the root gets `.git/index` rewritten to the new
/// tree before it moves, and its working tree, the mirror, untouched, so
/// an index git can't write leaves the branch where it was (`Failed`).
/// `Ok(None)` when the branch stays: its commit already has this tree, and
/// the index, which may hold the user's staged changes, is not touched.
/// `NotARepository` when `root` holds no `.git`, or `git` finds no
/// repository there.
pub(crate) fn update_branch(
    root: &Path,
    branch: &str,
    frontier: &FrontierId,
    tree: &BTreeMap<String, Option<TreeFile>>,
) -> Result<Option<GitHead>, GitRefusal> {
    let span = tracing::info_span!("git_branch", outcome = tracing::field::Empty);
    traced(&span, || {
        commits::update_branch(root, branch, frontier, tree)
    })
}

derive-exec

Source: crates/lvcs/src/runner/derive.rs.

/// One derivation run (row 180) of runner job `job`.
///
/// It regenerates the derived files from the documents `inputs` names at
/// `frontier`, running the generator `action`'s steps in the run directory
/// `<dir>/derive/`, where `dir` is the job's directory under
/// `.lvcs/tmp/ci/`, so `lvcs sandbox` gives it a check's profile.
/// `frontier` is the job's frontier less the documents deleted at their
/// heads there, as a check's [`super::Materialize`] is: an input it doesn't
/// hold is absent from the run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeriveJob {
    /// The runner job the run belongs to.
    pub(crate) job: JobId,
    /// The frontier it runs on.
    pub(crate) frontier: Frontier,
    /// The job's directory.
    pub(crate) dir: PathBuf,
    /// The generator.
    pub(crate) action: Action,
    /// The input documents' addresses.
    pub(crate) inputs: BTreeSet<String>,
}

/// How a derivation run ended.
///
/// It ends with the outputs it wrote, each derived file it created or
/// changed mapped to its bytes and their SHA-256 (sorted, at most
/// `GENERATION_OUTPUTS_MAX`), or why it failed. The runner machine's
/// own outcome type is lvcs-action's; the daemon maps this to it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DeriveResult {
    /// It exited 0 and wrote only derived files.
    Outputs(BTreeMap<String, Output>),
    /// It exited non-zero, was killed, or timed out (`Exit`), or it
    /// created, changed, or removed a file `is_derived` doesn't call
    /// derived (`NonDerivedWrite`).
    Failed(DeriveFailure),
}

/// One output of a derivation: its bytes and their SHA-256.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Output {
    /// The SHA-256 of its bytes.
    pub(crate) digest: [u8; 32],
    /// Its bytes, which the runner writes into its document.
    pub(crate) bytes: Vec<u8>,
}

/// Run derivation `job` on `files`, classifying what it touched.
///
/// `files` holds the bytes the shell read for its inputs at its frontier,
/// and each file the run touched is classified with `is_derived`, which
/// takes the file's path and its first
/// `DERIVED_HEADER_BYTES` (a removed file's from before the run).
///
/// The run directory is made fresh (0700, empty `work/`, `home/`,
/// `cargo/`, and `tmp/`), and `work/` holds each input `frontier` holds,
/// with its bytes from `files`. The generator's steps run there through
/// the `run` machine, journaled as `journal`, each through `lvcs sandbox`
/// with a change action's profile: no network, no secrets, within its
/// `timeout_secs`. A run that doesn't pass (a step's non-zero exit, a
/// kill, a timeout, or bad step files) is `Failed(Exit)`. Otherwise every regular file under `work/` that is new
/// or whose bytes changed, and every input that is gone, is classified: a
/// derived one that is new or changed is an output, and any other is
/// `Failed(NonDerivedWrite)`, as is anything under `work/` that is neither
/// a regular file nor a directory (a symlink, a device, a FIFO, a socket)
/// or whose path isn't UTF-8. The run directory ends with its `log` alone:
/// `work/`, `home/`, `cargo/`, and `tmp/` are removed whatever happened.
/// The run records a `derive` span whose `outcome` is `ok` for outputs and
/// `error` otherwise.
///
/// # Errors
///
/// With no result, which the runner must not record: an input
/// `frontier` holds is missing from `files` (a refused read) or isn't a
/// plain relative path; the run directory can't be made, written, walked,
/// or cleared; the sandbox can't be started; or the run's result can't be
/// recorded as a generation (a derived input it removed, which has no
/// bytes to name; more than `GENERATION_OUTPUTS_MAX` outputs; or a file
/// over `CHECK_FILE_BYTES`, which the sandbox's file size limit forbids).
pub(crate) fn derive(
    job: &DeriveJob,
    files: &BTreeMap<String, Vec<u8>>,
    is_derived: &dyn Fn(&str, &[u8]) -> bool,
    journal: RunJournal,
) -> io::Result<DeriveResult> {
    let span = tracing::info_span!("derive", outcome = Empty);
    let _entered = span.enter();
    let result = run_derivation(job, files, is_derived, journal);
    let outcome = match &result {
        Ok(DeriveResult::Outputs(_)) => Outcome::Ok,
        Ok(DeriveResult::Failed(_)) | Err(_) => Outcome::Error,
    };
    span.record("outcome", outcome.as_str());
    result
}
/// The input documents of a derivation at `frontier` (row 180).
///
/// Its default inputs are every file document `frontier` holds that isn't
/// derived (decision 171): `derived` names those whose `/meta/derived` is
/// true at their heads there, as the shell read them. `[derive] inputs`
/// patterns, when set, narrow the default, so a derived document is never
/// an input. `None` when a pattern doesn't parse, which the config's parse
/// already refuses.
pub(crate) fn derive_inputs(
    frontier: &Frontier,
    derived: &BTreeSet<String>,
    patterns: Option<&[String]>,
) -> Option<BTreeSet<String>> {
    let narrow = match patterns {
        Some(given) => Some(PathSet::new(given, false).ok()?),
        None => None,
    };
    Some(
        inputs::file_docs(frontier)
            .filter(|(address, _)| !derived.contains(*address))
            .filter(|(address, _)| narrow.as_ref().is_none_or(|set| set.matches(address)))
            .map(|(address, _)| address.clone())
            .collect(),
    )
}

/// What a derivation's `InputsHash` hashes: `[derive id, inputs,
/// toolchain_hash]`, `inputs` mapping each input document to its sorted
/// heads.
#[derive(Serialize)]
struct DeriveHashed<'a>(
    &'a ActionId,
    BTreeMap<&'a str, inputs::Heads>,
    #[serde(with = "lvcs_doc::cbor::fixed")] [u8; 32],
);

/// Derivation `derive`'s `InputsHash` at `frontier` (row 180).
///
/// It is `HASH([derive id, inputs, toolchain_hash])`, the derive id and the
/// toolchain hash as 32-byte byte strings, and `inputs` the map from each
/// address of `inputs` that `frontier` holds to its sorted heads, an array
/// of 32-byte byte strings, as a check's `InputsHash` maps a document that
/// isn't derived (decision 161).
///
/// # Errors
///
/// When the hashed array can't be encoded.
pub(crate) fn derive_inputs_hash(
    derive: &ActionId,
    frontier: &Frontier,
    inputs: &BTreeSet<String>,
    toolchain_hash: &[u8; 32],
) -> io::Result<InputsHash> {
    let heads: BTreeMap<&str, inputs::Heads> = frontier
        .docs
        .iter()
        .filter(|(address, _)| inputs.contains(*address))
        .map(|(address, heads)| (address.as_str(), inputs::Heads::of(heads)))
        .collect();
    cbor::digest(&DeriveHashed(derive, heads, *toolchain_hash))
        .map(InputsHash)
        .map_err(|err| io::Error::other(err.to_string()))
}

runner-exec

Source: crates/lvcs/src/runner/mod.rs.

/// Materialize `frontier` for one job into `dir`, the job's fresh directory
/// `.lvcs/tmp/ci/<job id>/` ([`job_dir`], 0700, owned by the
/// daemon's uid): one run directory per check, `<dir>/<check id>/`
/// ([`run_dir`]), whose `work/` holds the frontier's files that check's
/// `paths` and `uses` match, with the bytes the shell read through
/// `ReadAt`, beside a fresh `home/`, `cargo/`, and `tmp/`. A check sees
/// only its own declared inputs, so a reused result can't be wrong
/// because of a hidden one. `derived` names each derived document of
/// `frontier` (`/meta/derived` true at its heads there, as the shell read
/// it) with the SHA-256 of the bytes it is materialized as, which is how
/// it enters a check's `InputsHash` (draft row A).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Materialize {
    /// The frontier, less its documents deleted at their heads.
    pub(crate) frontier: Frontier,
    /// The job's directory.
    pub(crate) dir: PathBuf,
    /// Each derived document's address, with its bytes' SHA-256.
    pub(crate) derived: BTreeMap<String, [u8; 32]>,
}

/// What `lvcs sandbox` runs, read as one JSON object from standard input.
///
/// The spec comes through an inherited pipe, never from a path: `command`
/// (argv, no shell) in
/// the run directory `run`'s `work/` (or `cwd` under it), killed after `timeout_s` seconds (at
/// most `CHECK_TIMEOUT_SECS` for a check, `DEPLOY_TIMEOUT_SECS` for a
/// promote run). `run` must be a directory under `.lvcs/tmp/ci/` (a
/// check's) or `.lvcs/tmp/promote/` (a promote run's, decision 142) once
/// symlinks are resolved, and the sandbox refuses any other. The sandbox's filesystem holds only `run`'s `work/`
/// (writable) at `/work`, its `home/`, `cargo/`, and `tmp/` at `/home`,
/// `/cargo`, and `/tmp`, read-only, `nosuid` binds of `/usr`, `/bin`,
/// `/sbin`, `/lib`, and `/lib64` where present (a symlink among them, as
/// in a merged-`/usr` image, is recreated as the same symlink), and
/// `/dev/null`, `/dev/zero`, and `/dev/urandom`, read-only, `nosuid` binds
/// of the host's character devices, which the sandbox refuses to start
/// without (decision 134): a write still reaches the device, and a change
/// to a node's mode, times, or ACL fails. The command's standard input is
/// `/dev/null`. Its `/proc` holds only read-only binds of the host's
/// `/proc/cpuinfo`, `/proc/meminfo`, and `/proc/stat` (decision 248), so a
/// tool can count CPUs and memory. The environment is cleared, then
/// `PATH` (`path`'s entries, then the fixed `/usr/local/bin:/usr/bin:/bin`),
/// `HOME=/home`, `CARGO_HOME=/cargo`, `TMPDIR=/tmp`, and `LANG=C`, then
/// `env` and `secrets` in order (decisions 245 and 248), every name
/// non-empty and free of `=` and NUL, every value free of NUL, and their
/// bytes with `path`'s within `ACTION_ENV_BYTES`. Under `.lvcs/tmp/ci/` (a
/// change action's run) the sandbox refuses a spec with `network` set or
/// any `secrets` (fail closed). A promote run's may set `network`, which
/// skips only the network namespace, and `secrets`, its credentials
/// (decision 142): this pipe is the only way a credential reaches the
/// sandbox, so its `Debug` form names the variables and never their
/// values.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct SandboxSpec {
    /// The run directory.
    pub(crate) run: PathBuf,
    /// The command's argv, run with no shell.
    pub(crate) command: Vec<String>,
    /// Its wall clock, in seconds, within its profile's.
    pub(crate) timeout_s: u64,
    /// Whether it keeps the host's network (a promote run only).
    pub(crate) network: bool,
    /// The variables set after the fixed ones, by name.
    pub(crate) env: Vec<(String, String)>,
    /// The variables a secret hole filled, set after `env` (a promote run
    /// only).
    pub(crate) secrets: Vec<(String, String)>,
    /// The directories before the fixed `PATH`'s.
    pub(crate) path: Vec<String>,
    /// The working directory, relative to `/work`.
    pub(crate) cwd: Option<String>,
}

impl fmt::Debug for SandboxSpec {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("SandboxSpec")
            .field("run", &self.run)
            .field("command", &self.command)
            .field("timeout_s", &self.timeout_s)
            .field("network", &self.network)
            .field(
                "env",
                &self.env.iter().map(|(name, _)| name).collect::<Vec<_>>(),
            )
            .field(
                "secrets",
                &self
                    .secrets
                    .iter()
                    .map(|(name, _)| name)
                    .collect::<Vec<_>>(),
            )
            .field("path", &self.path)
            .field("cwd", &self.cwd)
            .finish()
    }
}

/// How one check command ended: `passed` when it exited 0 within its
/// timeout, `timed_out` when the sandbox killed it at its timeout (exit
/// status [`TIMED_OUT`], decision 230). Its last `CHECK_LOG_TAIL_BYTES` of output stay in its run
/// directory's `log`; `tail` is its readable text within
/// `CHECK_OUTPUT_TAIL_BYTES`, the command, its exit status, its failing
/// tests, the lines that look like errors, and the end of the output, with
/// stack frames left out, which a failed check's `check_failed`
/// notifications carry (decisions 193 and 242), and `tests` the failing
/// test names read from the log.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RunResult {
    /// Whether it exited 0 within its timeout.
    pub(crate) passed: bool,
    /// Whether the sandbox killed it at its timeout.
    pub(crate) timed_out: bool,
    /// Its readable text, within `CHECK_OUTPUT_TAIL_BYTES` (decision 242).
    pub(crate) tail: String,
    /// The failing test names read from its output.
    pub(crate) tests: Vec<String>,
}

/// The documents the shell reads through `ReadAt` at `frontier`'s heads
/// for a job of `checks`: every file document a check's `paths` or `uses`
/// matches.
pub(crate) fn reads(frontier: &Frontier, checks: &[Action]) -> BTreeSet<String> {
    inputs::reads(frontier, checks)
}

/// Write job `job`'s run directories from `files` and give each check's
/// `InputsHash`.
///
/// `files` holds each address the shell read, with its bytes. An
/// `InputsHash` is `HASH([check id, paths, uses, {}, toolchain_hash])`, the
/// check id and toolchain hash as 32-byte byte strings; `paths` the map
/// from each file document address the check's `paths` matches to its
/// sorted heads, an array of 32-byte byte strings; `uses` the same for its
/// `uses` matches, so a check reruns whenever a document either set matches
/// moves, a revert to equal bytes included (decision 161); and `{}` the
/// empty dependency frontier. A derived document (one `job.derived`
/// names) maps instead to the CBOR map `{"digest": <SHA-256 of its
/// bytes>}`, so a check reruns only when the bytes it materializes change
/// (draft row A); every other document keeps its heads. A check whose
/// `paths` or `uses` doesn't parse, one of whose documents is missing from
/// `files`, or one reading a derived document whose bytes in `files` don't
/// have the digest `job.derived` gives, is left out, which ends the job
/// unattested.
///
/// # Errors
///
/// When the job's directory, a run directory, or an input file can't be
/// written, or an `InputsHash` can't be encoded.
pub(crate) fn prepare(
    job: &Materialize,
    files: &BTreeMap<String, Vec<u8>>,
    checks: &[Action],
    toolchain_hash: &[u8; 32],
) -> io::Result<BTreeMap<ActionId, InputsHash>> {
    inputs::prepare(job, files, checks, toolchain_hash)
}

/// Run `check`'s steps for job `job` in its run directory through the
/// `run` machine ([`steps`]), journaled as `journal`, recording a `check`
/// span with `outcome` `ok` when it passed and `error` otherwise, a
/// timeout included.
pub(crate) fn run(job: &Materialize, check: &Action, journal: RunJournal) -> RunResult {
    let span = tracing::info_span!("check", outcome = tracing::field::Empty);
    let _entered = span.enter();
    let profile = Profile {
        network: false,
        env: Vec::new(),
        withhold: false,
        sha: job
            .frontier
            .id()
            .map(|id| crate::ids::hex(&id.0))
            .unwrap_or_default(),
    };
    let ran = check.id().ok().and_then(|id| {
        steps::execute(&run_dir(&job.dir, &id), check, &profile, journal)
            .inspect_err(|err| tracing::error!(error = %err, "a check couldn't run"))
            .ok()
    });
    let passed = ran.as_ref().is_some_and(|ran| ran.passed);
    let timed_out = ran.as_ref().is_some_and(|ran| ran.timed_out);
    let outcome = if passed { Outcome::Ok } else { Outcome::Error };
    span.record("outcome", outcome.as_str());
    let (tail, tests) = ran.and_then(|ran| ran.failed).map_or_else(
        || (String::new(), Vec::new()),
        |(command, status, log)| summary::summary(&command, status, &log),
    );
    RunResult {
        passed,
        timed_out,
        tail,
        tests,
    }
}

/// The runner's `toolchain_hash` (decisions 121 and 245): the `HASH` of
/// the CBOR array, sorted by value, of `[toolchain, [probe, ...]]` for
/// `rust` and each of `toolchains`, each probe the array `[exit status,
/// stdout bytes]` of its command run in the sandbox on the fixed `PATH`,
/// or the text `absent` when its binary is on none of the fixed `PATH`'s
/// directories.
/// Each probe runs in the fresh run directory `probe/` of the runner
/// directory under `layout`, after `true` has run there to show the
/// sandbox works: an error when it doesn't, so runner mode refuses to
/// start rather than attest under a toolchain hash of failed probes.
pub(crate) fn toolchain_hash(
    layout: &Layout,
    toolchains: &BTreeSet<Toolchain>,
) -> io::Result<[u8; 32]> {
    let probe = ci_dir(layout).join(PROBE_DIR);
    inputs::fresh_run_dir(&probe)?;
    let (status, _) = probe_check(&probe, &[SANDBOX_CHECK.to_owned()])?;
    if status != 0 {
        return Err(io::Error::other(format!(
            "the sandbox is unavailable here (lvcs sandbox exited {status})"
        )));
    }
    let mut probed: BTreeSet<Toolchain> = toolchains.clone();
    probed.insert(Toolchain::Rust);
    let mut results = Vec::new();
    for toolchain in probed {
        let mut probes = Vec::new();
        for argv in toolchain.probes() {
            let command: Vec<String> = argv.iter().map(|arg| (*arg).to_owned()).collect();
            let found = argv.first().is_some_and(|program| {
                SANDBOX_PATH
                    .iter()
                    .any(|dir| Path::new(dir).join(program).is_file())
            });
            if !found {
                probes.push(Probe::Absent(ABSENT));
                continue;
            }
            inputs::fresh_run_dir(&probe)?;
            let (status, stdout) = probe_check(&probe, &command)?;
            probes.push(Probe::Ran(status, Bytes(stdout)));
        }
        results.push((toolchain.as_str(), probes));
    }
    lvcs_doc::cbor::digest(&results).map_err(|err| io::Error::other(err.to_string()))
}

/// The runner directory `.lvcs/tmp/ci/` of the project at
/// `layout`, which holds every job directory.
pub(crate) fn ci_dir(layout: &Layout) -> PathBuf {
    layout.tmp_dir().join(CI_DIR)
}

/// Job `job`'s directory: `.lvcs/tmp/ci/<job id>/`, the id in
/// `ID_ENCODING`.
pub(crate) fn job_dir(layout: &Layout, job: &JobId) -> PathBuf {
    ci_dir(layout).join(ids::encode_id(&job.0))
}

/// Check `check`'s run directory in the job directory `job_dir`:
/// `<job_dir>/<check id>/`, the id in `ID_ENCODING`.
pub(crate) fn run_dir(job_dir: &Path, check: &ActionId) -> PathBuf {
    job_dir.join(ids::encode_id(&check.0))
}

/// Make `run` a fresh run directory: 0700, holding empty `work/`,
/// `home/`, `cargo/`, and `tmp/`, its parents created 0700.
///
/// # Errors
///
/// When what is at `run` can't be removed or a directory can't be created.
pub(crate) fn fresh_run_dir(run: &Path) -> io::Result<()> {
    inputs::fresh_run_dir(run)
}

/// Remove job directory `job_dir`'s build trees once its job has ended,
/// attested or not: each run directory keeps only its `log`, which stays
/// for the operator (decision 121), so an ended job holds neither its
/// inputs nor its build output on disk. A missing job directory has
/// nothing to remove.
pub(crate) fn prune(job_dir: &Path) -> io::Result<()> {
    inputs::prune(job_dir)
}

/// `lvcs sandbox`: read a [`SandboxSpec`] from standard input, enter the
/// sandbox, and run its command, exiting with the command's status (128
/// plus the signal for one a signal ended, 124 after its timeout), 125 for
/// a spec the sandbox refuses or a sandbox it can't set up, and 2 where
/// Linux namespaces are unavailable.
pub(crate) fn sandbox_main() -> ExitCode {
    let mut input = Vec::new();
    let spec = io::stdin()
        .read_to_end(&mut input)
        .map_err(|err| err.to_string())
        .and_then(|_| serde_json::from_slice::<SandboxSpec>(&input).map_err(|err| err.to_string()));
    match spec {
        Ok(spec) => enter(&spec),
        Err(err) => {
            let _ = writeln!(io::stderr(), "lvcs: sandbox: {err}");
            ExitCode::from(SANDBOX_REFUSED)
        }
    }
}

promote-exec

Source: crates/lvcs/src/runner/promote.rs.

/// One promote run, as the shell fills it from the runner actor's `Prepare`
/// and its promote action.
///
/// `run` is the run's key, `frontier` the frontier it promotes (the
/// actor's, deleted documents and all), `channel` the promote channel
/// it serves, and `action` the promote action. A credential's value is
/// never held here: the executor reads each from the process environment
/// as the step starts.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PromoteJob {
    /// The run's key, its `LVCS_RUN`.
    pub(crate) run: RunKey,
    /// The frontier it promotes, whose id is its `LVCS_FRONTIER`.
    pub(crate) frontier: Frontier,
    /// The channel it serves, its `LVCS_CHANNEL`.
    pub(crate) channel: String,
    /// The promote action.
    pub(crate) action: Action,
}

/// What one of a run's reads gave: a document's bytes at the frontier, or
/// the document deleted there, which the run sees absent (decision 145).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Input {
    /// The document's bytes.
    Bytes(Vec<u8>),
    /// The document is deleted at its heads in the frontier.
    Deleted,
}

/// How a promote run ended: `passed` when its command exited 0 within
/// `DEPLOY_TIMEOUT_SECS`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PromoteResult {
    /// Whether its command exited 0 within its timeout.
    pub(crate) passed: bool,
}

/// The executor's order (decision 168): the run holding it, from its reads
/// to its end, and the runs the actor asked to prepare meanwhile, oldest
/// first, each holding only its frontier and action.
#[derive(Debug, Default)]
pub(crate) struct Executor {
    /// The run holding the executor.
    current: Option<RunKey>,
    /// The runs waiting their turn, oldest first.
    waiting: VecDeque<PromoteJob>,
}

impl Executor {
    /// The actor asked to prepare `job`: it takes the executor and comes
    /// back when no run holds it, else it waits its turn. A run already
    /// holding or waiting for the executor is asked once.
    pub(crate) fn ask(&mut self, job: PromoteJob) -> Option<PromoteJob> {
        let asked = self.current == Some(job.run)
            || self.waiting.iter().any(|waiting| waiting.run == job.run);
        if asked {
            return None;
        }
        if self.current.is_some() {
            self.waiting.push_back(job);
            return None;
        }
        self.current = Some(job.run);
        Some(job)
    }

    /// Run `run` ended: when it held the executor, the oldest waiting run
    /// takes it and comes back; a waiting one leaves the queue.
    pub(crate) fn end(&mut self, run: RunKey) -> Option<PromoteJob> {
        if self.current != Some(run) {
            self.waiting.retain(|waiting| waiting.run != run);
            return None;
        }
        self.current = None;
        let next = self.waiting.pop_front()?;
        self.current = Some(next.run);
        Some(next)
    }
}

/// The documents the shell reads through `ReadAt` at `job`'s frontier:
/// every file document its `paths` match.
///
/// # Errors
///
/// When a `paths` pattern doesn't parse, which fails the run.
pub(crate) fn reads(job: &PromoteJob) -> io::Result<BTreeSet<String>> {
    let paths = PathSet::new(&job.action.inputs.paths, false).map_err(|err| {
        let reason = match err {
            PathSetError::Pattern { reason, .. } | PathSetError::Build { reason } => reason,
        };
        io::Error::other(format!("a paths pattern doesn't parse: {reason}"))
    })?;
    Ok(job
        .frontier
        .docs
        .keys()
        .filter(|address| address::kind_of(address) == Some(DocKind::File))
        .filter(|address| paths.matches(address))
        .cloned()
        .collect())
}

/// Write run `job`'s fresh directory ([`run_dir`], 0700, owned by the
/// daemon's uid) from what its reads gave.
///
/// Its `work/` holds each document [`reads`] names with its bytes, a
/// deleted one left absent, beside a fresh `home/`, `cargo/`, and `tmp/`.
/// The run directory comes back.
///
/// # Errors
///
/// When a document [`reads`] names has no answer in `inputs` (a document
/// refused the read, which fails the run), an address isn't a plain
/// relative path, or the directory or a file can't be written.
pub(crate) fn prepare(
    layout: &Layout,
    job: &PromoteJob,
    inputs: &BTreeMap<String, Input>,
) -> io::Result<PathBuf> {
    let addresses = reads(job)?;
    if addresses
        .iter()
        .any(|address| !inputs.contains_key(address))
    {
        return Err(io::Error::other("a document refused the run's read"));
    }
    let dir = run_dir(layout, &job.run);
    super::fresh_run_dir(&dir)?;
    let work = dir.join(WORK);
    for address in &addresses {
        if let Some(Input::Bytes(bytes)) = inputs.get(address) {
            write_input(&work, address, bytes)?;
        }
    }
    Ok(dir)
}

/// Run `job`'s promote action in its prepared directory, journaled as
/// `journal`, recording a `promote_run` span with `outcome` `ok` when it
/// passed and `error` otherwise.
///
/// Its profile sets `network` and, for every step, `LVCS_FRONTIER` (the
/// frontier's id), `LVCS_CHANNEL`, and `LVCS_RUN` (the run key); each
/// secret its `env` names is filled from this process's environment as
/// its step starts. A name missing there, or not Unicode, fails the step
/// before it starts, and its `log` says which; no value is ever logged,
/// journaled, put in a span, or written by LVCS under the project root.
pub(crate) fn run(layout: &Layout, job: &PromoteJob, journal: RunJournal) -> PromoteResult {
    let span = tracing::info_span!("promote_run", outcome = Empty);
    let _entered = span.enter();
    let dir = run_dir(layout, &job.run);
    let passed = match profile(job) {
        Ok(profile) => super::steps::execute(&dir, &job.action, &profile, journal)
            .inspect_err(|err| tracing::error!(error = %err, "a promote run couldn't run"))
            .is_ok_and(|ran| ran.passed),
        Err(reason) => {
            let line = format!("lvcs: promote: {reason}\n");
            if let Err(err) = super::fs_write_log(&dir, line.as_bytes()) {
                tracing::debug!(error = %err, "a promote run's log couldn't be written");
            }
            false
        }
    };
    let outcome = if passed { Outcome::Ok } else { Outcome::Error };
    span.record("outcome", outcome.as_str());
    PromoteResult { passed }
}

/// Remove the ended runs' `work/`, `home/`, `cargo/`, and `tmp/` under the
/// promote directory, each run's `log` staying (decision 168).
///
/// One run holds the executor at a time, so only the run that just ended
/// has any to remove; a missing promote directory has nothing to remove.
///
/// # Errors
///
/// When the promote action directory can't be listed or a tree can't be removed.
pub(crate) fn end(layout: &Layout) -> io::Result<()> {
    super::prune(&promote_dir(layout))
}

secret-entropy

Source: crates/lvcs/src/secrets/entropy.rs.

/// The rule a hit of the heuristic names in `SecretHit::rule`.
const ENTROPY_RULE: &str = "entropy";

/// Fewest code points of a candidate: a maximal run of ASCII letters,
/// digits, `+`, `/`, `_`, and `-` that holds an uppercase letter, a
/// lowercase letter, and a digit.
const ENTROPY_MIN_LEN: usize = 24;

/// Least share of adjacent pairs of the candidate's letters and digits
/// (its other code points skipped) that switch between uppercase,
/// lowercase, and digit.
const ENTROPY_MIN_SWITCHES: f64 = 0.35;

/// Least Shannon entropy of a candidate of [`ENTROPY_MIN_LEN`] code
/// points, in bits per code point. The floor rises with the candidate's
/// length, since a longer random string reaches a higher entropy.
const ENTROPY_FLOOR_MIN: f64 = 3.9;

/// How much the entropy floor rises per code point past
/// [`ENTROPY_MIN_LEN`].
const ENTROPY_FLOOR_STEP: f64 = 0.0375;

/// The highest the entropy floor rises, reached at 40 code points.
const ENTROPY_FLOOR_MAX: f64 = 4.5;

/// Fewest lowercase letters in a row that make a word, together with the
/// uppercase letter right before them (`Address` in `Ipv4AddressToString`).
const WORD_MIN_LOWER: usize = 3;

/// Most share of a candidate's letters that may sit in words: an
/// identifier (`NewSessionTicketPayloadTls13`) or a path is mostly words,
/// and a random string seldom is.
const ENTROPY_MAX_WORD_SHARE: f64 = 0.6;

/// Labels a digest starts with, so a candidate starting with one is not a
/// secret: `sha512-` in npm lockfiles and HTML `integrity` attributes.
const DIGEST_PREFIXES: &[&str] = &["sha1-", "sha256-", "sha384-", "sha512-"];

/// Labels public data comes right after, so a candidate following one is
/// not a secret: `h1:` in Go's `go.sum`, a data URI's `;base64,`, and the
/// key types of SSH public keys.
const PUBLIC_LEADS: &[&str] = &[
    "h1:",
    ";base64,",
    "ssh-rsa ",
    "ssh-dss ",
    "ssh-ed25519 ",
    "ecdsa-sha2-nistp256 ",
    "ecdsa-sha2-nistp384 ",
    "ecdsa-sha2-nistp521 ",
    "sk-ssh-ed25519@openssh.com ",
    "sk-ecdsa-sha2-nistp256@openssh.com ",
];

secret-formats

Source: crates/lvcs/src/secrets/formats.rs.

/// The characters a token's body may hold, all ASCII.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Body {
    /// `A-Z a-z 0-9`.
    Alnum,
    /// `A-Z 0-9`.
    UpperAlnum,
    /// `0-9 a-f`.
    LowerHex,
    /// `A-Z a-z 0-9 _`.
    AlnumUnderscore,
    /// `A-Z a-z 0-9 -`.
    AlnumDash,
    /// `A-Z a-z 0-9 _ -` (base64url).
    Base64Url,
    /// `A-Z a-z 0-9 /`.
    AlnumSlash,
}

/// How many code points a token's body holds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Len {
    Exactly(usize),
    AtLeast(usize),
}

/// How a known token format is recognized. A token starts only where the
/// code point before it is not an ASCII letter, digit, `_`, or `-`, so a
/// prefix inside a longer word (`task_live_`) is not a token.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
    /// One of `prefixes`, then the longest run of `body` characters, whose
    /// length must fit `len`, followed by no ASCII letter or digit. With
    /// `mixed`, the run must also hold an uppercase letter, a lowercase
    /// letter, and a digit, as a random token does and a word does not.
    Prefixed {
        prefixes: &'static [&'static str],
        body: Body,
        len: Len,
        mixed: bool,
    },
    /// A JSON Web Token: three base64url segments of at least
    /// [`JWT_SEGMENT_MIN`] code points joined by `.`, the first two
    /// starting `eyJ` (a base64url-encoded JSON object).
    Jwt,
}

/// A known token format; `rule` names it in `SecretHit::rule` and the
/// API's `SecretData.rule`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct TokenFormat {
    rule: &'static str,
    shape: Shape,
}

/// The rule of a PEM private key: `-----BEGIN <label>-----` whose label
/// ends in `PRIVATE KEY` or `PRIVATE KEY BLOCK`, through the first
/// `-----END ` after it when that closes the same label, with at least
/// [`PEM_BODY_MIN`] code points between them. A header with no body, as
/// code that parses PEM names it, is not a hit; a body written without
/// its footer is left to the high-entropy heuristic, line by line. A PEM
/// block with any other label (`CERTIFICATE`, `PUBLIC KEY`) is public
/// data: the heuristic skips it.
const PEM_PRIVATE_KEY_RULE: &str = "private-key";

/// Fewest code points between a PEM private key's header and footer.
const PEM_BODY_MIN: usize = 64;

/// Fewest code points in each segment of a JSON Web Token.
const JWT_SEGMENT_MIN: usize = 10;

/// The known token formats, tried in this order at each position: the
/// first that matches names the hit, so a format comes before a more
/// general one sharing its prefix (`anthropic-key` before `sk-key`).
const TOKEN_FORMATS: &[TokenFormat] = &[
    TokenFormat {
        rule: "aws-access-key-id",
        shape: Shape::Prefixed {
            prefixes: &["AKIA", "ASIA", "ABIA", "ACCA"],
            body: Body::UpperAlnum,
            len: Len::Exactly(16),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "github-token",
        shape: Shape::Prefixed {
            prefixes: &["ghp_", "gho_", "ghu_", "ghs_", "ghr_"],
            body: Body::Alnum,
            len: Len::AtLeast(36),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "github-pat",
        shape: Shape::Prefixed {
            prefixes: &["github_pat_"],
            body: Body::AlnumUnderscore,
            len: Len::AtLeast(40),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "gitlab-token",
        shape: Shape::Prefixed {
            prefixes: &["glpat-"],
            body: Body::Base64Url,
            len: Len::AtLeast(20),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "slack-token",
        shape: Shape::Prefixed {
            prefixes: &["xoxa-", "xoxb-", "xoxe-", "xoxp-", "xoxr-", "xoxs-"],
            body: Body::AlnumDash,
            len: Len::AtLeast(10),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "slack-webhook",
        shape: Shape::Prefixed {
            prefixes: &["https://hooks.slack.com/services/"],
            body: Body::AlnumSlash,
            len: Len::AtLeast(20),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "stripe-key",
        shape: Shape::Prefixed {
            prefixes: &["sk_live_", "rk_live_", "sk_test_", "rk_test_"],
            body: Body::Alnum,
            len: Len::AtLeast(16),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "anthropic-key",
        shape: Shape::Prefixed {
            prefixes: &["sk-ant-"],
            body: Body::Base64Url,
            len: Len::AtLeast(32),
            mixed: true,
        },
    },
    TokenFormat {
        rule: "sk-key",
        shape: Shape::Prefixed {
            prefixes: &["sk-"],
            body: Body::Base64Url,
            len: Len::AtLeast(32),
            mixed: true,
        },
    },
    TokenFormat {
        rule: "google-api-key",
        shape: Shape::Prefixed {
            prefixes: &["AIza"],
            body: Body::Base64Url,
            len: Len::Exactly(35),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "huggingface-token",
        shape: Shape::Prefixed {
            prefixes: &["hf_"],
            body: Body::Alnum,
            len: Len::AtLeast(34),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "npm-token",
        shape: Shape::Prefixed {
            prefixes: &["npm_"],
            body: Body::Alnum,
            len: Len::AtLeast(36),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "pypi-token",
        shape: Shape::Prefixed {
            prefixes: &["pypi-AgEIcHlwaS5vcmc"],
            body: Body::Base64Url,
            len: Len::AtLeast(50),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "digitalocean-token",
        shape: Shape::Prefixed {
            prefixes: &["dop_v1_", "doo_v1_", "dor_v1_"],
            body: Body::LowerHex,
            len: Len::Exactly(64),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "age-secret-key",
        shape: Shape::Prefixed {
            prefixes: &["AGE-SECRET-KEY-1"],
            body: Body::UpperAlnum,
            len: Len::Exactly(58),
            mixed: false,
        },
    },
    TokenFormat {
        rule: "jwt",
        shape: Shape::Jwt,
    },
];

secrets

Source: crates/lvcs/src/secrets/mod.rs.

/// What the secret scan found (design §15): code points `start..end` of
/// the scanned text and the `rule` that flagged them (a known token
/// format's name, or `entropy` for the high-entropy heuristic). The daemon
/// answers with it as `SecretData`. Owner: `M3.secrets`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SecretHit {
    pub(crate) start: usize,
    pub(crate) end: usize,
    pub(crate) rule: String,
}

/// `lvcs.toml` `[secrets]` (design §15), part of the daemon's `LvcsToml`:
/// `exempt` lists paths the scan skips (`tests/fixtures/**`), and
/// `never_sync` paths never read into documents beyond `DEFAULT_NEVER_SYNC`
/// and `.gitignore`, both gitignore syntax relative to the tree's root. A
/// missing key is empty. Owner: `M3.secrets`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(default)]
pub(crate) struct SecretsConfig {
    pub(crate) exempt: Vec<String>,
    pub(crate) never_sync: Vec<String>,
}

/// The scanner the daemon runs on a request's resulting text before it is
/// committed: `config` from `lvcs.toml`, and `allowed`, the hashes of the
/// strings participants confirmed with `secret.confirm` (the signed allowlist,
/// which is project data, not `lvcs.toml`). Owner: `M3.secrets`.
#[derive(Debug, Clone, Default)]
pub(crate) struct Scanner {
    config: SecretsConfig,
    allowed: BTreeSet<[u8; 32]>,
}

channels-actor

Source: crates/lvcs-action/src/channels/mod.rs.

/// A query the channels actor asks a document, echoed by its answer.
/// Chosen by the actor, in order.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct QueryId(pub u64);

/// The channels actor's events, all journaled.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelsEvent {
    /// First in every incarnation, and again whenever main's `lvcs.toml`
    /// approved heads move (decision 127).
    Config(ChannelsConfig),
    /// Second: the approved heads `.lvcs/approved.cbor` holds.
    Restore(Restore),
    /// A channel document rendered (`DocOutput::ChannelRendered`).
    Entries(Entries),
    /// The registry rendered new bundle records.
    Bundles(Bundles),
    /// A protected file document opened: its baseline heads.
    Baseline(Baseline),
    /// A document answered a `Descends` query.
    Descended(Descended),
    /// A document answered an `Overlap` query.
    Overlapped(Overlapped),
    /// `promote` asks whether a frontier may be promoted (decision 206),
    /// answered by [`ChannelsOutput::Promotable`].
    Promotable(Promotable),
}

/// The policy from main's `lvcs.toml` `[[actions]]` at its approved heads
/// (decisions 127, 141, and 245, row 180).
///
/// `required` is the id of every change action a `ci` attestation names
/// (every one but the generator), `ci_threshold` the attestations `ci`
/// counts a frontier at (the largest change action's quorum,
/// `Actions::ci_threshold`), `promote` every promote action,
/// `generator` the generator's id and quorum, when an action has `writes`,
/// and `ci_held` whether a source of the set refused something (decision
/// 245, stage C): `ci`'s value then doesn't move and no frontier is
/// eligible for a promotion, though approvals still count from `ci`'s
/// attestations, so the fix can be approved.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelsConfig {
    /// The change actions' ids a `ci` attestation names.
    pub required: BTreeSet<ActionId>,
    /// The attestations `ci` counts a frontier at.
    pub ci_threshold: u32,
    /// Every promote action.
    pub promote: Vec<Action>,
    /// The generator, when an action has `writes`.
    pub generator: Option<Generator>,
    /// Whether `ci` is held; left out of the encoding when not, so a
    /// policy's digest is what it was before stage C.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub ci_held: bool,
}

/// The generator's id and the records its generations need.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Generator {
    /// The generator's id: the generations of it count.
    pub id: ActionId,
    /// The records with agreeing digests a generation needs to count.
    pub quorum: u32,
}

/// Each protected path's approved heads, by address, from
/// `.lvcs/approved.cbor` (empty when it is missing).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Restore {
    /// Each protected path's approved heads.
    #[serde(with = "lvcs_doc::cbor::head_map")]
    pub approved: BTreeMap<String, Vec<ChangeHash>>,
}

/// The entries channel `channel` added since its last render.
///
/// Every one on its incarnation's first render (`full`), and only new ones
/// after. Every value at each new key is listed, so concurrent puts at one key
/// both appear.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Entries {
    /// The channel.
    pub channel: ChannelName,
    /// The entries added.
    pub added: Vec<ChannelEntry>,
    /// Whether `added` is every entry.
    pub full: bool,
}

/// The bundle records the registry render added since the last (all of
/// them the first time, `full`), computed by the shell from consecutive
/// `RegistryRendered` records.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Bundles {
    /// The records added.
    pub added: Vec<BundleRecord>,
    /// Whether `added` is every record.
    pub full: bool,
}

/// One bundle's merge record: each document it lists, with its side.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleRecord {
    /// The bundle.
    pub bundle: BundleId,
    /// Each document it lists, by address, with its side.
    pub sides: BTreeMap<String, BundleSide>,
}

/// A protected file document's `Baseline` answer.
///
/// The document at `address` opened, or changed while its baseline was
/// its genesis heads, and answered `heads`: after genesis and the owner's
/// import change, or genesis's own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Baseline {
    /// The document's address.
    pub address: String,
    /// Its baseline heads.
    #[serde(with = "lvcs_doc::cbor::heads")]
    pub heads: Vec<ChangeHash>,
}

/// The answer to `Descends` query `req`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Descended {
    /// The query answered.
    pub req: QueryId,
    /// The answer.
    pub outcome: Descent,
}

/// The answer to `Overlap` query `req`: whether the two bundles are
/// concurrent in that document, and whether their ranges overlap there.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Overlapped {
    /// The query answered.
    pub req: QueryId,
    /// Whether the two bundles are concurrent in the document.
    pub concurrent: bool,
    /// Whether their ranges overlap there.
    pub overlapping: bool,
}

/// `promote`'s question (decision 206): may `frontier` be promoted on
/// `channel`? The shell's `req` is echoed by the answer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Promotable {
    /// The shell's request, echoed.
    pub req: RequestId,
    /// The promote channel to promote on.
    pub channel: ChannelName,
    /// The frontier to promote.
    pub frontier: FrontierId,
}

/// The answer to a [`Promotable`] question: row 141's refusal rule for
/// `promote`, from the actor's own state (decision 206).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PromoteVerdict {
    /// No promote action at approved heads moves the channel.
    NoAction,
    /// The frontier is neither eligible for the channel's promote action nor named
    /// by a passed outcome the channel holds.
    NotEligible,
    /// The frontier may be promoted.
    Promotable,
}

/// A promote channel's pending run (decision 141): the run of its
/// greatest promotion `promotion`, of `frontier`, keyed `run`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct PendingRun {
    /// The run key, `HASH([ACTION_DOMAIN, action, frontier id, promotion])`.
    pub run: RunKey,
    /// The frontier the run promotes.
    pub frontier: FrontierId,
    /// The promotion it serves, by entry id.
    pub promotion: EntryId,
}

/// What the channels actor asks the shell to do.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelsOutput {
    /// Ask the document at `address` whether `heads` descends from `from`
    /// (`DocEvent::Descends`), answered by `Descended`; asked only for
    /// documents whose heads differ, and with `from` empty to ask whether
    /// the document holds `heads` at all. The shell answers every one,
    /// `Unknown` when the document refuses or is missing.
    Descends {
        /// The query.
        req: QueryId,
        /// The document's address.
        address: String,
        /// The heads that may descend.
        #[serde(with = "lvcs_doc::cbor::heads")]
        heads: Vec<ChangeHash>,
        /// The heads they may descend from.
        #[serde(with = "lvcs_doc::cbor::heads")]
        from: Vec<ChangeHash>,
    },
    /// Ask the document at `address` whether bundles `a` and `b` are
    /// concurrent and overlap there (`DocEvent::CheckOverlap`), answered by
    /// `Overlapped`. The shell answers every one, a refused check as
    /// concurrent and overlapping.
    Overlap {
        /// The query.
        req: QueryId,
        /// The document's address.
        address: String,
        /// The first bundle's side there.
        a: BundleSide,
        /// The second bundle's side there.
        b: BundleSide,
    },
    /// Channel `channel`'s value moved: for `ci`, its
    /// counted frontiers that no other counted frontier supersedes, sorted
    /// by id; for a promote channel, the frontier of its greatest
    /// satisfied promotion, zero or one (decision 141). Never re-sent
    /// unchanged (decision 145).
    Value {
        /// The channel.
        channel: ChannelName,
        /// Its value.
        frontiers: Vec<FrontierId>,
    },
    /// Protected path `path`'s approved heads moved to `heads`; the shell
    /// persists them (decision 127).
    Approved {
        /// The protected path.
        path: String,
        /// Its approved heads.
        #[serde(with = "lvcs_doc::cbor::heads")]
        heads: Vec<ChangeHash>,
    },
    /// promote channel `channel`'s pending run changed: the run of its
    /// greatest promotion, or none (decision 141). For the runner actor.
    Candidates {
        /// The channel.
        channel: ChannelName,
        /// Its pending run.
        run: Option<PendingRun>,
    },
    /// promote channel `channel` newly holds an outcome, passed or
    /// failed, of each run in `runs` (decision 142). For the runner actor.
    Outcomes {
        /// The channel.
        channel: ChannelName,
        /// The runs, sorted.
        runs: Vec<RunKey>,
    },
    /// What `channel.read` reports of promote channel `channel`'s
    /// value `frontier` moved (decision 141): `runners`, the runners with a
    /// passed outcome naming it, and `promotions`, the promotions naming
    /// it. Sent after the `Value` that set `frontier`, and never re-sent
    /// unchanged.
    Promoted {
        /// The channel.
        channel: ChannelName,
        /// Its value.
        frontier: FrontierId,
        /// The runners with a passed outcome naming it.
        runners: u32,
        /// The promotions naming it.
        promotions: u32,
    },
    /// The answer to [`ChannelsEvent::Promotable`] `req`.
    Promotable {
        /// The shell's request, echoed.
        req: RequestId,
        /// The answer.
        verdict: PromoteVerdict,
    },
    /// A generation of derivation `derive` changed (row 180). With
    /// `Counted`, it is the generation main's visible frontier should now
    /// show: among the counted generations, one whose frontier no other's
    /// frontier supersedes, ties to the greater `InputsHash` bytes. With
    /// `Diverged` or `Failed`, its inputs just came to that state, so they
    /// never count, and a shown generation that came to it is shown no
    /// more. `frontier` and `outputs` are the standing record's
    /// (`policy::generation`). Never re-sent unchanged.
    Generated {
        /// The generator.
        action: ActionId,
        /// The generation's `InputsHash`.
        inputs: InputsHash,
        /// The standing record's frontier.
        frontier: FrontierId,
        /// The standing record's outputs.
        outputs: BTreeMap<String, GeneratedOutput>,
        /// What its records come to.
        state: GenerationState,
    },
    /// No generation is shown any more (row 180): the generator went, or
    /// no counted generation stays one the view contains.
    Ungenerated,
}

action-config

Source: crates/lvcs-action/src/config.rs.

/// One `[[actions]]` entry of `lvcs.toml` as written (decision 245).
///
/// `name` is `[a-z0-9._-]+`, unique in the file. `on` is `change` (the
/// default: it runs on every merge into main and when main goes quiet, and
/// moves `ci`) or `promote` (it runs only on a promotion of its own
/// channel, which is its name). `after` names the channel a promote
/// action promotes from: `ci` (the default) or another promote action.
/// `paths` and `uses` are gitignore-syntax patterns (a check's were);
/// `paths` may be left out only by the generator. `run` is the argument
/// list of the action's one step (no shell); `steps` lists its steps
/// instead (decision 248), and exactly one of the two is given. `env` sets
/// variables for every step, each value a template (`${{ secrets.<NAME>
/// }}` in a promote action naming one of its `secrets`). `toolchain`
/// names values of the bounded `toolchain` vocabulary the action needs,
/// and a `node` step adds `node`. `writes` names the derived
/// files a change action generates: at most one action has it, and with no
/// `paths` its inputs are every synced file that isn't derived. `secrets`
/// (promote only) names the credential variables a run copies from its
/// runner's environment, never their values. `quorum` raises the
/// attestations or outcomes the action needs above `QUORUM_DEFAULT`, and a
/// promote action's above `QUORUM_PROTECTED`. `timeout_secs` can only
/// lower the class bound (`CHECK_TIMEOUT_SECS` for a change action,
/// `DEPLOY_TIMEOUT_SECS` for a promote action). Any other key is refused
/// at parse, and so is a list [`map`] refuses.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ActionEntry {
    /// The action's name.
    pub name: String,
    /// What runs it; `change` by default.
    #[serde(default)]
    pub on: On,
    /// The channel a promote action promotes from; `ci` by default.
    #[serde(default)]
    pub after: Option<String>,
    /// The paths it covers, in gitignore syntax.
    #[serde(default)]
    pub paths: Option<Vec<String>>,
    /// The further paths it reads, in gitignore syntax; empty by default.
    #[serde(default)]
    pub uses: Vec<String>,
    /// Its one step's argument list, when `steps` is not given.
    #[serde(default)]
    pub run: Option<Vec<String>>,
    /// Its steps, when `run` is not given (decision 248).
    #[serde(default)]
    pub steps: Vec<StepEntry>,
    /// The variables every step runs with, each value a template.
    #[serde(default)]
    pub env: BTreeMap<String, String>,
    /// The toolchains it needs, values of the `toolchain` vocabulary.
    #[serde(default)]
    pub toolchain: Vec<String>,
    /// The derived paths it generates, in gitignore syntax; empty by
    /// default.
    #[serde(default)]
    pub writes: Vec<String>,
    /// The credential variable names a promote action's run receives;
    /// empty by default.
    #[serde(default)]
    pub secrets: Vec<String>,
    /// The attestations or outcomes it needs, when above its floor.
    #[serde(default)]
    pub quorum: Option<u32>,
    /// Its wall clock and CPU limit, when below its class bound.
    #[serde(default)]
    pub timeout_secs: Option<u32>,
}

/// One `[[actions.steps]]` entry (decision 248): exactly one of `run` (an
/// argument list, no shell), `script` (text run by `shell`: `bash`, `sh`,
/// `python`, or a custom command line holding `{0}`; `bash -e {0}`, or `sh
/// -e {0}` without bash, when unset), and `node` (a directory of the
/// action's inputs holding `main`, and optionally `pre` and `post`, each
/// run with `node`, its inputs given by `with`).
///
/// `id` names it for `${{ steps.<id>.outputs.<name> }}`; `if` is
/// `success()` (the default), `always()`, `failure()`, `cancelled()`, or a
/// step output; `env` is set after the action's and earlier steps'
/// `GITHUB_ENV`; `working_directory` is relative to `/work`;
/// `continue_on_error` lets it fail without failing the run; and
/// `timeout_secs` can only lower what is left of the action's limit.
/// `script`, `env`, and `with` values are templates. Any other key is
/// refused at parse.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StepEntry {
    /// Its id.
    #[serde(default)]
    pub id: Option<String>,
    /// Its `if`.
    #[serde(default, rename = "if")]
    pub cond: Option<String>,
    /// Its environment.
    #[serde(default)]
    pub env: BTreeMap<String, String>,
    /// Its working directory, relative to `/work`.
    #[serde(default)]
    pub working_directory: Option<String>,
    /// Whether its failure leaves the run passing.
    #[serde(default)]
    pub continue_on_error: bool,
    /// Its own limit, in seconds.
    #[serde(default)]
    pub timeout_secs: Option<u32>,
    /// An argument list.
    #[serde(default)]
    pub run: Option<Vec<String>>,
    /// A script's text.
    #[serde(default)]
    pub script: Option<String>,
    /// The script's shell.
    #[serde(default)]
    pub shell: Option<String>,
    /// A node program's directory.
    #[serde(default)]
    pub node: Option<String>,
    /// The node program, in `node`.
    #[serde(default)]
    pub main: Option<String>,
    /// The node program run first, in `node`.
    #[serde(default)]
    pub pre: Option<String>,
    /// The node program run last, in `node`.
    #[serde(default)]
    pub post: Option<String>,
    /// The node program's inputs.
    #[serde(default)]
    pub with: BTreeMap<String, String>,
}

/// An `[[actions]]` entry's `on`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum On {
    /// It runs on every merge into main and when main goes quiet.
    #[default]
    Change,
    /// It runs on a promotion of its own channel.
    Promote,
}

/// `[runner]` (decisions 161 and 245): how long main's visible frontier
/// must be unchanged before a `ci` runner prepares a job for it.
///
/// `quiet_ms` can only raise `QUIET_MS`, up to `QUIET_MAX_MS`: a smaller
/// value counts as `QUIET_MS` and a larger one as `QUIET_MAX_MS`. It
/// defaults to `QUIET_MS`. Any other key is refused at parse.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RunnerTable {
    /// The quiet period asked for, in milliseconds.
    pub quiet_ms: u64,
}

action

Source: crates/lvcs-action/src/config.rs.

/// An action's name (decision 245): `[a-z0-9._-]+`, holding no `..`. A
/// promote action's name is also its channel's.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ActionName(String);

/// Where an action was declared: `lvcs.toml`, or a GitHub workflow file
/// (stage C), whose actions are named `<file stem>/<job id>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionSource {
    /// An `[[actions]]` entry of `lvcs.toml`.
    LvcsToml,
    /// A job of a `.github/workflows/` file.
    Workflow,
}

/// What runs an action.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Trigger {
    /// Every merge into main, and main going quiet (decision 161); the
    /// action's channel is `ci`.
    Change,
    /// A promotion of the action's own channel, of a frontier `after`
    /// holds (decision 141's `from`).
    Promote {
        /// `ci` or another promote action's channel.
        after: ChannelName,
    },
}

/// The paths an action reads: `paths`, which it covers, and `uses`, which
/// it reads besides, both gitignore-syntax patterns.
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Inputs {
    /// The paths it covers; for the generator, empty means every synced
    /// file that isn't derived.
    pub paths: Vec<String>,
    /// The paths it reads besides.
    pub uses: Vec<String>,
}

/// The canonical action (decision 245): one sandboxed run over one
/// revision, keyed by its inputs and the runner's toolchain, signed by its
/// runner, and reused while its inputs are unchanged.
///
/// `channel` is `ci` for a change action and the action's name for a
/// promote action. `writes` is non-empty only for the generator. `secrets`
/// is non-empty only for a promote action. `timeout_secs` is resolved: the
/// class bound or the lower value asked for. `quorum` is as written; the
/// count it means is [`Action::quorum`]. Its id is [`Action::id`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Action {
    /// Its name.
    pub name: ActionName,
    /// Where it was declared.
    pub source: ActionSource,
    /// What runs it.
    pub on: Trigger,
    /// The channel it moves.
    pub channel: ChannelName,
    /// The paths it reads.
    pub inputs: Inputs,
    /// The derived paths it generates.
    pub writes: Vec<String>,
    /// Its steps, in order.
    pub steps: Vec<Step>,
    /// The variables every step runs with; a promote action's holds each
    /// of its secrets under the secret's own name.
    pub env: BTreeMap<String, Template>,
    /// The credential variable names its run receives.
    pub secrets: Vec<String>,
    /// The toolchains it needs (`rust` is always probed besides).
    #[serde(with = "toolchains")]
    pub toolchain: BTreeSet<Toolchain>,
    /// Its wall clock and CPU limit, in seconds.
    pub timeout_secs: u32,
    /// The attestations or outcomes it asks for, as written.
    pub quorum: Option<u32>,
    /// The actions it runs after (a workflow job's `needs`, every cell of
    /// each needed job), by name.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub after_actions: Vec<ActionName>,
}

/// Why [`map`] refused an `[[actions]]` list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionConfigError {
    /// More than `PROMOTE_ACTIONS_MAX` promote actions.
    TooManyPromote,
    /// A name outside `[a-z0-9._-]+`, or holding `..`, or `ci` on a
    /// promote action.
    Name {
        /// The name refused.
        name: String,
    },
    /// Two entries have one name.
    Duplicate {
        /// The name given twice.
        name: String,
    },
    /// An entry's `run` is empty, or it gives both or neither of `run`
    /// and `steps`.
    EmptyRun {
        /// The entry's name.
        name: String,
    },
    /// A step is refused (decision 248).
    Step {
        /// The entry's name.
        name: String,
        /// The step's place, from 1.
        step: usize,
        /// Why.
        reason: String,
    },
    /// The action's `env`, `toolchain`, or `steps` is refused as a whole
    /// (decision 248).
    Action {
        /// The entry's name.
        name: String,
        /// Why.
        reason: String,
    },
    /// An entry other than the generator leaves out `paths`.
    NoPaths {
        /// The entry's name.
        name: String,
    },
    /// A change action sets `after` or `secrets`, or a promote action sets
    /// `writes`.
    NotFor {
        /// The entry's name.
        name: String,
        /// The key set.
        key: &'static str,
    },
    /// More than one action sets `writes`.
    TwoGenerators {
        /// The second one's name.
        name: String,
    },
    /// A promote action's `after` names itself, or is neither `ci` nor
    /// another promote action.
    After {
        /// The entry's name.
        name: String,
    },
    /// Promote actions' `after` links form a cycle through `name`.
    AfterCycle {
        /// An action on the cycle.
        name: String,
    },
    /// `paths`, `uses`, or `writes` holds more than `ACTION_PATTERNS_MAX`
    /// patterns.
    TooManyPatterns {
        /// The entry's name.
        name: String,
        /// The key.
        key: &'static str,
    },
    /// A pattern lvcs-doc's `paths` matcher refuses.
    Pattern {
        /// The entry's name.
        name: String,
        /// The key holding it.
        key: &'static str,
        /// The matcher's refusal, which names the pattern.
        error: PathSetError,
    },
    /// `writes` matches a file at the root that is never derived
    /// (`lvcs.toml`, `.gitignore`, or `.gitattributes`, row 179's
    /// `NEVER_DERIVED`).
    NeverDerived {
        /// The entry's name.
        name: String,
        /// The root file matched.
        file: &'static str,
    },
    /// `secrets` lists more than `ACTION_SECRETS_MAX` names.
    TooManySecrets {
        /// The entry's name.
        name: String,
    },
    /// A `secrets` name [`secret_name_allowed`] refuses.
    SecretName {
        /// The entry's name.
        name: String,
        /// The secret name refused.
        secret: String,
    },
    /// `timeout_secs` is 0.
    ZeroTimeout {
        /// The entry's name.
        name: String,
    },
}

/// The variables the sandbox sets itself, which a secret name may not be
/// (decision 141).
pub const ENV_RESERVED: [&str; 5] = ["PATH", "HOME", "CARGO_HOME", "TMPDIR", "LANG"];

/// The prefixes a secret name may not have: the dynamic loader's and
/// LVCS's own (decision 141).
pub const ENV_RESERVED_PREFIXES: [&str; 2] = ["LD_", "LVCS_"];

expr

Source: crates/lvcs-action/src/expr.rs.

/// A number, compared and ordered by its bits so the canonical form is
/// totally ordered.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Num(pub f64);

/// A value: GitHub's null, booleans, numbers, strings, arrays, and objects,
/// each object's keys sorted.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Value {
    /// `null`.
    Null,
    /// A boolean.
    Bool(bool),
    /// A number.
    Num(Num),
    /// A string.
    Str(String),
    /// An array.
    Array(Vec<Value>),
    /// An object, its keys sorted.
    Object(BTreeMap<String, Value>),
}

/// A function GitHub's expressions call.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Func {
    /// `contains(haystack, needle)`.
    Contains,
    /// `startsWith(text, prefix)`.
    StartsWith,
    /// `endsWith(text, suffix)`.
    EndsWith,
    /// `format(spec, args...)`.
    Format,
    /// `join(items, separator?)`.
    Join,
    /// `toJSON(value)`.
    ToJson,
    /// `fromJSON(text)`.
    FromJson,
    /// `hashFiles(globs...)`: the shell's digest of the matched files.
    HashFiles,
    /// `case(pred, value, ..., default)`.
    Case,
    /// `success()`.
    Success,
    /// `always()`.
    Always,
    /// `cancelled()`.
    Cancelled,
    /// `failure()`.
    Failure,
}

/// A binary operator.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BinOp {
    /// `&&`, returning the left value when it is falsy, else the right.
    And,
    /// `||`, returning the left value when it is truthy, else the right.
    Or,
    /// `==`.
    Eq,
    /// `!=`.
    Neq,
    /// `<`.
    Lt,
    /// `<=`.
    Le,
    /// `>`.
    Gt,
    /// `>=`.
    Ge,
}

/// One access after a path's head.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Seg {
    /// `.name`, looked up case-insensitively.
    Prop(String),
    /// `[expr]`.
    Index(Box<Ast>),
    /// `.*` or `[*]`: the object filter.
    Star,
}

/// What a path starts from.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Head {
    /// A context, by its lowercase name.
    Context(String),
    /// A call or a parenthesized expression.
    Expr(Box<Ast>),
}

/// An expression's tree.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Ast {
    /// A value.
    Lit(Value),
    /// A context read, or accesses into an expression's value.
    Path {
        /// What it starts from.
        head: Head,
        /// Its accesses, in order.
        segs: Vec<Seg>,
    },
    /// A function call.
    Call {
        /// The function.
        func: Func,
        /// Its arguments.
        args: Vec<Ast>,
    },
    /// A binary expression.
    Bin {
        /// The operator.
        op: BinOp,
        /// The left side.
        lhs: Box<Ast>,
        /// The right side.
        rhs: Box<Ast>,
    },
    /// `!expr`.
    Not(Box<Ast>),
}

action-policy

Source: crates/lvcs-action/src/policy.rs.

/// A counted set of attestations of one frontier.
///
/// They all name `frontier`, `toolchain_hash`, and `actions`, and come from
/// the distinct runner keys `runners`, at most one per runner key
/// and one per device. `runners.len()` is the set's size, which
/// `channel.read` reports.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Counted {
    /// The frontier every attestation names.
    pub frontier: FrontierId,
    /// The toolchain hash every attestation names.
    pub toolchain_hash: [u8; 32],
    /// The actions, with their inputs, every attestation names.
    pub actions: BTreeMap<ActionId, InputsHash>,
    /// The runner keys whose attestations count.
    pub runners: BTreeSet<ParticipantKey>,
}

/// The largest counted set among `attestations` (of one frontier, from one
/// channel), when it reaches `threshold`.
///
/// It holds at most one attestation per runner key and at most one per
/// device, all naming the same frontier id, `toolchain_hash`, and `actions`
/// map, and that map holding every action in `required` (every change
/// action but the generator at `lvcs.toml`'s approved heads, decisions 127
/// and 245). An
/// attestation whose frontier holds an override or workspace document
/// counts toward nothing. `None` when no set reaches `threshold`. Equal
/// attestations count once, so concurrent puts of one entry never count
/// twice. Among sets of the largest size, the one whose frontier id, then
/// `toolchain_hash`, then `actions` sorts first is chosen; its `runners`
/// are a maximum matching of runner keys to devices, found in key
/// order, so every peer reports the same set for the same attestations.
#[must_use]
pub fn counted(
    attestations: &[Attestation],
    required: &BTreeSet<ActionId>,
    threshold: u32,
) -> Option<Counted> {
    largest(attestations, required).filter(|set| reaches(set, threshold))
}

/// What one derivation's records for one `InputsHash` come to (row 180).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum GenerationState {
    /// At least the threshold of runners' records agree on every
    /// output digest, and no record differs or failed.
    Counted,
    /// Two records name different output digests: never counts.
    Diverged,
    /// A record failed: never counts.
    Failed,
}

/// The state of one `(derive, inputs)`'s records, and the record that
/// stands for it.
///
/// For `Counted`, `record` is the least runner key's record (the
/// records all agree on every output digest; their heads may differ). For
/// `Failed`, it is the least runner key's failed record. For
/// `Diverged`, it is the least runner key's record among those whose
/// digests differ from the least runner key's record.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GenerationCount<'a> {
    /// What the records come to.
    pub state: GenerationState,
    /// The record that stands for it.
    pub record: &'a Generation,
}

/// What `records`, every record of one derivation for one `InputsHash`,
/// come to under `threshold` (row 180, decision 120's rule).
///
/// A failed record makes them `Failed`, and two records whose output
/// digests differ make them `Diverged`; neither ever counts, so a later
/// contradicting record un-counts them. Otherwise they are `Counted` when
/// a maximum matching of their runner keys to their devices (at most
/// one record per key and one per device, as [`counted`] has it) reaches
/// `threshold`. `None` when there is no record, or the agreeing records
/// don't reach `threshold`. Records are ordered by runner key, then
/// device, then signature, so every peer picks the same record for the
/// same records.
#[must_use]
pub fn generation(records: &[Generation], threshold: u32) -> Option<GenerationCount<'_>> {
    let mut ordered: Vec<&Generation> = records.iter().collect();
    ordered.sort_by_key(|record| record_order(record));
    let first = *ordered.first()?;
    if let Some(failed) = ordered.iter().find(|record| record.failure.is_some()) {
        return Some(GenerationCount {
            state: GenerationState::Failed,
            record: failed,
        });
    }
    let agreed = digests(first);
    if let Some(differing) = ordered.iter().find(|record| digests(record) != agreed) {
        return Some(GenerationCount {
            state: GenerationState::Diverged,
            record: differing,
        });
    }
    let pairs: BTreeSet<(ParticipantKey, DeviceKey)> = ordered
        .iter()
        .map(|record| (record.runner, record.device))
        .collect();
    let runners = u32::try_from(matching(&pairs).len()).unwrap_or(u32::MAX);
    (runners >= threshold).then_some(GenerationCount {
        state: GenerationState::Counted,
        record: first,
    })
}

run

Source: crates/lvcs-action/src/run.rs.

/// The executor starts a run of `action`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Start {
    /// The action.
    pub action: Action,
    /// What the shell supplies its expressions.
    pub runtime: Box<Runtime>,
}

/// What a run's expressions read that the shell supplies (decision 245,
/// stage C): `github.sha`, the revision's frontier id; `runner.arch`;
/// and each `hashFiles` call's digest, by its globs, over the files of
/// `/work`.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Runtime {
    /// `github.sha`.
    pub sha: String,
    /// `runner.arch`: `X64` or `ARM64`.
    pub arch: String,
    /// Each `hashFiles` call's digest, by its globs.
    pub hash_files: BTreeMap<Vec<String>, String>,
}

/// Launch `launch` ended.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ended {
    /// The launch, as `Launch` named it.
    pub launch: u64,
    /// Its exit status: 128 plus the signal for one a signal ended, 125
    /// for a spec the sandbox refused.
    pub exit: i32,
    /// Whether the sandbox killed it at its timeout.
    pub timed_out: bool,
    /// How long it ran, in milliseconds, by the shell's clock.
    pub elapsed_ms: u64,
    /// What its step files held.
    pub files: FilesRead,
}

/// The run machine's events, all journaled.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RunEvent {
    /// See [`Start`].
    Start(Start),
    /// See [`Ended`].
    Ended(Ended),
}

/// Which phase a launch is in.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Phase {
    /// A node step's `pre`.
    Pre,
    /// A step's main run.
    Main,
    /// A node step's `post`.
    Post,
}

/// What a launch runs, the shell building its argument list.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Program {
    /// An argument list, with no shell.
    Exec {
        /// The argument list.
        argv: Vec<String>,
    },
    /// A script with every hole rendered (a script holds no secret),
    /// which the shell writes to a file and runs with `shell`.
    Script {
        /// Its shell.
        shell: Shell,
        /// Its text.
        text: String,
    },
    /// `node <file>`.
    Node {
        /// The program's path in the sandbox.
        file: String,
    },
}

/// Run one step through `lvcs sandbox` (decision 245).
///
/// `env` is set after the sandbox's fixed variables, each value rendered
/// but for its secret holes, which the shell fills; `path` goes before the
/// sandbox's fixed `PATH`; `cwd` is relative to `/work`; and `timeout_s` is
/// the step's wall clock. The shell makes the step files empty before the
/// launch and reads them after it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Launch {
    /// This launch, which `Ended` names.
    pub launch: u64,
    /// The step's place in the action, from 0.
    pub step: usize,
    /// The phase.
    pub phase: Phase,
    /// What kind of step it is, for the `action.step` span.
    pub kind: StepKindValue,
    /// What it runs.
    pub program: Program,
    /// Its environment.
    pub env: Vec<(String, Template)>,
    /// Its `PATH` entries before the fixed ones.
    pub path: Vec<String>,
    /// Its working directory, relative to `/work`.
    pub cwd: Option<String>,
    /// Its wall clock, in seconds.
    pub timeout_s: u64,
}

/// How the run ended.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Done {
    /// Whether it passed: no step failed the run, and it wasn't cancelled.
    pub passed: bool,
    /// Whether it ran out of time: the step that failed it was killed at
    /// its timeout, or the run was cancelled with steps left.
    pub timed_out: bool,
    /// The step that failed it, from 0, and its phase.
    pub failed: Option<(usize, Phase)>,
}

/// What the run machine asks of the executor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunOutput {
    /// Run one step.
    Launch(Launch),
    /// The run ended.
    Done(Done),
}

runner-actor

Source: crates/lvcs-action/src/runner/mod.rs.

/// A `ci` job's id: `HASH([frontier id])`, the frontier it prepares.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct JobId(#[serde(with = "lvcs_doc::cbor::fixed")] pub [u8; 32]);

/// The runner actor's events, all journaled. None carries a credential
/// value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RunnerEvent {
    /// First, and again at every caps render and every move of
    /// `lvcs.toml`'s approved heads: what its participant's grants ask it
    /// to serve.
    Config(RunnerConfig),
    /// The sandbox probe the runner asked for (`RunnerOutput::Probe`)
    /// ended.
    Probed(Probed),
    /// `ci`: main's visible frontier moved: sent before the `Merged` a
    /// merge causes, since a trigger opens a job on the visible frontier as
    /// last sent, and when a render of one of main's `file` documents moved
    /// it. Each starts a quiet period.
    Visible(Visible),
    /// `ci`: a bundle merged into main.
    Merged(Merged),
    /// `ci`: a [`CiOutput::Wake`] fired.
    Tick(Tick),
    /// `ci`: the executor prepared a job: each change action's inputs.
    Prepared(Prepared),
    /// `ci`: the executor ran one change action of a job.
    Ran(Ran),
    /// `ci`: the executor ran a job's generator (answering
    /// [`CiOutput::Derive`]).
    Derived(Derived),
    /// `ci`: a generation counted (row 180): the shell forwards each
    /// counted generation the channels actor names for main's visible
    /// frontier (`ChannelsOutput::Generated` with `Counted`), and the
    /// current one again after every `Config`.
    Generated(GeneratedSeen),
    /// `ci`: the shell couldn't run job `job`'s generator or record its
    /// generation (no grant, a quota reached, an executor error): the
    /// inputs count as never run, and the job ends unattested.
    Dropped(GenerationDropped),
    /// Promote: a promote channel's pending run changed
    /// (`ChannelsOutput::Candidates`).
    Candidates(Candidates),
    /// Promote: a promote channel newly holds an outcome of each of some
    /// runs (`ChannelsOutput::Outcomes`).
    Outcomes(Outcomes),
    /// Promote: a runner's presence on a promote channel's document topic
    /// arrived.
    Presence(Presence),
    /// Promote: the shell's tick, every `PROMOTE_PRESENCE_SECS`.
    PresenceTick(PresenceTick),
    /// Promote: the executor staged a run.
    Staged(Staged),
    /// Promote: the executor ran a run.
    Finished(Finished),
    /// Promote: a run's build trees are gone, and the executor is free of
    /// it.
    Released(Released),
}

/// What its participant's grants ask a runner to serve, and that
/// participant (decisions 206 and 245).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunnerConfig {
    /// The participant this actor runs as.
    pub me: ParticipantKey,
    /// What its grants ask for.
    pub wants: Wants,
    /// Every toolchain an action at `lvcs.toml`'s approved heads names
    /// (decision 245), which the sandbox probe covers besides `rust`.
    #[serde(with = "crate::config::toolchains")]
    pub toolchains: BTreeSet<Toolchain>,
}

/// What the participant's live grants, each naming a channel with
/// `Attest(<channel>)` exactly, ask a runner to serve, given the actions at
/// `lvcs.toml`'s approved heads: a pure function of both, computed by the
/// shell.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Wants {
    /// No grant names a channel there is an action for (the owner's
    /// capabilities need no grant, so it is always here).
    Nothing,
    /// Grants attest `ci` and the promote channel `channel`: one key never
    /// attests both, so the daemon must stop.
    Conflict {
        /// The promote channel.
        channel: ChannelName,
    },
    /// A grant attests `ci`.
    Ci(CiWants),
    /// Grants attest these promote channels.
    Promote(PromoteConfig),
}

/// What a `ci` runner runs, before the sandbox probe gives its toolchain
/// hash: the ids of the change actions a `ci` attestation names, in
/// `lvcs.toml`'s order at its approved heads (decision 127), the quiet
/// period `[runner]` resolves to, and the generator's id.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CiWants {
    /// The change actions' ids.
    pub checks: Vec<ActionId>,
    /// The quiet period, in milliseconds.
    pub quiet_ms: u64,
    /// The generator's id, when an action has `writes`.
    pub generator: Option<ActionId>,
}

/// How the sandbox probe ended.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Probed {
    /// The sandbox runs here, with this toolchain hash.
    Ready(#[serde(with = "lvcs_doc::cbor::fixed")] [u8; 32]),
    /// It can't run here, and why.
    Unavailable(String),
}

/// A `ci` runner's configuration, fixed for its incarnation: its
/// [`CiWants`] when it started serving `ci`, with the toolchain hash the
/// sandbox's toolchain probes gave (decision 245).
///
/// `checks` is the id of every change action a `ci` attestation names,
/// `quiet_ms` how long main's visible frontier must be unchanged before a
/// job opens on it, and `generator` the id of the action with `writes`,
/// which the shell names in every generation record; the runner derives
/// only with one.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CiConfig {
    /// The change actions' ids.
    pub checks: Vec<ActionId>,
    /// The toolchain hash.
    #[serde(with = "lvcs_doc::cbor::fixed")]
    pub toolchain_hash: [u8; 32],
    /// The quiet period, in milliseconds.
    pub quiet_ms: u64,
    /// The generator's id, when an action has `writes`.
    pub generator: Option<ActionId>,
}

/// A promote runner's configuration: each promote channel its
/// participant's live grants attest, with the id of the promote action
/// that moves it at `lvcs.toml`'s approved heads.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromoteConfig {
    /// Each served channel's promote action.
    pub served: BTreeMap<ChannelName, ActionId>,
}

/// Main's visible frontier as a delta since the last (the first is full).
///
/// `changed` holds the documents whose heads changed, with their heads,
/// and `removed` the documents that left it; the shell sent it at its Unix
/// time `now_ms`, in milliseconds.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Visible {
    /// The documents whose heads changed, with their heads.
    #[serde(with = "lvcs_doc::cbor::head_map")]
    pub changed: BTreeMap<String, Vec<ChangeHash>>,
    /// The documents that left it.
    pub removed: Vec<String>,
    /// When the shell sent it.
    pub now_ms: i64,
}

/// Bundle `bundle` merged into main.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Merged {
    /// The bundle that merged.
    pub bundle: BundleId,
}

/// A `Wake` fired, at the shell's Unix time `now_ms`, in milliseconds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tick {
    /// When the shell sent it.
    pub now_ms: i64,
}

/// Job `job` is materialized, with each change action's `InputsHash`; an
/// action left out couldn't be prepared, and ends the job unattested.
///
/// With a generator, `generator` is its `InputsHash` at the job's frontier
/// (row 180), and each action's inputs were computed over the counted
/// generation's bytes when one exists for it; a job prepared without it
/// ends unattested.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Prepared {
    /// The job.
    pub job: JobId,
    /// Each prepared change action's `InputsHash`.
    pub inputs: BTreeMap<ActionId, InputsHash>,
    /// The generator's `InputsHash`, with a generator.
    pub generator: Option<InputsHash>,
}

/// Change action `action` of job `job` ran, and how it ended.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ran {
    /// The job.
    pub job: JobId,
    /// The action that ran.
    pub action: ActionId,
    /// How it ended.
    pub outcome: RunOutcome,
}

/// How a change action's run ended (decision 230).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunOutcome {
    /// It exited 0 within its timeout.
    Passed,
    /// It exited non-zero within its timeout, or couldn't run.
    Failed,
    /// The sandbox killed it at its timeout (exit status 124): not a
    /// failure, never reusable, and the job runs its other actions.
    TimedOut,
}

/// What a generator run came to (row 180): each output address's SHA-256
/// digest, or why it failed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeriveOutcome {
    /// The run's derived outputs, each address with its bytes' digest.
    Outputs(#[serde(with = "digest_map")] BTreeMap<String, [u8; 32]>),
    /// The run failed.
    Failed(DeriveFailure),
}

/// Job `job`'s generator ran on the inputs `inputs`, with `outcome`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Derived {
    /// The job.
    pub job: JobId,
    /// The generator's `InputsHash`.
    pub inputs: InputsHash,
    /// What the run came to.
    pub outcome: DeriveOutcome,
}

/// Job `job`'s generator run on `inputs` was not recorded.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenerationDropped {
    /// The job.
    pub job: JobId,
    /// The generator's `InputsHash`.
    pub inputs: InputsHash,
}

/// The generator's inputs `inputs` have a counted generation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedSeen {
    /// The counted generation's `InputsHash`.
    pub inputs: InputsHash,
}

/// Promote channel `channel`'s pending run is now `run`, or none.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Candidates {
    /// The channel.
    pub channel: ChannelName,
    /// Its pending run.
    pub run: Option<PendingRun>,
}

/// Promote channel `channel` newly holds an outcome, passed or failed, of
/// each run in `runs`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Outcomes {
    /// The channel.
    pub channel: ChannelName,
    /// The runs.
    pub runs: Vec<RunKey>,
}

/// Runner `agent`'s presence on channel `channel` arrived.
///
/// It came on promote channel `channel`'s document topic, at the shell's
/// Unix time `now_ms`, in milliseconds: its arrival time, never the
/// sender's `ts` (decision 142). The shell sends only a presence from a
/// holder of `Attest(channel)`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Presence {
    /// The runner.
    pub agent: ParticipantKey,
    /// The channel.
    pub channel: ChannelName,
    /// When it arrived.
    pub now_ms: i64,
}

/// The shell's presence tick, at its Unix time `now_ms`, in milliseconds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct PresenceTick {
    /// When the shell sent it.
    pub now_ms: i64,
}

/// Run `run` is staged: its directory is materialized.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Staged {
    /// The run.
    pub run: RunKey,
}

/// Run `run` ended: `passed` when its command exited 0 in time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Finished {
    /// The run.
    pub run: RunKey,
    /// Whether it passed.
    pub passed: bool,
}

/// Run `run`'s build trees are gone, and it holds the executor no more.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Released {
    /// The run.
    pub run: RunKey,
}

/// What the runner asks the shell to do: its own outputs, then each role's.
/// None carries a credential value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RunnerOutput {
    /// Probe the sandbox once, on a blocking thread (answered by
    /// `Probed`), for `rust` and these toolchains.
    Probe {
        /// The toolchains besides `rust`.
        #[serde(with = "crate::config::toolchains")]
        toolchains: BTreeSet<Toolchain>,
    },
    /// The participant's grants attest `ci` and the promote channel
    /// `channel`, at once or one after the other within this incarnation:
    /// the daemon must stop (decisions 206 and 245).
    Refuse {
        /// The promote channel.
        channel: ChannelName,
    },
    /// What `status` names of this runner moved.
    Serving(Serving),
    /// The runner serves `ci` from now on, with this configuration: the
    /// shell starts its `ci` side. A runner that served `ci` before in
    /// this incarnation names the configuration it first served it with.
    StartCi(CiConfig),
    /// The runner serves `ci` no more: its participant's grants no longer
    /// attest it (decision 249). The shell drops its `ci` side: a job step
    /// under way is discarded when it returns, never told to the runner
    /// or attested, and its build trees are removed then.
    StopCi,
    /// The runner serves these promote channels from now on: the shell's
    /// promote side follows.
    ServePromote(PromoteConfig),
    /// An output of a `ci` runner.
    Ci(CiOutput),
    /// An output of a promote runner.
    Promote(PromoteOutput),
}

/// What a runner serves, as `status` names it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Serving {
    /// No grant names a channel there is an action for.
    Nothing,
    /// These channels, `ci` alone or promote channels.
    Channels(Vec<ChannelName>),
    /// These channels, once the sandbox probe answers.
    Probing(Vec<ChannelName>),
    /// These channels, which it runs nothing for: the sandbox can't run
    /// here, for this reason.
    Unavailable(Vec<ChannelName>, String),
    /// Nothing: its participant's grants no longer attest these channels
    /// it served (decision 249).
    Revoked(Vec<ChannelName>),
}

/// What a `ci` runner asks the shell to do.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CiOutput {
    /// Materialize `frontier` as job `job` and compute the inputs of
    /// `actions` (answered by `Prepared`).
    Prepare {
        /// The job.
        job: JobId,
        /// The frontier to materialize.
        frontier: Frontier,
        /// The change actions whose inputs to compute.
        actions: BTreeSet<ActionId>,
    },
    /// Run change action `action` of job `job` on `frontier`, whose
    /// prepared `InputsHash` is `inputs` (answered by `Ran`).
    Run {
        /// The job.
        job: JobId,
        /// The job's frontier.
        frontier: FrontierId,
        /// The action to run.
        action: ActionId,
        /// The action's prepared `InputsHash`.
        inputs: InputsHash,
    },
    /// Change action `action` of job `job` is reused, not run: its
    /// prepared `InputsHash` equals this runner's own last pass of it. The
    /// shell records its `check` span with `outcome` `reused` (decision
    /// 134). Emitted once per reused action, in `ActionId` order, when the
    /// job is prepared, so before any of the job's `Run`s and its `Attest`.
    Reused {
        /// The job.
        job: JobId,
        /// The action reused.
        action: ActionId,
    },
    /// Every change action passed on `frontier`, or was reused: the shell
    /// signs an attestation under the runner's key and device and puts it
    /// in `ci`.
    Attest {
        /// The frontier attested.
        frontier: Frontier,
        /// Every change action's prepared `InputsHash`.
        actions: BTreeMap<ActionId, InputsHash>,
        /// The runner's toolchain hash.
        #[serde(with = "lvcs_doc::cbor::fixed")]
        toolchain_hash: [u8; 32],
    },
    /// Job `job` ended unattested: an action of it failed, a `Prepared`
    /// left an action out, or its last action ran after one of its actions
    /// timed out. The shell gives the executor to the next job and removes
    /// the job's build trees.
    Ended {
        /// The job.
        job: JobId,
    },
    /// Send a `Tick` at or after the shell's Unix time `at_ms`, in
    /// milliseconds (`CoordOutput::Wake`'s pattern): the end of the quiet
    /// period the last `Visible` began.
    Wake {
        /// The end of the quiet period.
        at_ms: i64,
    },
    /// Run job `job`'s generator on `frontier` through the change executor
    /// and sandbox (answered by `Derived`): its inputs have no counted
    /// generation, and this runner never ran them.
    Derive {
        /// The job.
        job: JobId,
        /// The frontier to derive from.
        frontier: Frontier,
    },
    /// Record job `job`'s generator run on `inputs` at `frontier`: write
    /// its outputs (whole-file edits flagged `Mark`, under the runner's
    /// `write` grant) and sign its generation record, or sign the failure.
    Generate {
        /// The job.
        job: JobId,
        /// The generator's `InputsHash`.
        inputs: InputsHash,
        /// The frontier it ran on.
        frontier: Frontier,
        /// What the run came to.
        outcome: DeriveOutcome,
    },
}

/// What a promote runner asks the shell to do. None carries a credential
/// value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PromoteOutput {
    /// The channels served moved: the shell publishes this runner's
    /// presence on each one's document topic every `PROMOTE_PRESENCE_SECS`.
    Announce {
        /// The channels served, sorted.
        channels: Vec<ChannelName>,
    },
    /// Materialize `frontier`'s files under promote action `action`'s
    /// `paths` as run `run` (answered by `Staged`).
    Stage {
        /// The run.
        run: RunKey,
        /// The frontier it promotes.
        frontier: FrontierId,
        /// The promote action.
        action: ActionId,
    },
    /// Run `run` was cancelled while under way, since its channel's grant
    /// ended (decision 249), and the executor's answer for it (`Staged` or
    /// `Finished`) is back: the shell ends it, recording no outcome, and
    /// answers `Released` once the executor is free of it.
    Cancelled {
        /// The run.
        run: RunKey,
    },
    /// Run promote action `action`'s command as run `run` (answered by
    /// `Finished`).
    Run {
        /// The run.
        run: RunKey,
        /// The promote action.
        action: ActionId,
    },
    /// Run `run` of promote action `action`, of `frontier`, ended: the
    /// shell signs a `PromoteOutcome` with the runner and device keys and
    /// puts it in the action's channel.
    Outcome {
        /// The run.
        run: RunKey,
        /// The frontier it promoted.
        frontier: FrontierId,
        /// The promote action.
        action: ActionId,
        /// Whether it passed.
        passed: bool,
    },
}

step

Source: crates/lvcs-action/src/step.rs.

/// A step's id (decision 248): `[A-Za-z_][A-Za-z0-9_-]*`, unique within its
/// action; `${{ steps.<id>.outputs.<name> }}` names a step by it.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct StepId(String);

/// A status function (decision 245): what a step's `if` asks of the run
/// so far.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Status {
    /// No earlier step failed and the run was not cancelled: the default.
    Success,
    /// Always, unless the run was cancelled.
    Always,
    /// An earlier step failed.
    Failure,
    /// The run was cancelled.
    Cancelled,
}

/// An expression (decisions 245 and 248): a status function, an earlier
/// step's output, or any other GitHub expression, folded at mapping time.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Expr {
    /// `success()`, `always()`, `failure()`, or `cancelled()`.
    Status(Status),
    /// `steps.<step>.outputs.<name>`: what the step wrote to
    /// `GITHUB_OUTPUT` under `name`, empty when it wrote none.
    StepOutput {
        /// The step.
        step: StepId,
        /// The output's name.
        name: String,
    },
    /// Any other expression (stage C): what is left of it once what the
    /// action fixes is folded in. As an `if` that calls no status function
    /// it is `success() && (<expr>)` already.
    Eval(Ast),
}

/// A typed hole of a [`Template`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Hole {
    /// An expression, which the `run` machine renders.
    Expr(Expr),
    /// A secret by name (`secrets.<NAME>`), which only the shell fills,
    /// from the runner daemon's process environment, as it builds the
    /// sandbox spec.
    Secret(String),
}

/// A piece of a [`Template`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Part {
    /// Literal text.
    Text(String),
    /// A hole.
    Hole(Hole),
}

/// Literal text with typed holes, each written `${{ <expression> }}`
/// (decision 245).
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Template(Vec<Part>);

/// The program a script step runs its file with (decision 245's shell
/// templates on Linux, `{0}` being the script's path).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
    /// None given: `bash -e {0}`, or `sh -e {0}` when the sandbox has no
    /// `bash`.
    Default,
    /// `bash --noprofile --norc -eo pipefail {0}`.
    Bash,
    /// `sh -e {0}`.
    Sh,
    /// `python {0}`.
    Python,
    /// A custom argument list holding `{0}`.
    Custom {
        /// The argument list.
        argv: Vec<String>,
    },
}

/// A node step (decision 245): `node <tree>/<main>` with its inputs as
/// `INPUT_<NAME>`, after `pre` in the run's pre phase and before `post` in
/// its post phase. In stage B2 `tree` is a directory of the action's own
/// inputs, relative to `/work`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct NodeStep {
    /// The program's directory, relative to `/work`.
    pub tree: String,
    /// The main program, relative to `tree`.
    pub main: String,
    /// The program run in the pre phase, relative to `tree`.
    pub pre: Option<String>,
    /// The program run in the post phase, relative to `tree`.
    pub post: Option<String>,
    /// Its inputs, by name.
    pub inputs: BTreeMap<String, Template>,
}

/// A built-in action a workflow step's `uses:` names (decision 245, stage
/// C), matched by `owner/repo` at any ref; it needs no vendored tree.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Builtin {
    /// `actions/checkout`: nothing, since `/work` holds the revision
    /// already (it has no `.git`).
    Checkout,
    /// `actions/cache` (and its `restore` and `save`): nothing, its
    /// `cache-hit` output `false`; reuse is LVCS's cache.
    Cache,
    /// `actions/setup-node`: the step fails unless the node on the fixed
    /// `PATH`, which the `node` toolchain probe ran, has this major
    /// version; with none asked for, nothing.
    SetupNode {
        /// The major version `node-version` asks for.
        major: Option<u32>,
    },
    /// `actions/upload-artifact`: nothing; the files stay in the run.
    UploadArtifact,
}

impl Builtin {
    /// The action it stands for, as `uses:` names it.
    #[must_use]
    pub const fn uses(&self) -> &'static str {
        match self {
            Self::Checkout => "actions/checkout",
            Self::Cache => "actions/cache",
            Self::SetupNode { .. } => "actions/setup-node",
            Self::UploadArtifact => "actions/upload-artifact",
        }
    }
}

/// What a step runs.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StepKind {
    /// An argument list, with no shell.
    Exec {
        /// The argument list.
        argv: Vec<String>,
    },
    /// A script, written to a file and run by `shell`.
    Script {
        /// Its shell.
        shell: Shell,
        /// Its text.
        text: Template,
    },
    /// A node program.
    Node(NodeStep),
    /// A built-in action.
    Builtin(Builtin),
}

/// One step of an action (decision 245).
///
/// `cond` is its `if`, `success()` when unset; `env` is set after the
/// action's own `env` and the `GITHUB_ENV` of earlier steps;
/// `working_directory` is relative to `/work`; a step whose
/// `continue_on_error` is set fails without failing the run; and
/// `timeout_secs` can only lower what is left of the run's limit.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Step {
    /// Its id.
    pub id: Option<StepId>,
    /// Its `if`.
    pub cond: Option<Expr>,
    /// Its environment.
    pub env: BTreeMap<String, Template>,
    /// Its working directory, relative to `/work`.
    pub working_directory: Option<String>,
    /// Whether its failure leaves the run passing.
    pub continue_on_error: bool,
    /// Its own limit, in seconds.
    pub timeout_secs: Option<u32>,
    /// What it runs.
    pub kind: StepKind,
}

/// What one step wrote to its step files (decision 245), parsed: each
/// `GITHUB_ENV` variable and `GITHUB_OUTPUT` and `GITHUB_STATE` value in
/// the order written (a later one of a name wins), and each `GITHUB_PATH`
/// entry in the order written.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StepFiles {
    /// `GITHUB_ENV`.
    pub env: Vec<(String, String)>,
    /// `GITHUB_PATH`.
    pub path: Vec<String>,
    /// `GITHUB_OUTPUT`.
    pub output: Vec<(String, String)>,
    /// `GITHUB_STATE`.
    pub state: Vec<(String, String)>,
}

/// How the shell read a step's step files.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilesRead {
    /// What they hold.
    Read(StepFiles),
    /// Not handed to the machine: a promote action's run, whose step files
    /// a secret could have reached, is never journaled with them.
    Withheld,
    /// A file is malformed, past `STEP_FILE_BYTES`, or not a regular file,
    /// which fails the step.
    Bad(String),
}

/// The directory of the step files, under a run directory's `tmp/`, which
/// the sandbox sees at `/tmp`.
pub const STEP_FILES_DIR: &str = "lvcs-step";

/// Each step file's name in [`STEP_FILES_DIR`], with the variable naming
/// its path in the sandbox.
pub const STEP_FILES: [(&str, &str); 5] = [
    ("GITHUB_ENV", "env"),
    ("GITHUB_PATH", "path"),
    ("GITHUB_OUTPUT", "output"),
    ("GITHUB_STATE", "state"),
    ("GITHUB_STEP_SUMMARY", "summary"),
];

workflows

Source: crates/lvcs-action/src/workflow.rs.

/// `lvcs.toml`'s `[workflows]` (decision 245): `skip`, the action names
/// (`<file stem>/<job id>`, every cell of the job, or one cell's full
/// name) left out deliberately; `inputs`, an action's narrowed inputs by
/// name (`**`, every synced file, when unset); `vars`, the `vars` context;
/// and `repository`, `owner/name` for `github.repository`. Any other key
/// is refused at parse.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct WorkflowsTable {
    /// Action names left out.
    pub skip: Vec<String>,
    /// Narrowed inputs, by action name.
    pub inputs: BTreeMap<String, InputsEntry>,
    /// The `vars` context.
    pub vars: BTreeMap<String, String>,
    /// `github.repository`.
    pub repository: Option<String>,
}

/// One `[workflows] inputs."<name>"` entry: gitignore-syntax patterns.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InputsEntry {
    /// The paths the action covers.
    pub paths: Vec<String>,
    /// The paths it reads besides.
    #[serde(default)]
    pub uses: Vec<String>,
}

/// What one workflow file comes to: its actions, the jobs or cells it
/// skipped, why the whole file is ignored when it is, and the jobs it
/// refused.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FileMapped {
    /// The file's path from the root.
    pub file: String,
    /// The canonical actions, in job and cell order.
    pub actions: Vec<Action>,
    /// What it skipped: a name and why.
    pub skipped: Vec<(String, String)>,
    /// Why no job of it counts, when none does.
    pub ignored: Option<String>,
    /// What it refused.
    pub refused: Vec<Refusal>,
}

/// One refused job, cell, or file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
    /// The action name, or the file's path when the whole file is
    /// refused.
    pub name: String,
    /// The bounded reason.
    pub reason: ActionRefusal,
    /// What was refused, in words.
    pub detail: String,
    /// What to do: the exact `lvcs.toml` line, or the fix.
    pub next: String,
}

spawn

Source: crates/lvcs-actor/src/driver.rs.

/// Spawn `machine` on the current tokio runtime.
///
/// Its mailbox holds `capacity` events (at least 1). Every handled event is
/// recorded in `journal` as `id` before any of its effects is applied;
/// emitted outputs then go to `sink` in order.
pub fn spawn<M, K, J>(
    machine: M,
    id: ActorId,
    capacity: usize,
    sink: K,
    journal: J,
) -> (
    ActorHandle<M::Event>,
    JoinHandle<DriverReport<SnapshotOf<M>>>,
)
where
    M: Machine,
    K: EffectSink<Output<M>>,
    J: JournalSink<SnapshotOf<M>, M::Event>,

inline

Source: crates/lvcs-actor/src/inline.rs.

/// A machine run inside its owner's task (the daemon shell, row 211).
///
/// [`step`](Self::step) journals each event as `id` after the machine
/// processed it and before the owner applies any returned effect, in the
/// driver's format, so replay treats an inline machine like a spawned one.
/// The owner applies the effects in emit order in the same turn.
#[derive(Debug)]
pub struct Inline<M, J> {
    /// The machine.
    machine: M,
    /// Its journal identity.
    id: ActorId,
    /// Where its entries go.
    journal: J,
    /// The `actor` span, with this machine's `actor_kind`.
    span: Span,
}

impl<M, J> Inline<M, J>
where
    M: Machine,
    J: JournalSink<SnapshotOf<M>, M::Event>,
{
    /// Run `machine` inline as `id`, journaling to `journal`.
    pub fn new(machine: M, id: ActorId, journal: J) -> Self {
        let span = tracing::info_span!("actor", actor_kind = <M::Context as Context>::KIND);
        Self {
            machine,
            id,
            journal,
            span,
        }
    }

    /// Run `ev` to completion, journal it stamped `local_ts` (the owner's
    /// wall clock, audit only: no guard or action reads it), and return the
    /// effects for the owner to apply in order.
    pub fn step(&mut self, ev: M::Event, local_ts: LocalTs) -> Vec<Effect<Output<M>>> {
        let _entered = self.span.enter();
        journaled_step(&mut self.machine, &self.id, &mut self.journal, ev, local_ts)
    }

    /// The machine, for reads between events.
    pub const fn machine(&self) -> &M {
        &self.machine
    }

    /// The machine's journal identity.
    pub const fn id(&self) -> &ActorId {
        &self.id
    }
}

actor-id

Source: crates/lvcs-actor/src/journal.rs.

/// Identifies one actor incarnation in the local journal (decision 63).
///
/// Journal-only: the journal never leaves the peer, so `key` may be a
/// document path. Never use it as a telemetry attribute.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ActorId {
    /// The machine's `Context::KIND`, an `actor_kind` value.
    pub kind: &'static str,
    /// Instance key within the kind: the document address for `document`,
    /// the workspace id for `mirror` and `workspace`, and `""` for the
    /// single `router`.
    pub key: String,
    /// Starts at 0 and increases each time the shell spawns this
    /// `(kind, key)` again. Replay builds a fresh actor per incarnation: a
    /// document from its saves at the first entry's `state_before` heads; a
    /// router or mirror by replaying every earlier incarnation's entries.
    pub incarnation: u64,
}

incarnations

Source: crates/lvcs-actor/src/journal.rs.

/// Mints the [`ActorId`] for each spawn, so every spawn of a `(kind, key)`
/// is a new incarnation, across daemon restarts too (design §6).
///
/// At startup the shell passes the `ActorId` of every journal entry to
/// [`observe`](Self::observe); after that, [`next`](Self::next) mints the id
/// for each spawn. An incarnation that handled no events leaves no journal
/// entry, so its number may be minted again after a restart; no entry
/// refers to it.
#[derive(Debug, Clone, Default)]
pub struct Incarnations {
    /// The next incarnation to mint, by kind and then key.
    next: BTreeMap<&'static str, BTreeMap<String, u64>>,
}

impl Incarnations {
    /// The journal holds an entry for `id`: every id minted later for its
    /// `(kind, key)` has a higher incarnation.
    pub fn observe(&mut self, id: &ActorId) {
        let next = self.slot(id.kind, &id.key);
        *next = (*next).max(id.incarnation.saturating_add(1));
    }

    /// The id to run `M` at `key` under: incarnation 0 the first time,
    /// then one more than the newest minted or observed.
    pub fn next<M: Machine>(&mut self, key: &str) -> ActorId {
        let kind = <M::Context as Context>::KIND;
        let next = self.slot(kind, key);
        let incarnation = *next;
        *next = incarnation.saturating_add(1);
        ActorId {
            kind,
            key: key.to_owned(),
            incarnation,
        }
    }

    /// The next incarnation to mint for `(kind, key)`, inserted as 0 the
    /// first time the pair is seen.
    fn slot(&mut self, kind: &'static str, key: &str) -> &mut u64 {
        self.next
            .entry(kind)
            .or_default()
            .entry(key.to_owned())
            .or_insert(0)
    }
}

journal

Source: crates/lvcs-actor/src/journal.rs.

/// Milliseconds since the Unix epoch, read by the driver (the shell), never
/// by an action. Audit only: replay ignores it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LocalTs(pub u64);

/// One journal line: the event a machine handled and its state around it.
///
/// `S` is the machine's [`crate::Snapshot`]: its sml.rs label plus a
/// fingerprint of its durable context (decision 32), so comparing states
/// covers content too.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JournalEntry<S, E> {
    /// Which actor handled the event.
    pub actor: ActorId,
    /// The machine's snapshot before the event.
    pub state_before: S,
    /// The event, as delivered.
    pub event: E,
    /// The machine's snapshot after the event ran to completion.
    pub state_after: S,
    /// When the driver or the inline owner handled it.
    pub local_ts: LocalTs,
}

/// Receives journal entries from a driver, in handling order.
pub trait JournalSink<S, E>: Send + 'static {
    /// Record one entry.
    fn record(&mut self, entry: JournalEntry<S, E>);
}

/// A sink that discards every entry.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoJournal;

impl<S, E> JournalSink<S, E> for NoJournal {
    fn record(&mut self, _entry: JournalEntry<S, E>) {}
}

replay

Source: crates/lvcs-actor/src/journal.rs.

/// Where a replayed state first differed from the journal.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Divergence<S> {
    /// Zero-based index of the journal entry.
    pub index: u64,
    /// Whether `state_before` or `state_after` differed.
    pub phase: DivergencePhase,
    /// The state the journal recorded.
    pub expected: S,
    /// The state the replayed actor reported.
    pub actual: S,
}

/// Which side of an entry diverged.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DivergencePhase {
    /// The actor was not in the journaled state before the event.
    Before,
    /// Handling the event led to a different state.
    After,
}

/// Result of [`replay`].
#[derive(Debug)]
pub struct ReplayReport<S, O> {
    /// Entries replayed (including the diverging one, if any).
    pub replayed: u64,
    /// Effects the actor produced, recorded and never executed.
    pub effects: Vec<Effect<O>>,
    /// The first divergence; `None` means the actor is deterministic over
    /// this journal.
    pub divergence: Option<Divergence<S>>,
}

/// Replay `entries` into `machine`, stopping at the first divergence.
pub fn replay<M, I>(machine: &mut M, entries: I) -> ReplayReport<SnapshotOf<M>, Output<M>>
where
    M: Machine,
    I: IntoIterator<Item = JournalEntry<SnapshotOf<M>, M::Event>>,

rebuild

Source: crates/lvcs-actor/src/journal.rs.

/// Why [`rebuild`] returned no actor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RebuildError<S> {
    /// Entry `index` belongs to `actor`, not the actor being rebuilt.
    Foreign {
        /// Zero-based index of the entry.
        index: u64,
        /// The actor the entry belongs to.
        actor: ActorId,
    },
    /// Entry `index` names `incarnation`, older than the `newest` one
    /// before it.
    OutOfOrder {
        /// Zero-based index of the entry.
        index: u64,
        /// The incarnation the entry names.
        incarnation: u64,
        /// The newest incarnation an earlier entry named.
        newest: u64,
    },
    /// Replay differed from the journal, so the rebuilt context can't be
    /// trusted. A `Before` divergence on an incarnation's first entry means
    /// that incarnation started from an unfaithful rebuild.
    Diverged(Divergence<S>),
}

/// Restart a router or mirror machine from its own journal entries (design
/// §2 supervision, decision 56).
///
/// Feed `fresh`, a newly constructed machine, every entry for its
/// `(Context::KIND, key)`
/// from all its earlier incarnations, in journal order, and discard the
/// effects. Incarnations may skip numbers but never go back. Spawn the result
/// under the id [`Incarnations::next`] mints. Document actors restart from
/// their saves instead.
///
/// # Errors
///
/// [`RebuildError::Foreign`] when an entry belongs to another actor,
/// [`RebuildError::OutOfOrder`] when an entry's incarnation is older than an
/// earlier entry's, and [`RebuildError::Diverged`] when replay differs from
/// the journal.
pub fn rebuild<M, I>(fresh: M, key: &str, entries: I) -> Result<M, RebuildError<SnapshotOf<M>>>
where
    M: Machine,
    I: IntoIterator<Item = JournalEntry<SnapshotOf<M>, M::Event>>,

effect

Source: crates/lvcs-actor/src/lib.rs.

/// An effect requested by an actor while handling one event.
///
/// Actions never perform I/O directly; they return effects and the runtime
/// shell carries them out. During replay, effects are recorded but not
/// executed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Effect<O> {
    /// Hand this output to the driver's [`EffectSink`] (send, publish, write).
    Emit(O),
    /// Stop this actor after the current event has run to completion.
    Stop,
}

context

Source: crates/lvcs-actor/src/lib.rs.

/// What sml.rs lacks, on each machine's context (decision 222).
pub trait Context: Send + 'static {
    /// Bounded-cardinality actor kind: an `actor_kind` value, used as a
    /// telemetry attribute and as [`ActorId::kind`].
    const KIND: &'static str;
    /// Outputs handed to the effect sink.
    type Output: Send + 'static;
    /// A digest of the durable context (decision 32), journaled around
    /// each event; `PartialEq` so replay can detect divergence. A
    /// document's carries its heads too, which replay loads it at.
    type Fingerprint: Clone + Debug + PartialEq + Send + 'static;

    /// The fingerprint of the durable context now.
    fn fingerprint(&self) -> Self::Fingerprint;

    /// Take every effect the actions pushed since the last drain.
    fn drain(&mut self) -> Vec<Effect<Self::Output>>;
}

machine

Source: crates/lvcs-actor/src/lib.rs.

/// An actor: a generated sml.rs machine, with this impl emitted by
/// [`machine!`], or a hand-written enum of machines (the document actor).
pub trait Machine: Send + 'static {
    /// The hand-written event enum the mailbox carries and the journal
    /// records. Each maps to the machine's generated events by a pure
    /// `From`. `Clone` so the event can be journaled after it runs.
    type Event: Clone + Debug + Send + 'static;
    /// The sml.rs state label.
    type Label: Clone + Debug + PartialEq + sml::StateName + Send + 'static;
    /// The machine's context.
    type Context: Context;

    /// Run one event to completion; whether the table accepted it.
    fn process(&mut self, ev: Self::Event) -> bool;

    /// The current state label.
    fn label(&self) -> Self::Label;

    /// The machine's context.
    fn context(&self) -> &Self::Context;

    /// The machine's context, mutably.
    fn context_mut(&mut self) -> &mut Self::Context;
}

/// A machine's outputs.
pub type Output<M> = <<M as Machine>::Context as Context>::Output;

/// A machine's fingerprint.
pub type Fingerprint<M> = <<M as Machine>::Context as Context>::Fingerprint;

/// A machine's journaled state.
pub type SnapshotOf<M> = Snapshot<<M as Machine>::Label, Fingerprint<M>>;

snapshot

Source: crates/lvcs-actor/src/lib.rs.

/// The journaled state of a machine (decision 32): its sml.rs state label
/// and the fingerprint of its durable context, so comparing snapshots
/// covers content too.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot<L, F> {
    /// The sml.rs state label.
    pub label: L,
    /// The context's [`Context::fingerprint`].
    pub fingerprint: F,
}

/// `machine`'s snapshot now.
pub fn snapshot<M: Machine>(machine: &M) -> SnapshotOf<M> {
    Snapshot {
        label: machine.label(),
        fingerprint: machine.context().fingerprint(),
    }
}

/// Run `ev` to completion in `machine` and drain the effects its actions
/// pushed. Serves the driver, [`Inline`], replay, and offline callers.
pub fn step<M: Machine>(machine: &mut M, ev: M::Event) -> Vec<Effect<Output<M>>> {
    if !machine.process(ev) {
        tracing::error!("the machine's table rejected an event");
    }
    machine.context_mut().drain()
}

machine-macro

Source: crates/lvcs-actor/src/lib.rs.

/// Implement [`Machine`] for a generated sml.rs machine in one line.
///
/// `lvcs_actor::machine!(XStateMachine<XContext>, XEvents, XEvent, XStates);`
/// names the generated machine over its context, its generated events, the
/// hand-written event enum, and the generated states. `XEvents:
/// From<XEvent>` is the owning crate's pure map from the hand-written enum
/// to the generated events.
#[macro_export]
macro_rules! machine {
    ($machine:ident < $context:ty >, $events:ty, $event:ty, $label:ty) => {
        impl $crate::Machine for $machine<$context> {
            type Event = $event;
            type Label = $label;
            type Context = $context;

            fn process(&mut self, ev: $event) -> bool {
                <$machine<$context> as ::sml::Machine<$events>>::process_event(
                    self,
                    <$events>::from(ev),
                )
            }

            fn label(&self) -> $label {
                ::core::clone::Clone::clone($machine::<$context>::state(self))
            }

            fn context(&self) -> &Self::Context {
                $machine::<$context>::context(self)
            }

            fn context_mut(&mut self) -> &mut Self::Context {
                $machine::<$context>::context_mut(self)
            }
        }
    };
}

effect-sink

Source: crates/lvcs-actor/src/lib.rs.

/// Receives the outputs an actor emits. Implemented for any `FnMut(O)`.
///
/// The sink runs on the actor's task; I/O-heavy sinks should forward to a
/// channel rather than block.
pub trait EffectSink<O>: Send + 'static {
    /// Carry out one emitted output.
    fn apply(&mut self, output: O);
}

envelope

Source: crates/lvcs-actor/src/wire.rs.

/// The envelope's version (design §5).
///
/// `major` changes only with an incompatible layout: a peer rejects an
/// envelope whose `major` is not `lvcs_vocab::ids::WIRE_MAJOR` with
/// [`WireError::UnknownMajor`]. A new message kind is a `minor` change:
/// peers accept any `minor` and ignore kinds they don't know (design §10).
/// Encoded as the map `{ major, minor }`. Owner: `M3.envelope`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WireVersion {
    /// Changes only with an incompatible layout.
    pub major: u16,
    /// Changes when a message kind is added; peers accept any.
    pub minor: u16,
}

/// Who signed an envelope (design §5, §9, §15).
///
/// Chosen at the M3 freeze: gossip messages are signed by the participant
/// who sent them, so capability checks and per-signer token buckets key on
/// the participant, while gossipsub's own message author (the forwarding
/// device) is what a repeat offense down-scores; sync-stream frames are
/// signed by the sending device, whose identity Noise already
/// authenticated. Participant and device keys are separate keys (design
/// §2): a participant key is never a device key. Encoded as a one-entry map
/// `{ participant: <bytes> }` or `{ device: <bytes> }`. Owner:
/// `M3.envelope`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Sender {
    /// A participant's Ed25519 public key: the bytes of an
    /// `lvcs_doc::ParticipantKey`.
    Participant([u8; 32]),
    /// A device's Ed25519 public key, from which its libp2p `PeerId`
    /// derives.
    Device([u8; 32]),
}

/// One message between peers (design §5, decisions 20 and 67).
///
/// `payload` is the canonical CBOR encoding of the payload type `kind` names
/// (the `wire_kind` set in `docs/generated/vocabulary.md`, an open set: a
/// kind outside it is ignored, never rejected). `frontier` is the sender's
/// visible frontier id when the message is about project state (absent
/// otherwise; encoded as CBOR null, so the field set never varies). `sig`
/// is `sender`'s `SIGNATURE` over the canonical CBOR array
/// `[v, kind, sender, frontier, payload]` (an array of the signed fields,
/// like `ChangeSig`'s, chosen at the M3 freeze). Encoded as the map
/// `{ v, kind, sender, frontier, payload, sig }` in that order, digests and
/// `payload` as byte strings. Automerge sync messages travel inside a
/// `sync` payload unchanged (decision 30). Owner: `M3.envelope`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[expect(
    clippy::min_ident_chars,
    reason = "the field `v` is named for the encoded envelope's `v` map key (design section 5), \
              which this contract type mirrors field for field; Clippy checks a field's name \
              against the struct's lint level, so the struct is the narrowest item"
)]
pub struct Envelope {
    /// The envelope's version.
    pub v: WireVersion,
    /// The message kind: a `wire_kind` value, or one this peer doesn't know.
    pub kind: String,
    /// Who signed the envelope.
    pub sender: Sender,
    /// The sender's visible frontier id, when the message is about project
    /// state.
    pub frontier: Option<[u8; 32]>,
    /// The canonical CBOR encoding of the payload type `kind` names.
    pub payload: Vec<u8>,
    /// `sender`'s signature over the signed fields.
    pub sig: [u8; 64],
}

/// Why bytes are not an acceptable envelope (design §5).
///
/// Each maps to one `rejection` value: `TooLarge` and `Malformed` to
/// `malformed`, `UnknownMajor` to `unknown_version`, `BadSignature` to
/// `bad_signature`. Owner: `M3.envelope`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireError {
    /// Longer than `lvcs_vocab::limits::WIRE_MAX_BYTES`; not decoded.
    TooLarge {
        /// The length received, in bytes.
        len: usize,
    },
    /// Not a canonical CBOR envelope or payload.
    Malformed(String),
    /// The envelope's major version is not `WIRE_MAJOR`.
    UnknownMajor {
        /// The major version the envelope carried.
        major: u16,
    },
    /// `sig` is not `sender`'s signature over the signed fields.
    BadSignature,
}

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; 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.
    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),
    /// 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>,
    },
    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),
}

/// 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.
///
/// It holds the document's heads, which replay loads an incarnation at, and
/// a digest of the durable context (decisions 32, 84, and 222). Every event runs to completion, so
/// between events the label is `Ready` or `Closed`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocFingerprint {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// 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 as it is now, so it may already hold signatures
    /// that arrived after a journaled state; 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 effect outbox, 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),
    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::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>>,
}

doc-overlay

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

/// Set the anchors a workspace watches in this document (design §8,
/// decision 82), replacing any set `watcher` sent before; an empty set ends
/// the watch and is not answered. File and override documents take it: the
/// anchors are into `/content`. A stacked workspace watches the document
/// that holds each range in its base view, which may be an override of a
/// workspace it stacks on. The document answers a non-empty set at once
/// with [`AnchorsResolved`] at its current heads, then again just before
/// every render. A watch lasts for the document actor's incarnation.
///
/// Watches are durable context, so the journaled state covers them
/// (decision 32): [`DocFingerprint::digest`] covers the watch map, ordered
/// by watcher and then override id, beside the heads, and replay rebuilds
/// the map from the incarnation's journaled `WatchAnchors`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchAnchors {
    pub watcher: WorkspaceId,
    pub anchors: BTreeMap<OverrideId, RangeAnchor>,
}

/// One watcher's anchors resolved at `heads`, the document's heads after a
/// change (or when the watch arrived). Emitted once per watcher just before
/// the [`DocOutput::Rendered`] for the same heads, so the workspace composes
/// each render with spans of the same version. A span is `None` while the
/// document lacks the anchor's heads (they may not have synced yet), or
/// when the anchor names characters not visible, in order, at them;
/// [`AnchorSpan::Collapsed`] means the base deleted the whole range, and
/// the override has lost its anchor (design §8, decision 41): its `at` is
/// the gap where the range was, where the view keeps the override's text
/// (decision 92). `lines` names each span as a reader is shown it, from
/// the text at `heads` (`lines::name`; a collapsed span names the line its
/// gap is in), keyed as `spans` is, and is `None` where the span is
/// (decision 165).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorsResolved {
    pub watcher: WorkspaceId,
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub spans: BTreeMap<OverrideId, Option<AnchorSpan>>,
    pub lines: BTreeMap<OverrideId, Option<LineName>>,
}

/// Anchor a new claim for a workspace (design §8).
///
/// Claims never combine or overlap (decisions 71, 83, 95, 99, 100, 156, and
/// 160): the part of an edit inside an existing claim goes to that
/// override, and the workspace sends one `Claim` for the parts of an edit
/// in each maximal run of unclaimed characters, claimed together as one
/// range; adjacent overrides stay separate and the view composes them.
/// `at` is the heads of the document that holds the claimed characters in
/// the claimant's view (decision 89), and `start..end` are code points of
/// its `/content` there: the characters the parts replace, or an empty
/// range (`start == end`) for an insertion at a gap.
///
/// The document snaps the claim to whole lines of `/content` at `at`, in
/// every text document, `file` and `override` alike, and in every language
/// (decisions 156 and 160); a line is its characters and its trailing
/// newline (decision 95), and nothing is parsed. A range snaps from the
/// start of the line holding its first character to the end of the line
/// holding its last. An insertion snaps, in decision 95's order, to the
/// line ending just before the gap (whose newline, or the text's last
/// character, is just before it), else to the line holding the character
/// after it: so a gap inside a line claims that line, a gap at a line's
/// start the line before it, the gap at the text's end the last line, and
/// the gap at 0 the first line. It then clips the snapped range to
/// the run of unclaimed characters (none of `watcher`'s watched anchors in
/// this document covers them at `at`, decision 83's resolution) that
/// contains the range or, for an insertion, borders the gap (the run
/// holding the character before it, else the one holding the character
/// after it); an insertion tries those choices in order until one leaves a
/// non-empty range. A watched anchor that resolves to
/// [`AnchorSpan::Collapsed`] at `at` covers no characters, but its gap is a
/// boundary (decision 100): a run never holds one strictly inside it, so a
/// snapped range that would straddle the gap is clipped to the side holding
/// the claimed range, or for an insertion to the side the run bordering the
/// gap is on, and no new claim holds a collapsed override's gap strictly
/// inside it. It answers with exactly one [`Claimed`]. A claim whose lines
/// touch or overlap another claim of the same workspace is clipped beside
/// it, never merged into it; readers are shown the touching or
/// overlapping claims of one participant in one workspace and one document
/// as one line range (decisions 156 and 160).
///
/// `run_start..run_end` is the claimant's view run (decision 101's M3
/// deferral, frozen at the M3 freeze): the maximal stretch of the
/// claimant's view that comes from this document and holds `start..end`,
/// in code points of `/content` at `at`, bounded by the claimant's own
/// overrides and by those of every workspace it stacks on. The snapped
/// range is also clipped to it, so a claim of text a stacked workspace
/// inherits from a lower override never reaches past what the claimant
/// sees from that override: characters outside the run count as claimed,
/// so a range reaching past it is refused `OutOfBounds` like one that
/// overlaps a watched claim, and an insertion borders the run only from
/// inside it. A run past the text, or ending before it starts, refuses the
/// claim `OutOfBounds`. `M3.workspace` sends it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claim {
    pub watcher: WorkspaceId,
    /// Chosen by the workspace actor, increasing per workspace; echoed by
    /// [`Claimed`].
    pub claim: u64,
    #[serde(with = "crate::cbor::heads")]
    pub at: Vec<ChangeHash>,
    pub start: usize,
    pub end: usize,
    pub run_start: usize,
    pub run_end: usize,
}

/// The answer to a [`Claim`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claimed {
    pub watcher: WorkspaceId,
    pub claim: u64,
    pub outcome: ClaimOutcome,
}

/// What a [`Claim`] produced.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClaimOutcome {
    /// The snapped and clipped range, anchored at the claim's `at`: the
    /// anchor's [`RangeAnchor::heads`] are the claimed document's heads at
    /// claim time, and `start..end` is the range in code points of
    /// `/content` there (decision 87), so the workspace can cut the new
    /// override's init text and place the edit in it without waiting for
    /// a resolution at later heads. It overlaps none of the watcher's
    /// claims.
    Anchored {
        anchor: RangeAnchor,
        start: usize,
        end: usize,
    },
    /// `UnknownBase` when the document lacks `at`, `OutOfBounds` when
    /// `start..end` is past the text there, overlaps a watched claim,
    /// reaches outside the view run (or the run is past the text), or
    /// holds a collapsed watched claim's gap strictly inside it,
    /// `Failed` when nothing is left to anchor (no anchor is empty): an
    /// empty text, or an insertion with claimed characters (or an end of
    /// the text) on both sides of the gap; also `Failed` when the document
    /// faulted answering the claim or was closed.
    Refused(RejectReason),
}

/// Read this document's body at `heads` (design §8). The workspace actor
/// reads an override at its init heads for the claim-time base text and
/// executable bit a merge-back merges against (decision 85), a base
/// document at a claim's heads, with `within` the claim's anchor, for who
/// changed the range since, and a whited-out base document at its
/// whiteout's heads for who changed the file since (decision 86). `req` is
/// chosen by the workspace actor, as for an edit it routes. File and
/// override documents take it. Answered with exactly one [`ReadAtDone`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadAt {
    pub req: RequestId,
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// Limit the answer's `authors` to changes inside this range.
    pub within: Option<RangeAnchor>,
    /// Name the window from these heads to `heads` in the answer's
    /// `window` (decision 193): the changes in `heads`' history and not in
    /// these heads' history, an empty set naming every change up to
    /// `heads`. `None` for no window.
    #[serde(with = "crate::cbor::opt_heads")]
    pub since: Option<Vec<ChangeHash>>,
}

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

/// What a [`ReadAt`] found.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadAtOutcome {
    /// The file at `heads`, and the Automerge actors of the changes the
    /// document holds beyond `heads`; with `within`, only those that
    /// inserted a character inside its range or deleted one of its
    /// characters (a character is inside when it lies between the anchor's
    /// first and last characters in list order, decision 83; the range's
    /// characters are those visible inside it at the anchor's heads and
    /// those inserted inside it since). The daemon maps them to
    /// participants (see `Conflict.authors` in `lvcs-ws`). The two flags
    /// read every change beyond `heads`, `within` or not, for the
    /// protected-path gate (decision 122): `unattributed` is set when one
    /// names no [`AuthorId`] (an actor id that is not 16 bytes), and
    /// `bundled` when one's record names a merge bundle (decision 125).
    /// `window`, with `since`, maps the author of each change in the
    /// window to the lines of `body` it touched (decision 193): those
    /// holding a character it inserted that is visible at `heads`, and
    /// the line where a character it deleted was, as `lines::names` names
    /// them, without `near`; a binary or deleted body names no lines, and
    /// a change whose actor id names no [`AuthorId`] is left out. It is
    /// empty without `since`. `derived` is `/meta/derived` at `heads`
    /// (row 180).
    Read {
        body: FileBody,
        executable: bool,
        derived: bool,
        authors: BTreeSet<AuthorId>,
        unattributed: bool,
        bundled: bool,
        window: BTreeMap<AuthorId, Vec<LineName>>,
    },
    /// `UnknownBase` when the document lacks `heads`, `since`, or
    /// `within`'s heads;
    /// `Failed` when `within` names characters not visible, in order, at
    /// its heads, or the document faulted answering or was closed.
    Refused(RejectReason),
}

/// A merge bundle's id (design §8, decisions 48 and 57; `BundleId::of`):
/// the `HASH` of the canonical CBOR array (workspace id, merging
/// participant key, the workspace's frontier id, the base's visible
/// frontier id), each a byte string, so it never depends on the changes
/// that carry it. Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BundleId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// One merge-back change of a `WorkspaceMerged` bundle (decision 48), the
/// only way a workspace's work reaches a base document. Committed on
/// `origin.author`'s own branch at `base`, like an [`Edit`] or the matching
/// [`DiskChange`], except that the commit's record ([`ChangeMessage`],
/// its Automerge `message`) names `bundle`, so every peer knows the change
/// belongs to the bundle before the merge record arrives; no other change's
/// record names a bundle (decision 125). Answered like an `Edit`:
/// [`DocOutput::Changed`] with `req`, or [`DocOutput::Rejected`]
/// (`UnknownBase` for a `base` the document lacks, `OutOfBounds` for text
/// splices that don't apply there), then rendered. It always commits, so a
/// change that writes what `base` already holds records nothing and answers
/// with the document's heads unchanged. File and override documents take
/// it. `req` is chosen by the workspace actor, as for an edit it routes.
/// A `derived` of `Mark` makes a text or recreate change one whole-text
/// replacement at `base` that puts `/meta/derived` true, never a three-way
/// merge; `Unmark` puts it false; `Keep` leaves it (row 180).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleEdit {
    pub bundle: BundleId,
    pub req: RequestId,
    pub origin: Origin,
    #[serde(with = "crate::cbor::heads")]
    pub base: Vec<ChangeHash>,
    pub change: BundleChange,
    /// How the change treats `/meta/derived` ([`DerivedFlag`]).
    pub derived: DerivedFlag,
}

/// What a [`BundleEdit`] writes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BundleChange {
    /// Splices of `/content` at `base`, sorted and disjoint, applied like
    /// an [`Edit`]'s.
    Text(Vec<Splice>),
    /// Recreate a deleted file with this text (a workspace-local file
    /// whose base document is deleted, at genesis or after history,
    /// decision 98): at `base`, replace the whole `/content` with it, and
    /// delete and re-put `/meta/format` (`text`) and `/meta/deleted`
    /// (false) (decision 76), so the file's old content, which removal
    /// left in `/content`, is never kept beside the new text.
    Recreate(String),
    /// Replace the file with these bytes (a binary whole-document
    /// override), like [`DiskChange::Binary`].
    Binary(#[serde(with = "crate::cbor::bytes")] Vec<u8>),
    /// Set the executable bit, like [`DiskChange::Executable`].
    Executable(bool),
    /// Delete the file (a merged whiteout), like [`DiskChange::Removed`].
    Removed,
}

cursor

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

/// One character of an Automerge `Text`: the counter and actor of the
/// operation that inserted it, which every peer shares (decision 76).
/// Serialized as `{ counter, actor }` with the actor id as a byte string.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "ElementWire", into = "ElementWire")]
pub struct ElementId {
    counter: u64,
    actor: ActorId,
}

/// An inclusive range anchor (design §8, decisions 41, 76, and 83): `start`
/// is the range's first character, with `MoveCursor::After`, and `end` its
/// last, with `MoveCursor::Before`, both visible at the heads `at` it was
/// made at. No anchor is empty. An insert exactly at either boundary lands
/// outside the range, because each anchor is a character, not a gap.
/// Resolved where no character between the two anchors in list order is
/// visible, `start` moves past `end` and the anchor resolves as
/// [`AnchorSpan::Collapsed`] on every peer alike, at the gap where the
/// range was.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RangeAnchor {
    /// The heads the anchor was made at, sorted and deduplicated.
    #[serde(with = "crate::cbor::heads")]
    at: Vec<ChangeHash>,
    start: ElementId,
    end: ElementId,
}

/// A [`RangeAnchor`] resolved at some heads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AnchorSpan {
    /// The range is the code points `start..end` there (`start < end`):
    /// `start` is the first character's position or, once it is deleted,
    /// that of the next character visible in list order at those heads
    /// (decision 83); `end - 1` the same for the last character and the
    /// previous visible one.
    Range { start: usize, end: usize },
    /// No character between the anchors in list order is visible there:
    /// every character the range held is deleted, and so is anything
    /// inserted inside it, so the resolved `start` is past the resolved
    /// `end` (decision 41). `at` is the gap where the range was, in code
    /// points at those heads: how many characters visible there come
    /// before the range's first character in list order (decision 92).
    Collapsed { at: usize },
}

frontier

Source: crates/lvcs-doc/src/frontier.rs.

/// A frontier id (design §2): the `HASH` of the canonical CBOR map from
/// document address to its sorted heads. Written in `ID_ENCODING`; its one
/// implementation is [`Frontier::id`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct FrontierId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// A frontier (design §2): each document's heads, by address. Its canonical
/// CBOR encoding is the map itself, each head a 32-byte byte string, so a
/// frontier with sorted head lists encodes as exactly what its id hashes.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Frontier {
    #[serde(with = "crate::cbor::head_map")]
    pub docs: BTreeMap<String, Vec<ChangeHash>>,
}

/// Whether one set of a document's heads descends from another (decision
/// 120): `Yes` when every change in `from` is in the history of `heads`
/// (equal sets included), `No` when one is not, and `Unknown` when the
/// document lacks a change of either set, so a peer short of history
/// decides nothing until sync supplies it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Descent {
    Yes,
    No,
    Unknown,
}

authorize

Source: crates/lvcs-doc/src/kinds/caps/authorize.rs.

/// One use of a capability to check: `signer`, holding the grant `grant`
/// (`GrantId::OWNER` for the owner, who needs none), does `need` on the
/// document at `address` at `time_s`, a change's Automerge time in Unix
/// seconds. For a change, `signer` and `grant` are its `ChangeSig`'s and
/// `address` its `doc`. Owner: `M3.grants`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Attempt<'a> {
    pub signer: &'a ParticipantKey,
    pub grant: &'a GrantId,
    pub address: &'a str,
    pub time_s: i64,
    pub need: &'a Capability,
}

/// What [`authorize`] found (decision 146): the signer's trust tier
/// (design §15; `owner` for the owner, `participant` under any grant), the
/// grant's change quota (`None` for the owner and for a grant whose terms
/// name none), which the document actor counts by `seq` among the actor's
/// changes under the grant (decision 70) and which makes the grant's own
/// actor id the change's (`AuthorId::granted`, decision 125), and the
/// grant's rate terms. Owner: `M3.grants`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Authorized {
    pub tier: TrustTier,
    pub quota: Option<u64>,
    pub rates: GrantRates,
}

/// A grant's rate terms (`GrantTerms`, decision 146): each one present is
/// the grant's value for the grantee's signing or gossip bucket in place
/// of its `[limits]` default, and the daemon meters the grantee at the
/// largest over its live grants (decision 178); all absent for the owner,
/// who holds no grant, and for a grant whose terms name none. Owner:
/// `M3.grants`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct GrantRates {
    pub sign_per_sec: Option<u32>,
    pub sign_burst: Option<u32>,
    pub gossip_per_sec: Option<u32>,
}

/// Whether the grants in `known` authorize `attempt`: the owner (the
/// signer is `known.root`'s owner and names `GrantId::OWNER`) may do
/// anything; any other signer needs `attempt.grant` in `known.grants`,
/// granted to it, with a window containing `time_s`, naming `need`, with a
/// path pattern matching `address`, and issued by the owner or by the
/// grantee of a chain link: a grant that names [`Capability::Delegate`]
/// and itself holds at `time_s` for `need` and `address`, issued by the
/// owner or another link. Every link must cover the attempt, so a
/// delegated grant is never wider than its issuer's (decision 109). No
/// rule reads a grant's terms (decisions 146 and 147): every window is
/// `not_before_s..not_after_s` as written, any grant may name any
/// capability on any paths, and any grant naming `Delegate` is a link. A
/// grant `known.revoked` holds at `t` (decision 148) holds only before
/// `t`: its window ends at `min(not_after_s, t)`, for the attempt and for
/// every chain link, so a revoked `Delegate` link counts only for an
/// attempt before `t`.
/// Path patterns are gitignore syntax, matched against `address` and the
/// directories above it; a `!` pattern excludes what it matches wherever it sorts; an
/// `address` with a segment that is empty, `.`, or `..`, and a list with a
/// pattern that doesn't parse or a `!` line whose rest starts with `#` or
/// `!` (stripped of its `!`, it would be a comment or another `!` line,
/// and exclude nothing), match nothing. For a control document,
/// `address` is what the write concerns (decisions 108, 109, 111, 112,
/// and 147; the document verifier maps each change to its attempts): for an override
/// change, the file path its claim resolves to in the workspace
/// (`OverrideClaim::path`, decision 111), never its base, which for a
/// stacked claim is the lower override's address; for an index entry, the
/// file path its key names (a whiteout's key, or the `path` of the claim an
/// override or workspace-local entry's key, its override id, commits to;
/// decision 112), never what its value claims; and each path a merge
/// record lists, one attempt each;
/// a change to a thread document (`THREAD_ADDRESS`), with
/// `need` `Write` or `Message`, needs any valid grant: its chain and
/// windows are checked, its capabilities and paths are not. So does
/// `Write` on the workspace registry (`REGISTRY_ADDRESS`) or on a
/// workspace index (`WORKSPACE_INDEX_ADDRESS`, one name in its slot;
/// decisions 111 and 112), which the document verifier names only for a
/// registry workspace entry and an index init (`/name`, `/base`,
/// `/subtree`, `/created_ms`, `/base_frontier`), each written alone and
/// accepted only where `WorkspaceId::of` its own fields and its signer
/// names it, so a grant lets its holder create workspaces of its own and
/// never write another participant's; an index entry's attempt is `Write`
/// on the file path its key names. So does `Write` on `ALLOWLIST_ADDRESS`
/// (decision 147), which no document has and the document verifier names
/// only for a caps change adding `/allowlist` entries, so any valid grant
/// may add one. A change to the caps document
/// (`CAPS_ADDRESS`, `need` `Write`) needs a grant that names `Delegate`
/// (decision 109): its chain and windows are checked, its paths
/// are not; any other `need` on that address is checked like a path, as
/// is any other `need` on the registry's, an index's, or
/// `ALLOWLIST_ADDRESS`'s own address. The
/// thread, registry, index, allowlist, and caps rules read `address` alone, so the
/// caller names one of those addresses only for a change to that
/// document (for `ALLOWLIST_ADDRESS`, a caps change's allowlist adds),
/// never as the path an entry, claim, or merge record of a
/// change holds. On a channel document's address (`CHANNEL_PREFIX` and a
/// name `ChannelName::parse` takes), `Attest` naming that channel and
/// `Approve` are decided by the address alone, like the thread and caps
/// rules (decision 119): the chain, windows, and capability are checked,
/// its paths are not; any other
/// `need` there is checked like a path. `Approve` on a file address stays a
/// path check. That a caps change only adds entries (a `/grants/<id>`
/// entry whose issuer is the signer, an `/allowlist/<hash>` entry), never
/// deletes or overwrites one, and that only the
/// project's first change writes `/root`, is the caller's merge-time check
/// (`M3.doc`), not this one's.
/// Refusals, in
/// this order: `NoCapability` (`known.root` is unknown, a signer other
/// than the owner names `GrantId::OWNER`, the grant is unknown or another
/// signer's), `GrantExpired` (its window doesn't contain `time_s`, or it
/// was revoked at or before `time_s`),
/// `OutOfScope` (it doesn't name `need` or match `address`), and
/// `NoCapability` (no chain of links reaches the owner, as when the
/// issuer's own grant is narrower than the attempt). A chain link's path patterns are compiled
/// only once its issuer is reached, so grants that don't reach the owner
/// cost no matching.
pub fn authorize(known: &GrantsKnown, attempt: &Attempt<'_>) -> Result<Authorized, SyncRejection> {

caps

Source: crates/lvcs-doc/src/kinds/caps/mod.rs.

/// The root of every grant chain (design §11): the owner's key and the
/// project name, whose [`ProjectRoot::project_id`] is the project id,
/// `HASH` of the canonical CBOR array (owner key bytes, name text), so
/// `lvcs://<project-id>` names a project without a registry. A daemon
/// accepts a `.lvcs/caps` owner only when its root's project id equals the
/// project id the daemon runs (the one `lvcs init` made, or the one
/// `lvcs join` was given): `lvcs join` learns the root from the caps
/// document's seed and checks it before it verifies any other change, so
/// the first peer it syncs from can't substitute an owner of its own.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectRoot {
    pub owner: ParticipantKey,
    pub name: String,
}

/// A grant's id: the `HASH` of its [`Grant`]'s canonical CBOR encoding
/// ([`Grant::id`]), so a renewal (a new window) is a new grant with a new
/// id and a fresh quota (design §15). `ChangeSig::grant` names it. Written
/// in `ID_ENCODING`. The all-zero id ([`GrantId::OWNER`]) names no grant:
/// the owner's own changes, which need none (chosen at the M3 freeze).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct GrantId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// One thing a grant lets its grantee do (design §11, §15). Chosen at the
/// M3 freeze: changes and gossip are separate capabilities, so a grant can
/// let a participant talk without writing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Capability {
    /// Sign changes to documents under the grant's `paths`: edits, mirror
    /// saves, merge-back bundle changes, and control-document writes.
    Write,
    /// Publish gossip messages (notifications, responses, thread messages,
    /// presence, typing) about documents under the grant's `paths`.
    Message,
    /// Attest the named channel (`ci`, `staging`): a
    /// runner's capability (design §7, §10).
    Attest(String),
    /// Approve protected-path changes (design §15).
    Approve,
    /// Issue grants no wider than this one (design §11).
    Delegate,
}

/// A grant's terms (design §15, decision 146), every one optional and
/// left out of the grant's encoding when absent. `harness` and `model`
/// name what the grantee runs, for attribution only: a receiving daemon
/// shows them beside the sender's `participant` tier, computed from the
/// grant `authorize` found, never from a message (decision 147), and no
/// check reads them but their length: each is at most
/// `GRANT_TERM_MAX_BYTES` (decision 178), and a caps change putting a
/// grant with a longer one is `out_of_scope`. `quota` is decision 70's change quota, enforced only
/// when set: in each document, a change signed under the grant is invalid
/// on every peer when it comes after `quota` among its actor's changes
/// under the grant, in Automerge `seq` order, and such a change's actor id
/// is the grant's own (`AuthorId::granted`, decision 125); the daemon signs
/// under a grant without a quota before a quota'd one, and under the
/// largest quota first (decision 178). `sign_per_sec`, `sign_burst`, and
/// `gossip_per_sec` are this grant's values for the grantee's signing and
/// gossip buckets, an absent one giving the `[limits]` default in force;
/// the grantee is metered at the largest over its live grants (decision
/// 178), so no grant lowers the rate another gives. An issuer may set any
/// value, since limits bound load, not authority. A renewal copies the
/// terms. `alias` is the name the project owner gives the grantee
/// (decisions 243 and 247), for display and addressing only, no
/// authority: at most `ALIAS_MAX_BYTES` of lowercase ASCII letters,
/// digits, and `-`, starting with a letter ([`alias_valid`]), on a grant
/// the owner issued, else the grant is `out_of_scope` on every peer. An
/// alias is bound for the life of the project to the grantee of the first
/// grant naming it, ordered by `not_before_s` then grant id, whether that
/// grant is live, expired, or revoked; a later grant naming it for another
/// grantee gives that grantee no alias.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantTerms {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub harness: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub quota: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sign_per_sec: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sign_burst: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gossip_per_sec: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub alias: Option<String>,
}

/// A capability grant (design §11, §15, decision 58): `issuer` (the owner,
/// or a holder of [`Capability::Delegate`]) lets `grantee`, on the
/// [`GrantTerms`] `terms`, do `capabilities` on `paths` (gitignore-syntax
/// patterns relative to the project root, like every LVCS path pattern,
/// matched with the `ignore` crate's gitignore matcher, which `lvcs-doc`
/// links from the M3 freeze; sorted; `**` for everything)
/// within the window `not_before_s..not_after_s` in Unix seconds. A change
/// is within the window when the window contains the change's own Automerge
/// time (design §3), which its hash covers, so every peer decides alike
/// whatever its clock says; `not_after_s` is exclusive and absent only for
/// a grant with no expiry. `capabilities` is sorted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Grant {
    pub issuer: ParticipantKey,
    pub grantee: ParticipantKey,
    pub terms: GrantTerms,
    pub paths: Vec<String>,
    pub capabilities: Vec<Capability>,
    pub not_before_s: i64,
    pub not_after_s: Option<i64>,
}

/// A grant with its issuer's `SIGNATURE` over [`Grant::signing_input`].
/// The `.lvcs/caps` document holds every signed grant; a grant whose
/// signature or chain fails is ignored by every peer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedGrant {
    pub grant: Grant,
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
}

/// The static signed capability file `lvcs init` writes and that seeds the
/// `.lvcs/caps` document (design §11), at `.lvcs/capabilities.cbor`: its
/// format version `v` (1), the project id's bytes, the project name and
/// the owner's key (the [`ProjectRoot`], whose project id must equal
/// `project`, so a peer that knows only the id can check the owner), the
/// seed grants (`lvcs init` writes none), and the owner's `SIGNATURE` over
/// [`CapsFile::signing_input`]. Encoded as canonical CBOR. The caps
/// document's seed change carries `name`, `owner`, and the grants, so a
/// peer that syncs `.lvcs/caps` learns the root.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapsFile {
    pub v: u64,
    #[serde(with = "crate::cbor::fixed")]
    pub project: [u8; 32],
    pub name: String,
    pub owner: ParticipantKey,
    pub grants: Vec<SignedGrant>,
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
}

channel

Source: crates/lvcs-doc/src/kinds/channel/mod.rs.

/// A channel's name (decisions 119 and 245): one or more `/`-separated
/// segments of `[a-z0-9._-]`, none of them empty or `.`, holding no `..`
/// anywhere, and at most `CHANNEL_NAME_MAX_BYTES` bytes in all. `ci`
/// (`CI_CHANNEL`) is the channel every change action moves; any other
/// name is a promote action's channel, named for the action. Its document is at
/// `CHANNEL_PREFIX` + the name, written literally, and stores no name: the
/// verifier reads it from the address. Every API parameter and `[git]
/// branches` key parses with [`ChannelName::parse`]. A JSON or CBOR text
/// string; one that doesn't parse is refused.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ChannelName(String);

/// Why [`ChannelName::parse`] refused a name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelNameError {
    /// A segment is empty or `.`, the name holds `..`, or a segment holds
    /// a byte outside `[a-z0-9._-]`.
    Segment,
    /// It is longer than `CHANNEL_NAME_MAX_BYTES` bytes.
    TooLong,
}

/// An action's id (decision 245): `HASH([ACTION_DOMAIN, action])` over
/// the canonical encoding of the action less its name, source, and quorum,
/// computed by lvcs-action's `Action::id`. Change actions, promote actions,
/// and the generator are all keyed by it. Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ActionId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// The `HASH` of everything one action read at a frontier (decisions 121
/// and 245).
///
/// It covers the action id, the heads of the documents its `paths` and
/// `uses` match (decision 161), the dependency frontier, and the toolchain
/// hash. Two runners that ran an action on equal inputs name equal hashes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct InputsHash(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// A runner's attestation (decisions 119 and 245), only in `ci`: every
/// change action in `actions` passed on `frontier` under the toolchain
/// `toolchain_hash`. `sig` (the `runner` key's) and `device_sig` (the
/// `device` key's, the runner's daemon) both cover
/// [`Attestation::signing_input`]. Keyed [`Attestation::key`],
/// `HASH([runner, frontier id])`, so a runner attests a frontier once.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attestation {
    pub frontier: Frontier,
    pub actions: BTreeMap<ActionId, InputsHash>,
    #[serde(with = "crate::cbor::fixed")]
    pub toolchain_hash: [u8; 32],
    pub runner: ParticipantKey,
    pub device: DeviceKey,
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
    #[serde(with = "crate::cbor::fixed")]
    pub device_sig: [u8; 64],
}

/// A human's approval (decision 119), only in `ci`: `approver`
/// approves the changes `frontier` carries to each of `paths`, which are
/// sorted, unique, non-empty, at most `APPROVAL_MAX_PATHS`, and each a file
/// address `paths::protected()` matches. It carries no grant and no inner
/// signature: the `ChangeSig` of the change that puts it names the grant
/// and signs it. Keyed [`Approval::key`], `HASH([approver, frontier id,
/// paths])`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Approval {
    pub frontier: FrontierId,
    pub approver: ParticipantKey,
    pub paths: Vec<String>,
}

/// A promote action's run key (decisions 141 and 245).
///
/// It is `HASH([ACTION_DOMAIN, action, frontier id, promotion])` for the
/// promotion it serves, so each promotion runs once. A run's commands
/// receive it as `LVCS_RUN`.
/// Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct RunKey(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// A promote action's run outcome (decisions 141 and 245), only in a
/// promote channel.
///
/// `runner` ran the promote action `action` on `frontier` as the run
/// `run`, and it `passed` or failed, at `time_ms` on the runner's clock,
/// which orders nothing. `sig` (the `runner` key's) and `device_sig` (the
/// `device` key's, the runner's daemon) both cover
/// [`PromoteOutcome::signing_input`]. Keyed [`PromoteOutcome::key`],
/// `HASH([runner, run])`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PromoteOutcome {
    /// The frontier the run promoted.
    pub frontier: Frontier,
    /// The promote action that ran.
    pub action: ActionId,
    /// The run's key.
    pub run: RunKey,
    /// Whether the run's commands exited zero.
    pub passed: bool,
    /// The participant whose runner ran it, and the change's signer.
    pub runner: ParticipantKey,
    /// The runner's daemon.
    pub device: DeviceKey,
    /// When the run ended, in milliseconds since the Unix epoch.
    pub time_ms: i64,
    /// The runner key's signature over the signing input.
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
    /// The device key's signature over the signing input.
    #[serde(with = "crate::cbor::fixed")]
    pub device_sig: [u8; 64],
}

/// A promotion (decision 141), only in a promote channel.
///
/// `promoter` asks that the channel's promote action run on `frontier`, at
/// `time_ms`.
/// Promotions are ordered by `(time_ms, entry id)`. It carries no inner
/// signature: the `ChangeSig` of the change that puts it names the grant
/// and signs it. Keyed [`Promotion::key`], `HASH([promoter, frontier id,
/// time_ms])`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Promotion {
    /// The frontier to promote.
    pub frontier: FrontierId,
    /// The participant that promoted it, and the change's signer.
    pub promoter: ParticipantKey,
    /// When it was promoted, in milliseconds since the Unix epoch: the
    /// change that puts it is dated `time_ms / 1000` seconds, floored, or
    /// every verifier refuses it.
    pub time_ms: i64,
}

/// One output of a generation (row 180): the SHA-256 of the output's
/// bytes, and the heads holding those bytes in its document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeneratedOutput {
    /// The SHA-256 of the output's bytes.
    #[serde(with = "crate::cbor::fixed")]
    pub digest: [u8; 32],
    /// The heads holding those bytes in the output's document.
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
}

/// Why a derivation failed (row 180).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeriveFailure {
    /// The generator exited non-zero, was killed, or timed out.
    Exit,
    /// The run created, changed, or removed a file the classifier doesn't
    /// call derived.
    NonDerivedWrite,
}

/// A runner's generation record (row 180, decision 245), only in `ci`.
///
/// `runner` ran the generator `action` (the one action with `writes`) on
/// the inputs `inputs` at `frontier` under the toolchain `toolchain_hash`,
/// and it wrote or found `outputs` (each output address's digest and
/// heads), or it failed with `failure`. `outputs` is empty exactly when
/// `failure` is set, and it holds at most `GENERATION_OUTPUTS_MAX`
/// addresses (a map, so sorted and unique). `sig` (the `runner` key's) and
/// `device_sig` (the `device` key's, the runner's daemon) both cover
/// [`Generation::signing_input`]. Keyed [`Generation::key`],
/// `HASH([runner, inputs])`, so a runner records one outcome per inputs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Generation {
    /// The generator that ran.
    pub action: ActionId,
    /// The generator's `InputsHash`.
    pub inputs: InputsHash,
    /// The frontier the derivation ran on.
    pub frontier: Frontier,
    /// Each output address's digest and heads.
    pub outputs: BTreeMap<String, GeneratedOutput>,
    /// Why the derivation failed, when it did.
    pub failure: Option<DeriveFailure>,
    /// The toolchain the derivation ran under.
    #[serde(with = "crate::cbor::fixed")]
    pub toolchain_hash: [u8; 32],
    /// The runner that ran it, and the change's signer.
    pub runner: ParticipantKey,
    /// The runner's daemon.
    pub device: DeviceKey,
    /// The runner key's signature over the signing input.
    #[serde(with = "crate::cbor::fixed")]
    pub sig: [u8; 64],
    /// The device key's signature over the signing input.
    #[serde(with = "crate::cbor::fixed")]
    pub device_sig: [u8; 64],
}

/// One entry of a channel document.
///
/// It is a value of one of its genesis maps (`/attestations` in a
/// `ci`, and in `ci` also `/approvals` and
/// `/generations`; `/outcomes` and `/promotions` in a promote
/// channel), at the key [`ChannelEntry::key`] gives.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChannelEntry {
    Attestation(Attestation),
    Approval(Approval),
    /// A promote action's run outcome, in `/outcomes`.
    Outcome(PromoteOutcome),
    /// A promotion, in `/promotions`.
    Promotion(Promotion),
    /// A generation record, in `ci`'s `/generations`.
    Generation(Generation),
}

/// A channel entry's key in its map, written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct EntryId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// Put `entry` into the channel document (decisions 119 and 141).
///
/// It is committed by `origin` like an edit and answered like one:
/// `DocOutput::Changed` with `req`, or `DocOutput::Rejected` `Failed` when
/// the channel has no map for the entry (an approval or a generation
/// outside `ci`, an attestation in a promote channel, or an
/// outcome or promotion outside one), the entry's key
/// doesn't encode, its key holds another entry (an attestation of the same
/// frontier by the same runner with other bytes, such as another
/// device's), or the document is of another kind or closed. An entry is
/// its canonical CBOR as a byte string at its key ([`ChannelEntry::key`]
/// in `ID_ENCODING`) in its map, put only at a key
/// that holds nothing, so no put overwrites an entry; one the document
/// already holds at its key commits nothing and answers at the current
/// heads, so a re-put is a no-op. The caller checks it first (the shell
/// signs attestations and outcomes and checks approvals and promotions):
/// every peer's verifier refuses
/// a change whose entry would fail there.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelPut {
    pub req: RequestId,
    pub origin: Origin,
    pub entry: ChannelEntry,
}

/// The channel document's render (decision 119).
///
/// It comes after every change that adds entries and on
/// `DocEvent::Render`: the incarnation's first render
/// is `full`, listing every entry the document holds, and every later one
/// lists only the entries the changes since the previous render added, so
/// journals grow linearly. `added` lists every value at each key (Automerge
/// `get_all`, or each put a change made), so concurrent puts at one key
/// both appear, and a re-put appears again; `policy::counted` dedupes. A
/// full render lists the maps in genesis order (attestations, then
/// approvals, then generations; or outcomes, then promotions), each map by key; a later one
/// lists entries in the order the changes put them.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChannelRendered {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub added: Vec<ChannelEntry>,
    pub full: bool,
}

override

Source: crates/lvcs-doc/src/kinds/override/mod.rs.

/// An override's id (decisions 71 and 111; `OverrideClaim::id`, its one
/// implementation): the `HASH` of the canonical CBOR array (workspace id,
/// claimant key, base address, path, claim time), the ids and key as byte
/// strings, the base address and path as text, and the claim time as the
/// claiming request's `Origin.time_ms`. Written in `ID_ENCODING`. A workspace
/// actor's claims never overlap (see `Claim`); claims made concurrently on
/// different peers (M3) may, and then the view uses the override with the
/// smaller id, comparing the 32 bytes (not the `ID_ENCODING` text, whose
/// digits sort before its letters).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct OverrideId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// What part of its base an override replaces in the workspace view.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum OverrideScope {
    /// A range of the base document's `/content`, anchored inclusively
    /// (decisions 41, 76, and 83): base edits outside it flow through to the
    /// view and edits inside it are hidden. [`RangeAnchor::heads`] are the
    /// base heads when the range was claimed.
    Range(RangeAnchor),
    /// The whole base document, whatever it holds, claimed at `base_heads`:
    /// every base edit is hidden. A binary file is overridden this way,
    /// since it has no ranges (design §3).
    Whole {
        #[serde(with = "crate::cbor::heads")]
        base_heads: Vec<ChangeHash>,
    },
    /// No base: a file created only in this workspace, a whole-document
    /// override with an absent base (decision 71).
    Absent,
}

/// The claim an override was created for: written once, by
/// [`OverrideInit`], and never changed.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverrideClaim {
    /// For a `Range` or `Whole` scope, the address of the document that
    /// holds the claimed content in the workspace's base view: the `file`
    /// document at the path or, in a stacked workspace, an override
    /// document of a workspace it stacks on. For `Absent`, the path the
    /// file has in the workspace.
    pub base: String,
    /// The file path the claim resolves to in the workspace (decision 111),
    /// set by the workspace that claims it: `base` itself when `base` is the
    /// path's file document or the scope is `Absent`, and for a stacked
    /// claim the file path of the lower override `base` names, never that
    /// override's address. An override change, and the override's index
    /// entry, is authorized for `Write` on it (decisions 111 and 112), and
    /// every peer accepts a claim only with `base` equal to `path` or
    /// naming an override (`OVERRIDE_ADDRESS`).
    pub path: String,
    pub scope: OverrideScope,
    /// Who claimed it; with the workspace id, `base`, `path`, and
    /// `time_ms`, an input of the override id.
    pub claimant: ParticipantKey,
    /// The claiming request's `Origin.time_ms`.
    pub time_ms: i64,
}

/// Create an override's content at claim time (design §8): the first event
/// a new override document gets. It is idempotent (decision 97): an override
/// document already holding an equal init (its `/claim` holds `claim`, and
/// the change that wrote it is `origin.author`'s at `origin.time_ms / 1000`
/// seconds and wrote `content` and `executable`, read at that change's
/// heads) commits nothing and renders again, so an init re-sent after a
/// workspace actor's restart is answered by a render. One holding a
/// different init refuses it, and a file document ignores it: the event
/// has no answer, so both write and render nothing.
///
/// Layout: the genesis operations are the `file` kind's, in the same order
/// (`/content` an empty Text, `/meta` a map, then `format` = `text`,
/// `executable` = false, `deleted` = true), so an override document renders,
/// takes `Edit`, `DiskEdit`, `BundleEdit`, `ReadAt`, `WatchAnchors`, and
/// `Claim`, and answers like a file document. The init change, on top of genesis and committed by
/// `origin.author` at `origin.time_ms / 1000` seconds, puts `/claim` as a
/// byte string holding the canonical CBOR encoding of `claim`, then writes
/// `content` and `executable` the way an import does, deleting and
/// re-putting `/meta/format` and `/meta/deleted` (false unless `content` is
/// `Deleted`) (decision 76). `claim.time_ms` is `origin.time_ms`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverrideInit {
    pub origin: Origin,
    pub claim: OverrideClaim,
    /// What the base view holds in the claimed scope at claim time: the
    /// range's text, or the whole document's body. `Deleted` for an
    /// `Absent` scope: its first content comes with the edit or save that
    /// created the file.
    pub content: FileBody,
    pub executable: bool,
}

/// An override document's render, emitted where a file document emits
/// `DocOutput::Rendered`: after every applied change, once for every
/// `DiskEdit`, and on `Render`. `claim` is `None` while the document holds
/// only its genesis change, before its [`OverrideInit`] arrived or synced
/// (or when `/claim` holds no claim's encoding). `after` is as for
/// `DocOutput::Rendered`: the `stamp` of the `DiskEdit` whose handling
/// produced this render, `None` for any other render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverrideRendered {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub claim: Option<OverrideClaim>,
    pub body: FileBody,
    pub executable: bool,
    pub after: Option<u64>,
}

registry

Source: crates/lvcs-doc/src/kinds/registry/mod.rs.

/// One workspace as the registry lists it: what `ws.fork` created it with,
/// every input of its id ([`WorkspaceId::of`]) but its creator, who signs
/// the entry. A workspace whose name another entry also has mirrors to
/// `.lvcs/ws/<name>-<first 6 characters of its id>/` (design §8).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceEntry {
    pub name: String,
    pub base: WorkspaceBase,
    pub subtree: Option<String>,
    /// The forking request's `Origin.time_ms`, an input of the workspace id.
    pub created_ms: i64,
    /// The base's visible frontier id when the workspace was forked, an
    /// input of the workspace id (decisions 57 and 112).
    #[serde(with = "crate::cbor::fixed")]
    pub base_frontier: [u8; 32],
}

/// A merge record as the registry stores it: the canonical CBOR encoding
/// of an `lvcs_ws::merge::MergeRecord`, opaque here so `lvcs-doc` stays
/// independent of `lvcs-ws`, except for what a record's change is checked
/// against, which `lvcs-doc` reads from the record's `bundle`, `inputs`,
/// and `docs`: the change is authorized by `Write` on every document it
/// lists (decision 108), the keys of `docs`, the address of each; and it
/// is written only at the record's own key, by its merger: `bundle` is
/// its key in `/bundles` and the id of `inputs` ([`BundleId::of`]), and
/// `inputs.merger` signs the change. Serializes as a byte string.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct EncodedRecord(#[serde(with = "crate::cbor::bytes")] pub Vec<u8>);

/// The registry as its document holds it.
///
/// Layout: the genesis operations put `/workspaces` and `/bundles` as maps,
/// in that order, so every peer writes into the same two objects. A
/// workspace is the key `/workspaces/<ws-id>`, a map holding `name`, `base`
/// (`main`, or the base workspace id), and `subtree` (when there is one) as
/// strings, `created_ms` as an integer, and `base_frontier` as a 32-byte
/// byte string; a bundle is the key `/bundles/<bundle-id>`, holding its
/// [`EncodedRecord`] as a byte string. Ids in keys and values are in
/// `ID_ENCODING`. Each key is written once, by the peer that created the
/// workspace or merged the bundle: sync and the audit refuse a change that
/// writes a key holding a value at the change's dependencies, whatever
/// predecessors it names. A bundle's key is its merger's alone (see
/// [`EncodedRecord`]), and a workspace entry's key its creator's: sync and
/// the audit accept an entry only under the key
/// `WorkspaceId::of(name, signer, base_frontier, created_ms)` of its own
/// fields and its change's signer (decision 112), in a change that writes
/// nothing else, so an entry written at older dependencies, without seeing
/// an earlier one, names a workspace of its own signer's and never stands
/// beside another participant's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Registry {
    pub workspaces: BTreeMap<WorkspaceId, WorkspaceEntry>,
    pub bundles: BTreeMap<BundleId, EncodedRecord>,
}

/// Record a new workspace (design §8), sent once by the forking daemon,
/// committed by `origin.author` at `origin.time_ms / 1000` seconds on the
/// live document, and signed by the workspace's creator: `id` is the
/// [`WorkspaceId::of`] the entry's fields and its creator's key, the only
/// key a peer accepts the entry at (decision 112). A registry that already
/// lists the id ignores it: each key is written once.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisterWorkspace {
    pub origin: Origin,
    pub id: WorkspaceId,
    pub entry: WorkspaceEntry,
}

/// Record a merge bundle's merge record (decision 48), sent once by the
/// merging workspace actor after every change of the bundle is applied,
/// committed by `origin.author` at `origin.time_ms / 1000` seconds on the
/// live document. Signed like every change (ChangeSig, M3), by the record's
/// merger (`inputs.merger`), the only signer whose record change a peer
/// accepts at `bundle`. A registry that already holds the bundle's record
/// ignores it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordBundle {
    pub origin: Origin,
    pub bundle: BundleId,
    pub record: EncodedRecord,
}

/// The registry document's state at `heads`, emitted after every applied
/// change and on `Render`. An entry whose id or fields are malformed is
/// left out.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryRendered {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub registry: Registry,
}

thread

Source: crates/lvcs-doc/src/kinds/thread/mod.rs.

/// A thread's id: the `HASH` of the canonical CBOR array (anchor, opener
/// key, open time): the anchor as a nested [`ThreadAnchor`] value (not a
/// byte string), the key as a byte string, and the open time as the opening
/// request's `Origin.time_ms`. Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ThreadId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// A message's id: the `HASH` of the message's canonical CBOR encoding, so
/// an answer can name the ask it replies to. Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MessageId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// What a thread is about (design §9). Fixed when the thread opens.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ThreadAnchor {
    /// An address, `path#pointer` or a whole document (design §4).
    Address(String),
    /// A workspace.
    Workspace(WorkspaceId),
    /// A set of participants: a direct conversation.
    Participants(BTreeSet<ParticipantKey>),
}

/// The kind of a stored message (design §9). The set is open: later kinds
/// (navigator `review`, `accept`, `dismiss`, §10) need no major version, so
/// a reader skips a stored message whose kind it doesn't know. `presence`
/// and `typing` are never stored.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MessageKind {
    Say,
    /// Expects an `answer` and stays open until one arrives. Addressed to
    /// the thread's `to` (see [`ThreadOpen`]), or to anyone subscribed when
    /// `to` is empty.
    Ask,
    /// Replies to the ask `ask` in the same thread.
    Answer {
        ask: MessageId,
    },
}

/// One message of a thread.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadMessage {
    pub author: ParticipantKey,
    /// The sending request's `Origin.time_ms`.
    pub time_ms: i64,
    pub kind: MessageKind,
    pub text: String,
}

/// Open a thread: write its anchor, its addressees, and its first message
/// (design §9), committed by `origin.author` at `origin.time_ms / 1000`
/// seconds: the first event a new thread document gets; a thread already
/// open, and a document of another kind, ignore it. `first.author` is the
/// opener of the thread id. A merge-back conflict thread is anchored on the
/// conflicting base document's address and addressed to both authors
/// (design §8).
///
/// Layout: the genesis operation puts `/messages` as a list, so every
/// participant appends to the same object. Opening puts `/anchor` as a byte
/// string holding the canonical CBOR encoding of `anchor` and `/to` as a
/// list of the addressees' keys in `ID_ENCODING`, sorted by key bytes (the
/// order of `to`, not that of the encoded text, whose digits sort before its
/// letters), then appends `first` like a [`ThreadAppend`], all in one
/// change. Each message is a map at the end of `/messages` with `id`,
/// `author`, `kind` (`say`, `ask`, or `answer`), and, for an answer, `ask`
/// as strings (ids and keys in `ID_ENCODING`), `time_ms` as an integer, and
/// `text` as a string.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadOpen {
    pub origin: Origin,
    pub anchor: ThreadAnchor,
    /// Who the thread is addressed to: the participants its asks expect an
    /// answer from, and whose inboxes it reaches (M3). Fixed when the thread
    /// opens.
    pub to: BTreeSet<ParticipantKey>,
    pub first: ThreadMessage,
}

/// Append a message to an open thread (design §9), committed by
/// `origin.author` at `origin.time_ms / 1000` seconds; a thread not yet
/// open, and a document of another kind, ignore it. Concurrent appends both
/// land, ordered by Automerge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadAppend {
    pub origin: Origin,
    pub message: ThreadMessage,
}

workspace-index

Source: crates/lvcs-doc/src/kinds/workspace/mod.rs.

/// A workspace's id (design §8, decisions 57 and 112): the `HASH` of the
/// canonical CBOR array (name, creator key, base frontier id, creation
/// time), the name as text, the key and frontier id as byte strings, and
/// the creation time as the forking request's `Origin.time_ms`; its one
/// implementation is `WorkspaceId::of`. Written in `ID_ENCODING`. `main`
/// has none.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WorkspaceId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);

/// The workspace a workspace was forked from: its base view. Workspaces
/// stack (`main` → `auth-refactor` → `agent-scratch`), so a base may be
/// another workspace's view.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkspaceBase {
    /// The shared live `main` workspace: the `file` documents themselves.
    Main,
    Workspace(WorkspaceId),
}

/// A workspace index as its document holds it (design §8): every lookup it
/// doesn't answer falls through to the base view.
///
/// Layout: the genesis operations put `/overrides`, `/whiteouts`, and
/// `/local` as maps, in that order, so every participant writes into the
/// same three objects. [`IndexInit`] puts `/name` and `/base` (`main`, or
/// the base workspace id in `ID_ENCODING`) as strings, `/subtree` as a
/// string when there is one, `/created_ms` as an integer, and
/// `/base_frontier` as a 32-byte byte string: with the init's signer, the
/// inputs of the workspace id ([`WorkspaceId::of`]). An override is the key
/// `/overrides/<id>` and a workspace-local file `/local/<id>`, the override
/// id in `ID_ENCODING`, each holding a byte string, the canonical CBOR
/// encoding of the override's [`OverrideClaim`] (an `Absent` claim under
/// `/local`, any other under `/overrides`): the key is that claim's id in
/// this workspace (`OverrideClaim::id`), so concurrent claims never write
/// the same key, and the key commits to the claim's base and `path`, which
/// every peer reads from the value and checks against it (decision 112).
/// The index reads only the claim's `base` and `path`; the override
/// document's own claim is what a view composes by. A whiteout is the key
/// `/whiteouts/<path>` holding a byte string, the canonical CBOR encoding
/// of its [`WhiteoutEntry`] (decision 86). Removing an entry deletes its
/// key, so a concurrent re-add survives.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceIndex {
    pub name: String,
    pub base: WorkspaceBase,
    /// The path prefix the workspace was forked with (`ws fork --subtree`):
    /// its view holds only paths under it. `None`: the whole project.
    pub subtree: Option<String>,
    /// Range and whole-document overrides, by base address: the document
    /// whose content each claims (see `OverrideClaim.base`).
    pub overrides: BTreeMap<String, BTreeSet<OverrideId>>,
    /// Paths deleted in this workspace though the base view has them, each
    /// with what its whiteout recorded.
    pub whiteouts: BTreeMap<String, WhiteoutEntry>,
    /// Files created only in this workspace, by path: each an override with
    /// an absent base. More than one id at a path means concurrent
    /// creations; the view uses the smallest.
    pub local: BTreeMap<String, BTreeSet<OverrideId>>,
}

/// What a whiteout records (decision 86): the heads of the document that
/// held the path in the base view when it was whited out, which merge-back
/// deletes the file at only while the base is still at them, and the
/// participant who placed it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WhiteoutEntry {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub by: ParticipantKey,
}

/// Write a new index's name, base, subtree, creation time, and base
/// frontier (design §8, decision 112): the first event a new index document
/// gets, sent once by the forking daemon, committed by `origin.author` at
/// `origin.time_ms / 1000` seconds on the live document (an index's writes
/// have no base: each writes keys of its own). `/created_ms` is
/// `origin.time_ms`, its one source, and `base_frontier` the base's visible
/// frontier id when the workspace was forked, so the index holds every
/// input of its workspace's id but the creator, who signs the init. A peer
/// accepts the init only in the index of the workspace
/// `WorkspaceId::of(name, signer, base_frontier, created_ms)` names, in a
/// change that writes nothing else (decision 112). An index that already
/// has a name ignores it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexInit {
    pub origin: Origin,
    pub name: String,
    pub base: WorkspaceBase,
    pub subtree: Option<String>,
    #[serde(with = "crate::cbor::fixed")]
    pub base_frontier: [u8; 32],
}

/// Change one entry of an index (design §8), committed by `origin.author` at
/// `origin.time_ms / 1000` seconds on the live document. An edit that
/// changes nothing (an entry added that is already there with that claim, a
/// whiteout with the same heads and participant included, or one removed
/// that isn't there) makes no commit and no render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexChange {
    pub origin: Origin,
    pub edit: IndexEdit,
}

/// One index entry change (layout on [`WorkspaceIndex`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum IndexEdit {
    /// Record the range or whole-document override `id` of `claim.base`:
    /// `id` is `claim.id` of this workspace, and the scope isn't `Absent`.
    AddOverride {
        claim: OverrideClaim,
        id: OverrideId,
    },
    /// Retire an override from the view.
    RemoveOverride { id: OverrideId },
    /// Delete `path` in this workspace: `heads` are those of the document
    /// that holds it in the base view, and `by` the participant deleting
    /// it, as its [`WhiteoutEntry`] records them.
    Whiteout {
        path: String,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        by: ParticipantKey,
    },
    /// Undo a whiteout of `path`.
    ClearWhiteout { path: String },
    /// Record the file `id` created only in this workspace at `claim.path`:
    /// `id` is `claim.id` of this workspace, and the scope is `Absent`.
    AddLocal {
        claim: OverrideClaim,
        id: OverrideId,
    },
    /// Drop a workspace-local file.
    RemoveLocal { id: OverrideId },
}

/// An index document's state at `heads`, emitted after every applied change
/// and on `Render`, for the workspace actor. `index` is `None` while the
/// document holds only its genesis change, before its [`IndexInit`] arrived
/// or synced, or when `/base` names no workspace; an entry whose id or
/// value is malformed is left out.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexRendered {
    #[serde(with = "crate::cbor::heads")]
    pub heads: Vec<ChangeHash>,
    pub index: Option<WorkspaceIndex>,
}

lines

Source: crates/lvcs-doc/src/lines.rs.

/// A range as a reader is shown it (decisions 156 and 160).
///
/// `first` and `last` are the 1-based numbers of its first and last lines in
/// the text it was named in, inclusive. `near` is git's default hunk-header
/// line, taken at or above `first`: the nearest line whose first character
/// is an ASCII letter, `_`, or `$`, without its newline and trailing
/// whitespace, cut to `RANGE_CONTEXT_MAX_CHARS` code points. It is a
/// heuristic line, never a parse, so it can name a line outside the
/// enclosing definition. `Display` writes `line 40` or `lines 40\u{2013}52`,
/// then `, near "<near>"` when there is one; a surface that doesn't already
/// name the file puts the path first.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LineName {
    /// The range's first line, 1-based.
    pub first: usize,
    /// Its last line, 1-based and inclusive.
    pub last: usize,
    /// The hunk-header line at or above `first`, if any.
    pub near: Option<String>,
}

/// Names the code points `start..end` of `text`.
///
/// A range runs from the line holding `start` to the line holding
/// `end - 1`. An empty range names the line
/// holding the character after it, at the text's end the last line, and in
/// an empty text line 1. `None` when the range ends past the text or
/// before it starts.
#[must_use]
pub fn name(text: &str, start: usize, end: usize) -> Option<LineName> {
    let chars: Vec<char> = text.chars().collect();
    name_chars(&chars, start, end)
}

/// Names `spans` of `text` (code points) as a reader is shown them.
///
/// They are sorted, with spans that touch or overlap (`a.end >= b.start`) joined, and each
/// joined run named as [`name`] does. A run that ends past the text is left
/// out.
#[must_use]
pub fn names(text: &str, spans: &[Range<usize>]) -> Vec<LineName> {
    let chars: Vec<char> = text.chars().collect();
    let mut sorted = spans.to_vec();
    sorted.sort_unstable_by_key(|span| (span.start, span.end));
    let mut runs: Vec<Range<usize>> = Vec::with_capacity(sorted.len());
    for span in sorted {
        match runs.last_mut() {
            Some(run) if run.end >= span.start => run.end = run.end.max(span.end),
            _ => runs.push(span),
        }
    }
    runs.into_iter()
        .filter_map(|run| name_chars(&chars, run.start, run.end))
        .collect()
}

paths

Source: crates/lvcs-doc/src/paths.rs.

/// A set of gitignore-syntax patterns, matched against document addresses
/// (design §4). The lines keep their gitignore meaning: the last line that
/// matches the address, or a directory above it, decides, so a `!` line
/// re-includes what an earlier line matched, and blank and `#` lines match
/// nothing. With `case_insensitive`, ASCII letters match either case and
/// every other character matches only itself (ASCII folding, decision 122),
/// as a case-insensitive filesystem resolves `BUILD.RS` to `build.rs`.
#[derive(Debug, Clone)]
pub struct PathSet {
    matcher: Gitignore,
}

/// Why a [`PathSet`] didn't build.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSetError {
    /// A line gitignore syntax refuses, with the reason.
    Pattern { pattern: String, reason: String },
    /// Every line parsed, but the set didn't compile.
    Build { reason: String },
}

impl PathSet {
    /// The set of `patterns`, one gitignore line each, in order.
    pub fn new<I>(patterns: I, case_insensitive: bool) -> Result<Self, PathSetError>
    where
        I: IntoIterator,
        I::Item: AsRef<str>,
    {
        let build = |err: ignore::Error| PathSetError::Build {
            reason: err.to_string(),
        };
        let mut builder = GitignoreBuilder::new("");
        // Before any line: the setting applies to lines added after it.
        builder.case_insensitive(case_insensitive).map_err(build)?;
        for pattern in patterns {
            let pattern = pattern.as_ref();
            builder
                .add_line(None, pattern)
                .map_err(|err| PathSetError::Pattern {
                    pattern: pattern.to_owned(),
                    reason: err.to_string(),
                })?;
        }
        let matcher = builder.build().map_err(build)?;
        Ok(Self { matcher })
    }

    /// Whether the set matches the file at `address`: a line matches it or
    /// a directory above it, and no later `!` line re-includes it. An
    /// address that isn't a plain relative path, one whose every
    /// `/`-separated segment is a name (not empty, `.`, or `..`), matches
    /// nothing; no valid address (design §4) is one.
    pub fn matches(&self, address: &str) -> bool {
        let plain = address
            .split('/')
            .all(|segment| !matches!(segment, "" | "." | ".."));
        plain
            && self
                .matcher
                .matched_path_or_any_parents(Path::new(address), false)
                .is_ignore()
    }
}

/// `DEFAULT_PROTECTED` as one case-insensitive [`PathSet`], built once:
/// the protected-path rule (decision 122).
pub fn protected() -> &'static PathSet {
    static PROTECTED: LazyLock<PathSet> = LazyLock::new(|| {
        PathSet::new(DEFAULT_PROTECTED.iter().map(|rule| rule.pattern), true)
            .expect("DEFAULT_PROTECTED is valid gitignore syntax")
    });
    &PROTECTED
}

router

Source: crates/lvcs-doc/src/router.rs.

/// A subscription, chosen by the daemon shell, unique per daemon.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SubId(pub u64);

/// Mailbox events of the router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouterEvent {
    /// A request names `address`; the router spawns its document actor if
    /// none is running.
    Open {
        address: String,
    },
    /// Deliver changes for an exact address or a `**` prefix pattern
    /// (gitignore syntax: `src/auth/**` matches every address below
    /// `src/auth`, `**` every address), replacing any earlier pattern of
    /// `sub`. An exact address also spawns its document actor if none is
    /// running (design §2: actors start on first `open`, `subscribe`, or
    /// `DiskEdit`); a prefix pattern spawns nothing. The shell validates
    /// `pattern`; one the router cannot parse (empty, an empty, `.`, or `..`
    /// segment, `#`, or glob syntax other than a trailing `/**`) changes
    /// nothing.
    Subscribe {
        sub: SubId,
        pattern: String,
    },
    Unsubscribe {
        sub: SubId,
    },
    /// A document actor applied a change.
    Changed {
        address: String,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        splices: Vec<Splice>,
    },
    /// The shell stopped the actor for `address` (idle or closed).
    Stopped {
        address: String,
    },
}

/// Outputs of the router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum RouterOutput {
    /// Spawn the document actor for `address` (next incarnation).
    Spawn { address: String },
    /// Push a change to one subscriber. A change produces one `Deliver` per
    /// matching subscription, in `SubId` order.
    Deliver {
        sub: SubId,
        address: String,
        #[serde(with = "crate::cbor::heads")]
        heads: Vec<ChangeHash>,
        splices: Vec<Splice>,
    },
}

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
/// `SyncEvent::Outgoing`.
#[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 `SyncEvent::Settled`, 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,
}

/// 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>,
}

coord-actor

Source: crates/lvcs-net/src/coord/machine.rs.

/// The coord actor of a daemon (`KIND` = `coord`), spawned once per daemon
/// run.
///
/// It is empty and `Unconfigured` at start: the shell sends
/// [`CoordEvent::Config`] first in every incarnation (decision 111). Within
/// a run, a restarted one rebuilds from a fresh `new` by replaying its own
/// incarnation's journal entries, `Config` first, effects discarded
/// (decisions 56 and 107).
pub type CoordActor = CoordinationStateMachine<Coordinator>;

coord

Source: crates/lvcs-net/src/coord/mod.rs.

/// `lvcs.toml` `[coord]` (design §5).
///
/// `wait_ms` is the wait window, `WAIT_WINDOW_MS` by default; the daemon's
/// loader caps it at `WAIT_WINDOW_MAX_MS`, so 0 to that bound is accepted,
/// and the shell sends it to the coord actor as [`CoordEvent::Config`].
/// Owner: `M3.coord`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct CoordConfig {
    /// The wait window, in milliseconds.
    pub wait_ms: u64,
}

/// A notification's id.
///
/// It is the `HASH` of its [`Notification`] payload's canonical CBOR
/// encoding ([`NotificationId::of`]), written in `ID_ENCODING` on the API;
/// the last key of the yield order. Owner: `M3.coord`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NotificationId(#[serde(with = "crate::wire::cbor::fixed")] pub [u8; 32]);

/// A notification's range resolved by this daemon's document actor
/// (`DocEvent::Resolve`) to code points of the document's text at its
/// current heads.
///
/// Overlap between notifications is decided on these spans (chosen at the
/// M3 freeze; the cursor pair itself is opaque to `lvcs-net`). Two spans
/// overlap when each starts before the other ends. A whole-document
/// notification (an empty `Notification::range`) has the span
/// [`Span::WHOLE`], `0..usize::MAX`, which overlaps every span, so it needs
/// no resolution. A span meets another when they overlap or touch (one
/// ends where the other starts): a save is announced again only when no
/// current notification of its author meets its span. Owner: `M3.coord`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
    /// Where the span starts, in code points (inclusive).
    pub start: usize,
    /// Where the span ends, in code points (exclusive).
    pub end: usize,
}

/// Whether a response is a `note` or a `hold` (design §5).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResponseKind {
    /// Information only: the notifier proceeds.
    Note,
    /// An objection: the notifier's edit pauses, and a thread opens on the
    /// range (design §9).
    Hold,
}

/// One response as the coord actor keeps it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Responded {
    /// Whether it is a `note` or a `hold`.
    pub kind: ResponseKind,
    /// The response payload.
    pub response: Response,
}

/// One head of a document (an Automerge change hash's bytes), opaque to
/// `lvcs-net`. Owner: `M3.coord`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Head(#[serde(with = "crate::wire::cbor::fixed")] pub [u8; 32]);

/// A committed save of a document through a main mirror, by the mirror's
/// author, which holds the message capability on it (decision 165).
///
/// `range` is the canonical CBOR encoding of the anchor the save's touched
/// lines were anchored to at `heads` (empty for a whole document, a
/// removal, or a binary save, whose span is [`Span::WHOLE`]), opaque here
/// like [`Notification::range`]; `span` is that range resolved at `heads`.
/// `op` and `what` are what the notification says. Owner: `M3.coord`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Saved {
    /// The document saved.
    pub doc: String,
    /// The saving participant's key bytes: the mirror's author.
    #[serde(with = "crate::wire::cbor::fixed")]
    pub by: [u8; 32],
    /// The canonical CBOR encoding of the touched lines' anchor; empty for
    /// the whole document.
    #[serde(with = "crate::wire::cbor::bytes")]
    pub range: Vec<u8>,
    /// The touched lines resolved at `heads`.
    pub span: Span,
    /// The document's heads after the save.
    pub heads: Vec<Head>,
    /// What the save did to the range.
    pub op: NotifyOp,
    /// What the save is, in generated plain text.
    pub what: String,
}

/// A local participant's save, seen at `now_ms` (decision 165): the event
/// of [`CoordEvent::SaveSeen`]. Owner: `M3.coord`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SaveSeen {
    /// The save.
    pub saved: Saved,
    /// The shell's clock.
    pub now_ms: i64,
}

/// Mailbox events of the coord actor. `now_ms` is the shell's clock (Unix
/// milliseconds). Owner: `M3.coord`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CoordEvent {
    /// The wait window, `lvcs.toml` `[coord] wait_ms` ([`CoordConfig`]):
    /// the first event of every incarnation, sent by the shell and
    /// journaled like every event (decision 111), so a rebuilt or replayed
    /// actor reaches the same window. The actor caps it at
    /// `WAIT_WINDOW_MAX_MS`. Until it arrives the actor is `Unconfigured`
    /// and takes no part in coordination: an `announce` is answered `Proceed`
    /// with no notes and is neither published nor kept, a bulletin request
    /// gets an empty bulletin, and every other event is dropped. A later
    /// one changes nothing: the window is fixed for an incarnation.
    Config {
        /// The wait window, in milliseconds.
        window_ms: u64,
    },
    /// A local participant notifies (op `announce`): `req` is the daemon's
    /// request id, answered by exactly one [`CoordOutput::Decided`];
    /// `notification` is the payload the daemon built, its range anchored
    /// at the participant's base (`DocEvent::Anchor`); `span` is that range
    /// resolved locally.
    Notify {
        /// The daemon's request id.
        req: u64,
        /// The payload the daemon built.
        notification: Notification,
        /// Its range resolved locally.
        span: Span,
        /// The shell's clock.
        now_ms: i64,
    },
    /// A validated notification from another daemon (`GossipOutput::Deliver`
    /// of kind `notification`), its range resolved locally: `span` is
    /// `None` while this daemon's copy of the document lacks the anchors.
    Received {
        /// The notification.
        notification: Notification,
        /// Its range resolved locally; `None` while the anchors are unknown.
        span: Option<Span>,
        /// The shell's clock.
        now_ms: i64,
    },
    /// A `note` or `hold`, local (ops `note` and `hold`) or received
    /// (kinds `note` and `hold`).
    Response {
        /// The response.
        responded: Responded,
        /// Whether a local participant made it.
        local: bool,
        /// The shell's clock.
        now_ms: i64,
    },
    /// `presence.by` is on `presence.doc` (or in `presence.workspace`) as
    /// of `presence.ts`, and counts as present there until
    /// `PRESENCE_TTL_SECS` after: a validated `presence` message
    /// (`GossipOutput::Deliver`), or, with `local`, one of this daemon's
    /// participants, which the daemon reports on `open`, `subscribe`, and
    /// every edit (an edit counts as presence, design §5) with its own
    /// clock as `ts`. `may_publish` is the shell's scope verdict on a local
    /// presence (grants stay in the shell, decision 111): its participant
    /// holds the message capability on the document, so the actor may have
    /// the presence published ([`CoordOutput::PublishPresence`]). It is
    /// false for a received presence.
    Presence {
        /// The presence.
        presence: Presence,
        /// Whether it is a local participant's.
        local: bool,
        /// The shell's scope verdict on a local presence.
        may_publish: bool,
    },
    /// The wait window of the local notification `notification` ends: the
    /// shell's timer, armed by [`CoordOutput::Wake`].
    Elapsed {
        /// The local notification whose window ends.
        notification: NotificationId,
        /// The shell's clock.
        now_ms: i64,
    },
    /// A local participant's committed save through a main mirror
    /// (decision 165). It is announced ([`CoordOutput::AutoPublish`]) when
    /// no current notification of its author on the document (one still
    /// waiting, or within `BULLETIN_SECS`: a client's `announce` or
    /// `track_edit`, or an earlier automatic one) meets its span;
    /// otherwise it changes nothing. It never opens a wait window and is
    /// never answered.
    SaveSeen(SaveSeen),
    /// Read the bulletin of `doc` (the session-start delivery of design §5);
    /// answered by [`CoordOutput::Bulletin`].
    Bulletin {
        /// The daemon's request id.
        req: u64,
        /// The document's address.
        doc: String,
        /// The shell's clock.
        now_ms: i64,
    },
}

/// How a local notification's wait ended (design §5, decision 51).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
    /// No one else was present, or the window ended with no hold and no
    /// earlier overlapping notification: the edit proceeds. `notes` are
    /// the notes received meanwhile.
    Proceed {
        /// The notes received meanwhile.
        notes: Vec<Response>,
    },
    /// At least one `hold` arrived: the edit pauses, and each hold names
    /// the thread its holder opened.
    Hold {
        /// The holds received.
        holds: Vec<Response>,
        /// The notes received meanwhile.
        notes: Vec<Response>,
    },
    /// An overlapping notification earlier in the yield order was seen
    /// during the window: the participant yields and turns its edit into
    /// an `ask` to that notification's sender (advisory).
    Yield {
        /// The earlier overlapping notification.
        to: Notification,
        /// The notes received meanwhile.
        notes: Vec<Response>,
    },
}

/// Outputs of the coord actor, which the daemon shell carries out.
/// Owner: `M3.coord`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CoordOutput {
    /// Seal `notification` with its participant's key and publish it on
    /// the document's topic and every ancestor directory's topic.
    Publish(Notification),
    /// Seal and publish a local response on the notifier's inbox topic and
    /// the document's topic.
    Respond(Responded),
    /// Publish a local participant's presence on its document's topic and
    /// every ancestor directory's topic, with a token from the
    /// participant's gossip bucket: its first local presence on the
    /// document in this incarnation, then one at least a third of
    /// `PRESENCE_TTL_SECS` after the last one published, so other daemons
    /// count it present without a gap.
    PublishPresence(Presence),
    /// Send [`CoordEvent::Elapsed`] for `notification` at `at_ms`.
    Wake {
        /// The local notification whose window ends.
        notification: NotificationId,
        /// When, by the shell's clock.
        at_ms: i64,
    },
    /// Answer the `announce` request `req`.
    Decided {
        /// The request answered.
        req: u64,
        /// The notification's id.
        notification: NotificationId,
        /// How its wait ended.
        verdict: Verdict,
    },
    /// Push a received notification to this daemon's clients.
    Announce {
        /// The notification's id.
        id: NotificationId,
        /// The notification.
        notification: Notification,
    },
    /// Push a response to the clients of its notification's sender.
    Forward(Responded),
    /// Seal `notification`, an automatic announcement of a local save
    /// (`auto` true), with its participant's key and publish it like
    /// [`CoordOutput::Publish`], with a token from the participant's gossip
    /// bucket, since no request took one. `span` is its range resolved at
    /// `heads`, the save's heads.
    AutoPublish {
        /// The automatic notification.
        notification: Notification,
        /// Its range resolved at `heads`.
        span: Span,
        /// The document's heads after the save.
        heads: Vec<Head>,
    },
    /// Answer the bulletin request `req`: the document's notifications from
    /// the last `BULLETIN_SECS`, oldest first.
    Bulletin {
        /// The request answered.
        req: u64,
        /// The document's recent notifications, oldest first.
        notifications: Vec<Notification>,
    },
}

gossip

Source: crates/lvcs-net/src/gossip/mod.rs.

/// What the gossip actor knows of a signer that holds a live `Message`
/// grant, a live grant to attest `ci`, or a live grant to
/// attest a promote channel (design §11, §15; decisions 111, 146,
/// 203, and 206).
///
/// It is `gossip`, the rate its gossip bucket refills at, and the windows
/// of the three kinds of grant that admit gossip: `message`, from its
/// `Message` grants, admits every kind but `check_failed` and
/// `check_passed`; `attest`, from its grants to attest `ci`,
/// admits those two only (decision 203); and `promote`, from its grants to
/// attest a promote channel, one window per such channel's document
/// address, each admitting `presence` only, and only when the presence's
/// `doc` is that address (decision 206). `message` and `attest` are `None`
/// and `promote` empty when the signer holds no such grant. The daemon
/// shell derives it from the `.lvcs/caps` document's render
/// (`lvcs_doc::CapsRendered`): every signer that holds any of them there,
/// the owner included with `message` and `attest`, so `lvcs-net` never
/// names a grant (chosen at the M3 freeze). Every signer's `gossip` is the
/// limits in force's ([`Limits::gossip_rate`]), its per-second rate
/// replaced by the signer's grant terms where they name one (decision
/// 146), so its `burst` is always `gossip_burst`, the capacity the gossip
/// actor sizes its memory and tolerance by. The gossip actor drops a
/// message whose signer is not in the table, whose kind its windows don't
/// admit, or whose kind's window has ended by the shell's clock, before
/// forwarding it; whether a grant's paths cover the message's document is
/// checked only before delivery ([`GossipOutput::Deliver`]). Owner:
/// `M3.gossip`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignerCap {
    /// The rate the signer's gossip bucket refills at.
    pub gossip: Rate,
    /// The window of its `Message` grants, which admits every kind but
    /// the check notices; `None` when it holds none.
    pub message: Option<Window>,
    /// The window of its grants to attest `ci`, which admits
    /// `check_failed` and `check_passed` only; `None` when it holds none.
    pub attest: Option<Window>,
    /// The window of its grants to attest each promote channel, by
    /// that channel document's address, each admitting `presence` about
    /// that document only; empty when it holds none.
    pub promote: BTreeMap<String, Window>,
}

/// How long a kind of grant admits a signer's gossip ([`SignerCap`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Window {
    /// The end of the latest window among those grants, in Unix seconds;
    /// `None` for a grant with no expiry.
    pub until_s: Option<i64>,
}

/// Mailbox events of the gossip actor. Time enters only as event data.
/// Owner: `M3.gossip`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GossipEvent {
    /// The limits in force (`lvcs.toml` `[limits]`; decisions 111 and
    /// 146): `gossip_burst`, every gossip bucket's capacity, with each
    /// signer's rate from the signer table, sets how many of a signer's
    /// accepted envelopes are remembered (`GOSSIP_FRESH_MS` sets how long;
    /// decision 112) and when a forwarder is penalized. The shell sends it
    /// first in each incarnation, before any other event; a later one
    /// replaces the limits in force. Until the first one the actor is
    /// `Unconfigured` and drops every other event, so a message received
    /// before it gets no verdict.
    Limits(Limits),
    /// Join `topic`; the daemon formats it from a `TOPIC_*` constant.
    Subscribe {
        /// The topic to join.
        topic: String,
    },
    /// Leave `topic`.
    Unsubscribe {
        /// The topic to leave.
        topic: String,
    },
    /// Publish `envelope`, already sealed and signed by the daemon for one
    /// of its participants, on each of `topics` (a document's topic and its
    /// ancestors', for instance).
    Publish {
        /// The topics to publish on.
        topics: Vec<String>,
        /// The sealed, signed envelope.
        #[serde(with = "crate::wire::cbor::bytes")]
        envelope: Vec<u8>,
    },
    /// A message waiting for validation (`TransportEvent::Gossip`), with
    /// the shell's clock as `now_ms`: a payload whose signed `ts` is more
    /// than `GOSSIP_FRESH_MS` from it is ignored `stale`, and a copy of a
    /// remembered envelope (accepted, or with a take outstanding) on a
    /// topic it already came on is ignored `duplicate`, with no take and no
    /// delivery (decision 112).
    Received {
        /// Gossipsub's message id.
        #[serde(with = "crate::wire::cbor::bytes")]
        id: Vec<u8>,
        /// The peer that forwarded it.
        propagation: Peer,
        /// The topic it came on.
        topic: String,
        /// The encoded envelope.
        #[serde(with = "crate::wire::cbor::bytes")]
        data: Vec<u8>,
        /// The shell's clock, in Unix milliseconds.
        now_ms: i64,
    },
    /// The shell's answer to [`GossipOutput::Admit`] for the message `id`.
    Admission {
        /// The message's id.
        #[serde(with = "crate::wire::cbor::bytes")]
        id: Vec<u8>,
        /// The answer.
        admission: Admission,
    },
    /// Every signer that holds `Capability::Message`, a grant to attest
    /// `ci`, or a grant to attest a promote channel
    /// ([`SignerCap`]), replacing the previous table, sent whenever the
    /// caps document renders.
    Signers {
        /// Each signer that holds `Capability::Message`, a grant to attest
        /// `ci`, or a grant to attest a promote channel.
        signers: BTreeMap<Signer, SignerCap>,
    },
    /// A connection to `peer` is up (`TransportEvent::Connected`): its
    /// offenses are counted from now on. The shell sends it, and
    /// [`GossipEvent::Gone`], in the order the transport reported them
    /// among the gossip messages.
    Connected {
        /// The connected peer.
        peer: Peer,
    },
    /// The last connection to `peer` closed (`TransportEvent::Gone`): its
    /// offenses are forgotten, so the gossip actor keeps them only for
    /// connected peers.
    Gone {
        /// The peer whose last connection closed.
        peer: Peer,
    },
}

/// The answer to the take of [`GossipOutput::Admit`]. Owner: `M3.gossip`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Admission {
    /// The signer's bucket had a token (`GrantsOutput::Taken` with
    /// `allowed`): accepted.
    Admitted,
    /// The signer's bucket was empty: ignored `rate_limited`.
    RateLimited,
}

/// Outputs of the gossip actor, which the daemon shell carries out.
/// Owner: `M3.gossip`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GossipOutput {
    /// Join a topic (`TransportCommand::Subscribe`).
    Subscribe {
        /// The topic to join.
        topic: String,
    },
    /// Leave a topic (`TransportCommand::Unsubscribe`).
    Unsubscribe {
        /// The topic to leave.
        topic: String,
    },
    /// Publish `data` on `topic` (`TransportCommand::Publish`).
    Publish {
        /// The topic to publish on.
        topic: String,
        /// The encoded envelope.
        data: Vec<u8>,
    },
    /// Admit the message `id`, which passed every other check (design §9,
    /// §15): the shell takes a token from `signer`'s gossip bucket at
    /// `rate`, the signer table's (`GrantsEvent::Take` with
    /// `TakeKey::Gossip(id)` at `now_ms`), and answers with exactly one
    /// [`GossipEvent::Admission`].
    Admit {
        /// The message's id.
        id: Vec<u8>,
        /// Its signer.
        signer: Signer,
        /// The signer's gossip rate, from the signer table.
        rate: Rate,
        /// The shell's clock when the message was received, in Unix
        /// milliseconds.
        now_ms: i64,
    },
    /// Report the validation of the message `id`
    /// (`TransportCommand::Validate`); `reason` is set for a rejected or
    /// ignored message, for the span's `rejection` attribute.
    Validate {
        /// The message's id.
        id: Vec<u8>,
        /// The peer that forwarded it.
        propagation: Peer,
        /// The verdict.
        acceptance: Acceptance,
        /// Why it was rejected or ignored; `None` when accepted.
        reason: Option<Rejection>,
    },
    /// An accepted message for this daemon (design §9). `address` is the
    /// path of the document the message is about: a notification's or
    /// typing's `doc`, presence's `doc`, or a thread post's `thread`, each
    /// without its `#pointer`; `None` for a response (`note`, `hold`) and
    /// for presence in a workspace. Before it delivers the message to local
    /// clients, the shell checks its scope (decision 111): the subject
    /// document is `address`, or for a response the document of the
    /// notification it answers, and a response whose notification this
    /// daemon doesn't hold is dropped; `lvcs_doc::authorize`, with the caps
    /// render's grants, must accept `signer` doing `Capability::Message` on
    /// the subject document at the shell's clock in whole Unix seconds
    /// (rounded down) under one of its grants. An envelope names no grant,
    /// so the shell tries the owner's `GrantId::OWNER` and every grant whose
    /// grantee is `signer`; a thread document's address is covered by any
    /// valid grant (decision 108). Out of scope, the message is dropped
    /// with `rejection` = `out_of_scope`; presence in a workspace has no
    /// subject document and needs no scope. In scope, the shell routes it by
    /// `kind`: `notification`, `note`, `hold`, and `presence` go to the
    /// coord actor; `say`, `ask`, and `answer` are pushed to clients (the
    /// thread document catches up through sync); `typing` is pushed to
    /// clients only. `payload` is the envelope's payload.
    Deliver {
        /// The topic it came on.
        topic: String,
        /// The envelope's kind.
        kind: WireKind,
        /// Its signer.
        signer: Signer,
        /// The path of the document it is about, if any.
        address: Option<String>,
        /// The envelope's payload.
        payload: Vec<u8>,
    },
    /// Lower the forwarding device's score after a repeat offense (design
    /// §15; `TransportCommand::Penalize`).
    Penalize {
        /// The forwarding peer.
        peer: Peer,
        /// The offense.
        reason: Rejection,
    },
}

grants

Source: crates/lvcs-net/src/grants/mod.rs.

/// Which bucket a token comes from (design §15).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Bucket {
    /// Gossip messages a signer may have forwarded or published.
    Gossip,
    /// Changes the daemon signs for a participant key.
    Sign,
}

/// What a bucket refills at (design §15, decision 146): `per_sec` tokens
/// a second, holding at most `burst` (and always room for one, so a burst
/// of 0 still lets the rate through).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rate {
    /// Tokens added a second.
    pub per_sec: u32,
    /// The most tokens the bucket holds.
    pub burst: u32,
}

/// What a take is for, echoed by [`GrantsOutput::Taken`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum TakeKey {
    /// The gossipsub message id the gossip actor is validating.
    Gossip(#[serde(with = "crate::wire::cbor::bytes")] Vec<u8>),
    /// The daemon's id of the request about to be committed.
    Sign(u64),
}

/// The limits in force (design §15, decision 146).
///
/// They are the limit constants in `docs/generated/vocabulary.md` by
/// default ([`Limits::DEFAULT`]), each replaced by its key in protected
/// `lvcs.toml` `[limits]` (the daemon's `LvcsToml`; a missing key keeps its
/// default), so a participant can't change them by editing the file. The
/// field names are the `[limits]` keys (the constants' names in lower
/// case). They are the defaults for every signer, the owner included: the
/// shell gives each take its [`Rate`] from them ([`Limits::sign_rate`],
/// [`Limits::gossip_rate`]), each per-second rate and the signing burst
/// replaced by the signer's grant terms where they name one (decision 146),
/// and the gossip actor sizes its memory and tolerance by `gossip_burst`. A
/// change quota is never a limit: a grant's terms carry it (decision 70).
/// Owner: `M3.grants`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Limits {
    /// `PARTICIPANT_SIGN_PER_SEC` in force.
    pub participant_sign_per_sec: u32,
    /// `PARTICIPANT_SIGN_BURST` in force.
    pub participant_sign_burst: u32,
    /// `PARTICIPANT_GOSSIP_PER_SEC` in force.
    pub participant_gossip_per_sec: u32,
    /// `GOSSIP_BURST` in force.
    pub gossip_burst: u32,
}

impl Limits {
    /// The protocol defaults.
    pub const DEFAULT: Self = Self {
        participant_sign_per_sec: PARTICIPANT_SIGN_PER_SEC,
        participant_sign_burst: PARTICIPANT_SIGN_BURST,
        participant_gossip_per_sec: PARTICIPANT_GOSSIP_PER_SEC,
        gossip_burst: GOSSIP_BURST,
    };

    /// The default signing rate: `participant_sign_per_sec`, holding
    /// `participant_sign_burst`.
    #[must_use]
    pub const fn sign_rate(&self) -> Rate {
        Rate {
            per_sec: self.participant_sign_per_sec,
            burst: self.participant_sign_burst,
        }
    }

    /// The default gossip rate: `participant_gossip_per_sec`, holding
    /// `gossip_burst`.
    #[must_use]
    pub const fn gossip_rate(&self) -> Rate {
        Rate {
            per_sec: self.participant_gossip_per_sec,
            burst: self.gossip_burst,
        }
    }
}

impl Default for Limits {
    fn default() -> Self {
        Self::DEFAULT
    }
}

/// Mailbox events of the grants actor. Owner: `M3.grants`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GrantsEvent {
    /// Take one token from `signer`'s `bucket`, which refills at `rate`,
    /// at `now_ms` (the shell's clock, Unix milliseconds); answered by
    /// exactly one [`GrantsOutput::Taken`] with `key`. With `wait`, a take
    /// the bucket can't serve now is not refused (a mirror save past its
    /// author's signing rate, decision 128, or a renewal past its issuer's,
    /// decision 185): the actor keeps it, emits
    /// [`GrantsOutput::Wake`] for when the bucket should hold a token, and
    /// answers it allowed once a [`GrantsEvent::Refill`] finds one. The
    /// keys of the takes kept at once are distinct (the shell's request
    /// ids); a waiting take with the key of one already kept replaces it.
    Take {
        /// What the take is for.
        key: TakeKey,
        /// The bucket to take from.
        bucket: Bucket,
        /// Whose bucket.
        signer: Signer,
        /// What the bucket refills at.
        rate: Rate,
        /// The shell's clock, in Unix milliseconds.
        now_ms: i64,
        /// Whether a take the bucket can't serve now waits instead of being
        /// refused.
        wait: bool,
    },
    /// The shell's timer for the kept take `key` fired
    /// ([`GrantsOutput::Wake`]): its bucket is refilled to `now_ms` at the
    /// take's rate and the take tried again. Still short of a token
    /// (another take spent it meanwhile), the take stays kept and is woken
    /// again. A `key` no take is kept for changes nothing.
    Refill {
        /// The kept take.
        key: TakeKey,
        /// The shell's clock, in Unix milliseconds.
        now_ms: i64,
    },
    /// The grants this daemon renews (decision 148), the shell's whole
    /// set after each caps render: the newest grant of each windowed
    /// lineage whose issuer key the daemon holds and none of whose grants
    /// is revoked (decision 185). It replaces
    /// the set before it; a grant still listed keeps when it was last
    /// asked to be renewed, and a grant no longer listed is forgotten. Of
    /// two entries naming one grant, the later one counts.
    Renewable(Vec<Renewable>),
    /// The shell's renewal tick at `now_ms` (Unix milliseconds): each
    /// listed grant whose renewal is due is answered by one
    /// [`GrantsOutput::Renew`]. A renewal is due once half the grant's
    /// window has passed (`now_ms`, in seconds, at or after
    /// `not_before_s + (not_after_s - not_before_s) / 2`), past its end
    /// included, so a lapsed lineage renews when its issuer's daemon
    /// returns, unless the grant was asked less than
    /// `GRANT_RENEW_RETRY_MS` ago.
    Tick {
        /// The shell's clock, in Unix milliseconds.
        now_ms: i64,
    },
}

/// A grant the issuer's daemon renews (decision 148): its id and its
/// window, in Unix seconds. lvcs-net never names a grant (decision 30),
/// so the id is opaque here: the shell's grant id, 32 bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Renewable {
    /// The shell's grant id, opaque here.
    #[serde(with = "crate::wire::cbor::fixed")]
    pub grant: [u8; 32],
    /// The window's start, in Unix seconds.
    pub not_before_s: i64,
    /// The window's end, in Unix seconds.
    pub not_after_s: i64,
}

/// Outputs of the grants actor. Owner: `M3.grants`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GrantsOutput {
    /// The answer to a [`GrantsEvent::Take`]. When `allowed` is false,
    /// `retry_after_ms` is how long until the bucket holds a token, at
    /// most one hour (3 600 000, for a bucket whose rate is 0 and so never
    /// refills; decision 109): for a gossip take the message is dropped (`rejection` = `rate_limited`),
    /// and for a sign take the daemon answers the request `rate_limited`
    /// with it, uncommitted. A waiting take is never refused: it is
    /// answered allowed, with `retry_after_ms` 0, once its bucket holds a
    /// token.
    Taken {
        /// The take answered.
        key: TakeKey,
        /// Whether a token was taken.
        allowed: bool,
        /// How long until the bucket holds a token, when refused.
        retry_after_ms: u64,
    },
    /// Send [`GrantsEvent::Refill`] for the kept take `key` at `at_ms`,
    /// when its bucket should hold a token (at most one hour on, for a
    /// bucket whose rate is 0).
    Wake {
        /// The kept take.
        key: TakeKey,
        /// When, by the shell's clock.
        at_ms: i64,
    },
    /// Renew the listed grant `grant` (decision 148): the shell re-issues
    /// it with the same terms and a window of the same length starting
    /// now, signed by its issuer once the issuer's waiting signing take is
    /// given (decision 185), or drops the renewal when it can't. The
    /// grant is asked again after `GRANT_RENEW_RETRY_MS` while a renewal
    /// hasn't replaced it in [`GrantsEvent::Renewable`].
    Renew {
        /// The shell's grant id, opaque here.
        grant: [u8; 32],
    },
}

sync

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

/// Mailbox events of the sync actor. Time enters only as event data.
/// Owner: `M3.sync`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncEvent {
    /// The one sync stream with `peer` is open (`TransportEvent::SyncOpened`).
    /// The actor joins `.lvcs/caps` with the peer first (decision 68), and
    /// that `Join`'s tag opens the stream: an `Outgoing` with an older tag
    /// was made for a stream since closed (decision 110). Every other
    /// document it holds joins only once the caps document reports
    /// [`SyncEvent::Settled`] for the peer with the latest caps input's
    /// tag, so the caps changes reach the receiver ahead of the changes
    /// made under them, which it would otherwise defer (decision 108). A
    /// second `Opened` for a peer whose stream is open is ignored.
    Opened {
        /// The peer the stream is with.
        peer: Peer,
    },
    /// One frame from the stream (`TransportEvent::SyncFrame`): an encoded
    /// envelope of kind `sync` whose sender must be `Sender::Device` with
    /// `peer`'s key and whose payload is a [`SyncFrame`] naming a whole
    /// document by a synced address (decision 110): relative, in Unicode
    /// NFC, with no empty, `.`, or `..` segment, no `#` or NUL, the control
    /// prefix (`.lvcs`) spelled exactly when the first segment is any case
    /// variant of it, and at most 4096 bytes. A frame that fails any of these
    /// disconnects the peer ([`SyncOutput::Disconnect`]), as does one for a
    /// document this daemon does not hold once the peer's frames have
    /// introduced `PROJECT_MAX_DOCUMENTS` such documents (decision 112); a
    /// valid one is handed to its document ([`SyncOutput::Deliver`]). One of
    /// a kind from a newer minor version is ignored (design §10).
    Frame {
        /// The peer the stream is with.
        peer: Peer,
        /// The frame's bytes.
        #[serde(with = "crate::wire::cbor::bytes")]
        frame: Vec<u8>,
    },
    /// The stream with `peer` closed, or the peer went away: every document
    /// leaves it ([`SyncOutput::Leave`]).
    Closed {
        /// The peer the stream was with.
        peer: Peer,
    },
    /// A document this daemon holds: sent once per address, for every
    /// stored document at start and for each one created later (by an
    /// edit, an import, a fork, or a sync), so the journal grows with the
    /// number of documents, not its square. The actor joins it with every
    /// peer past its caps round.
    Known {
        /// The document's address.
        doc: String,
    },
    /// The document `doc` has a sync message for `peer`
    /// (`DocOutput::SyncOut`), to frame and send. `tag` is the tag of the
    /// last `Join` or `Deliver` the document had for `peer` (decision 110);
    /// one older than the peer's stream is dropped.
    Outgoing {
        /// The document's address.
        doc: String,
        /// The peer the message is for.
        peer: Peer,
        /// The sync message, opaque here.
        #[serde(with = "crate::wire::cbor::bytes")]
        payload: Vec<u8>,
        /// The tag of the document's last input for `peer`.
        tag: u64,
    },
    /// The document `doc` has nothing more to send `peer` until it hears
    /// from it (`DocOutput::SyncSettled`): Automerge's sync generated no
    /// message. `tag` is as for [`SyncEvent::Outgoing`]; a caps `Settled`
    /// whose tag is not the latest caps input's is ignored (decision 110).
    Settled {
        /// The document's address.
        doc: String,
        /// The peer it has nothing more for.
        peer: Peer,
        /// The tag of the document's last input for `peer`.
        tag: u64,
    },
    /// The document `doc` rejected a whole message from `peer`
    /// (`DocOutput::SyncRejected`, decision 68): honest peers never forward
    /// unverified changes, so the peer is faulty.
    Rejected {
        /// The document's address.
        doc: String,
        /// The peer the message came from.
        peer: Peer,
        /// Why it was rejected.
        #[serde(with = "crate::wire::cbor::vocab")]
        reason: Rejection,
    },
}

/// Outputs of the sync actor, which the daemon shell carries out.
/// Owner: `M3.sync`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncOutput {
    /// Seal `frame` in an envelope of kind `sync` signed by this device's
    /// key and write it to the sync stream with `peer`
    /// (`TransportCommand::SyncFrame`).
    Send {
        /// The peer to send to.
        peer: Peer,
        /// The frame to seal and send.
        frame: SyncFrame,
    },
    /// Hand a received payload to the document at `doc`, spawning its actor
    /// if it isn't running (`DocEvent::SyncIn`, carrying `tag`). The shell
    /// refuses one whose address the path filter excludes (never-sync,
    /// decision 81) or that collides with an existing directory, before
    /// spawning any document (decision 110), and spawns no document for one
    /// while it holds `PROJECT_MAX_DOCUMENTS` documents, dropping it without
    /// penalty (decision 112).
    Deliver {
        /// The document's address.
        doc: String,
        /// The peer the payload came from.
        peer: Peer,
        /// The sync message, opaque here.
        payload: Vec<u8>,
        /// The input's tag.
        tag: u64,
    },
    /// Start syncing `doc` with `peer` (`DocEvent::PeerJoined`, carrying
    /// `tag`). Every `Join` and `Deliver` takes the next value of a counter
    /// rising through the daemon run (decision 110); tags are never
    /// telemetry values.
    Join {
        /// The document's address.
        doc: String,
        /// The peer to sync with.
        peer: Peer,
        /// The input's tag.
        tag: u64,
    },
    /// Stop syncing `doc` with `peer` (`DocEvent::PeerLeft`).
    Leave {
        /// The document's address.
        doc: String,
        /// The peer to stop syncing with.
        peer: Peer,
    },
    /// Close the stream and every connection to `peer` and lower its score
    /// (`TransportCommand::Disconnect` and `Penalize`): a faulty frame, a
    /// document's rejection, or `over_limit` (decisions 111 and 112).
    Disconnect {
        /// The faulty peer.
        peer: Peer,
        /// Why it is disconnected.
        reason: Rejection,
    },
}

transport

Source: crates/lvcs-net/src/transport/mod.rs.

/// A peer device as the net actors name it (design §2).
///
/// It is its Ed25519 public key, from which its libp2p `PeerId` derives.
/// Every LVCS device key is Ed25519, so the transport refuses a connection
/// whose identity is another key type (chosen at the M3 freeze: a
/// fixed-size, journaled form, where a `PeerId` would need libp2p's serde
/// feature). Journaled as a byte string. Owner: `M3.transport`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Peer(#[serde(with = "crate::wire::cbor::fixed")] pub [u8; 32]);

/// How the transport found a peer (design §2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Discovery {
    /// mDNS on the local network.
    Mdns,
    /// The Kademlia DHT.
    Kademlia,
    /// A `--bootstrap` address.
    Bootstrap,
}

/// What `AutoNAT` concluded about this device's reachability (design §2):
/// a `Private` device is reached through a circuit relay v2 reservation,
/// upgraded by `DCUtR` when hole punching succeeds.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum NatStatus {
    /// Not concluded yet.
    Unknown,
    /// Publicly reachable.
    Public,
    /// Not publicly reachable.
    Private,
}

/// How the transport runs, from `lvcs daemon`'s flags (design §2).
///
/// It holds the multiaddrs to listen on (`--listen`) and to dial at start
/// (`--bootstrap`), and seed mode (`--seed`): the same binary with no
/// special authority, which pins every document it learns of, serves as a
/// circuit relay v2 server, and is a bootstrap address for others. Owner:
/// `M3.transport`; the daemon (`M3.daemon`) builds it.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TransportConfig {
    /// The multiaddrs to listen on (`--listen`).
    pub listen: Vec<String>,
    /// The multiaddrs to dial at start (`--bootstrap`).
    pub bootstrap: Vec<String>,
    /// Seed mode (`--seed`).
    pub seed: bool,
}

/// What the swarm reports (design §2), in the order it saw it.
///
/// The daemon shell turns each into the events of the actors that need it:
/// peers and the sync stream go to the sync actor (`SyncEvent`), gossip
/// messages to the gossip actor (`GossipEvent`). Addresses are multiaddrs
/// in text form. Owner: `M3.transport`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportEvent {
    /// The swarm listens on `addr`.
    Listening {
        /// The listen address.
        addr: String,
    },
    /// A connection to `peer` is up (the first one, if there are several).
    Connected {
        /// The connected peer.
        peer: Peer,
    },
    /// The last connection to `peer` closed.
    Gone {
        /// The peer whose last connection closed.
        peer: Peer,
    },
    /// `peer` was discovered at `addrs` through `via`.
    Discovered {
        /// The discovered peer.
        peer: Peer,
        /// Its addresses.
        addrs: Vec<String>,
        /// How it was found.
        via: Discovery,
    },
    /// `AutoNAT`'s conclusion changed.
    Nat(NatStatus),
    /// The one sync stream with `peer` is open (decision 68): whichever
    /// side's device key bytes are smaller opens it, and the transport
    /// closes a second one, so each peer pair has exactly one.
    SyncOpened {
        /// The peer the stream is with.
        peer: Peer,
    },
    /// One length-delimited frame read from the sync stream: an encoded
    /// envelope of kind `sync`, at most `WIRE_MAX_BYTES` (a longer frame
    /// closes the stream and is reported as [`TransportEvent::SyncClosed`]).
    SyncFrame {
        /// The peer the stream is with.
        peer: Peer,
        /// The frame's bytes.
        frame: Vec<u8>,
    },
    /// The sync stream with `peer` closed.
    SyncClosed {
        /// The peer the stream was with.
        peer: Peer,
    },
    /// A gossipsub message waiting for validation (design §9).
    ///
    /// `id` is gossipsub's message id, `propagation` the peer that
    /// forwarded it (whose score a repeat offense lowers, §15), and `data`
    /// the encoded envelope, which its participant signed. Gossipsub
    /// forwards it only after [`TransportCommand::Validate`] accepts it.
    /// The publishing device is not reported: nothing downstream needs it,
    /// since the envelope names its signer and the yield order reads only
    /// the payload (chosen at the M3 freeze).
    Gossip {
        /// Gossipsub's message id.
        id: Vec<u8>,
        /// The peer that forwarded it.
        propagation: Peer,
        /// The topic it came on.
        topic: String,
        /// The encoded envelope.
        data: Vec<u8>,
    },
}

/// Gossipsub's validation verdict for a received message (design §9):
/// only an accepted message is forwarded; a rejected one also counts
/// against the propagating peer's score; an ignored one does neither.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Acceptance {
    /// Forward the message.
    Accept,
    /// Drop it and count it against the propagating peer's score.
    Reject,
    /// Drop it.
    Ignore,
}

/// What the daemon shell asks the swarm to do, built from the net actors'
/// outputs. Owner: `M3.transport`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportCommand {
    /// Dial the multiaddr `addr`.
    Dial {
        /// The multiaddr to dial.
        addr: String,
    },
    /// Write one frame (an encoded envelope) to the sync stream with
    /// `peer`, length-delimited.
    SyncFrame {
        /// The peer the stream is with.
        peer: Peer,
        /// The frame's bytes.
        frame: Vec<u8>,
    },
    /// Close every connection to `peer`.
    Disconnect {
        /// The peer to disconnect.
        peer: Peer,
    },
    /// Join a gossipsub topic.
    Subscribe {
        /// The topic to join.
        topic: String,
    },
    /// Leave a gossipsub topic.
    Unsubscribe {
        /// The topic to leave.
        topic: String,
    },
    /// Publish `data` (an encoded envelope) on `topic`.
    Publish {
        /// The topic to publish on.
        topic: String,
        /// The encoded envelope.
        data: Vec<u8>,
    },
    /// Report the validation of the gossipsub message `id` received from
    /// `propagation`.
    Validate {
        /// Gossipsub's message id.
        id: Vec<u8>,
        /// The peer that forwarded it.
        propagation: Peer,
        /// The verdict.
        acceptance: Acceptance,
    },
    /// Lower `peer`'s score for a protocol violation (design §11, §15).
    Penalize {
        /// The peer to penalize.
        peer: Peer,
        /// The violation.
        reason: Rejection,
    },
}

transport-spawn

Source: crates/lvcs-net/src/transport/shell.rs.

/// Run the transport for `swarm` (from `build_swarm`) on the current tokio
/// runtime with `config`, listening on every IPv4 interface (TCP and QUIC,
/// ports the system picks) when `config.listen` is empty.
///
/// Events go to `events` in order, and the shell waits while `events` is
/// full, so the receiver must keep draining it. Commands go through the
/// returned handle (`TransportHandle::send`, which waits while the command
/// queue is full and returns the command when the transport has stopped);
/// the transport stops once every handle is dropped, and the task then
/// ends. Owner: `M3.transport`; the daemon (`M3.daemon`) runs it.
///
/// # Errors
///
/// [`NetError`] when a configured address does not parse, or the sync
/// protocol is already accepted.
pub fn spawn(
    swarm: Swarm<Behaviour>,
    config: &TransportConfig,
    events: mpsc::Sender<TransportEvent>,
) -> Result<(TransportHandle, JoinHandle<()>), NetError>

wire-payloads

Source: crates/lvcs-net/src/wire/mod.rs.

/// A participant that signs gossip messages, by its Ed25519 public key (the
/// bytes of an `lvcs_doc::ParticipantKey`; `Sender::Participant`).
///
/// The net actors' per-signer state (capabilities, token buckets) keys on
/// it. Journaled as a byte string. Owner: `M3.envelope`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Signer(#[serde(with = "cbor::fixed")] pub [u8; 32]);

/// Payload of kind `sync` (design §2, decisions 30 and 68; lane `M3.sync`).
///
/// It is one document's sync message on the one sync stream of a peer pair.
/// `doc` is the document address the message is tagged with; `payload` is
/// what that document's actor emitted (`DocOutput::SyncOut`), handed
/// unchanged to the receiving peer's actor for `doc` (`DocEvent::SyncIn`).
/// The envelope's sender is the sending device.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncFrame {
    /// The document address the message is tagged with.
    pub doc: String,
    /// What that document's actor emitted (`DocOutput::SyncOut`), opaque
    /// here.
    #[serde(with = "cbor::bytes")]
    pub payload: Vec<u8>,
}

/// What a notification says the sender will do to its range (design §5).
/// Encoded as `adding`, `changing`, or `removing`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotifyOp {
    /// The sender will add to the range.
    Adding,
    /// The sender will change the range.
    Changing,
    /// The sender will remove the range.
    Removing,
}

/// Payload of kind `notification` (design §5, decisions 20 and 51; lane
/// `M3.coord`).
///
/// It is `{ doc, range, op, what, ref?, by, ts, workspace?, auto, issue? }`,
/// published on the document's topic and every ancestor directory's topic.
/// `range` is the canonical CBOR encoding of the `lvcs_doc::RangeAnchor`
/// cursor pair the notifier's document anchored (`DocEvent::Anchor`),
/// opaque here: the receiving daemon resolves it against its own copy of
/// `doc` (the coord actor sees only resolved spans). An empty `range` is a whole-document
/// notification (`AnchorOutcome::Whole`: an empty document has no character
/// to anchor on), whose span is `coord::Span::WHOLE` (chosen at the M3
/// freeze). `ref` optionally names more detail (`kad://<key>` or
/// `lvcs://<doc>#<pointer>`). `by` is the notifying participant's key bytes
/// and must equal the envelope's `Sender::Participant`; `ts` is its Unix
/// time in milliseconds, which only orders overlapping notifications: by
/// `ts`, then `by`, then the notification id (decision 51 as the M3 freeze
/// revises it; see `coord`). `workspace` is the id of the workspace whose
/// view the range is in, absent for `main` (chosen at the M3 freeze).
/// `auto` is true for a notification the daemon made from its author's
/// save through a mirror (decisions 155 and 165), false for one a client
/// sent (`announce`, `track_edit`). `issue` names the issue the notification
/// reports, absent for one that reports none (decision 187). A
/// notification's id is the `HASH` of this payload's canonical CBOR
/// encoding (`coord::NotificationId::of`), so the id and the signature
/// cover `auto` and `issue`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Notification {
    /// The document notified about.
    pub doc: String,
    /// The canonical CBOR encoding of the anchored `lvcs_doc::RangeAnchor`
    /// cursor pair; empty for the whole document.
    #[serde(with = "cbor::bytes")]
    pub range: Vec<u8>,
    /// What the sender will do to the range.
    pub op: NotifyOp,
    /// What the change is, in the sender's words.
    pub what: String,
    /// Where more detail is (`kad://<key>` or `lvcs://<doc>#<pointer>`),
    /// under the key `ref`.
    #[serde(rename = "ref")]
    pub reference: Option<String>,
    /// The notifying participant's key bytes.
    #[serde(with = "cbor::fixed")]
    pub by: [u8; 32],
    /// The notifier's Unix time in milliseconds.
    pub ts: i64,
    /// The workspace whose view the range is in; `None` for `main`.
    #[serde(with = "cbor::opt_fixed")]
    pub workspace: Option<[u8; 32]>,
    /// Whether the daemon made it from its author's save through a mirror,
    /// not a client.
    pub auto: bool,
    /// The issue it reports; `None` for one that reports none.
    #[serde(with = "cbor::opt_fixed")]
    pub issue: Option<[u8; 32]>,
}

/// Payload of kinds `note` and `hold` (design §5; lane `M3.coord`).
///
/// It is a response to the notification whose id is `notification`,
/// published on the notifier's inbox topic and the document's topic. A
/// `note` is information only; a `hold` is an objection, and `thread` is
/// the address of the thread the holder opened on the range (design §9), so
/// the notifier's edit pauses and both sides talk there. `by` must equal
/// the envelope's `Sender::Participant`; `ts` is its Unix time in
/// milliseconds.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Response {
    /// The id of the notification answered.
    #[serde(with = "cbor::fixed")]
    pub notification: [u8; 32],
    /// The responding participant's key bytes.
    #[serde(with = "cbor::fixed")]
    pub by: [u8; 32],
    /// The response's text.
    pub text: String,
    /// For a `hold`, the address of the thread the holder opened on the
    /// range.
    pub thread: Option<String>,
    /// The responder's Unix time in milliseconds.
    pub ts: i64,
}

/// Payload of kinds `say`, `ask`, and `answer` (design §9; lane
/// `M3.gossip`).
///
/// It is a thread message published live on the thread anchor's topics (the
/// address's topic and every ancestor's, `ws/<id>/talk`, or each
/// addressee's inbox), and also appended to the thread document at
/// `thread`, so offline participants catch up through sync. `message` is
/// the canonical CBOR encoding of the `lvcs_doc::ThreadMessage`, opaque
/// here; its `author` must equal the envelope's `Sender::Participant`. `ts`
/// is the post's Unix time in milliseconds, signed outside the opaque
/// message so decision 112's stale and duplicate rules cover thread posts
/// like the other gossip kinds (decision 128); it must equal the message's
/// `time_ms`, which the receiving daemon checks where it decodes the
/// message, and drops a post whose two times differ. Direct messages on an
/// inbox topic are signed but not encrypted in M3: the M3 freeze defers
/// design §9's end-to-end encryption to M4, which settles it together with
/// the confidentiality of the participant-anchored thread document they are
/// appended to (revising decision 16).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThreadPost {
    /// The address of the thread document the message is appended to.
    pub thread: String,
    /// The canonical CBOR encoding of the `lvcs_doc::ThreadMessage`, opaque
    /// here.
    #[serde(with = "cbor::bytes")]
    pub message: Vec<u8>,
    /// The post's Unix time in milliseconds, equal to the message's
    /// `time_ms`.
    pub ts: i64,
}

/// Payload of kind `presence` (design §5, §9; lane `M3.coord`).
///
/// It says `by` is on the document `doc`, or in the workspace `workspace`,
/// as of `ts` (Unix milliseconds), and counts as present there until
/// `PRESENCE_TTL_SECS` after it. Never stored.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Presence {
    /// The document `by` is on, if any.
    pub doc: Option<String>,
    /// The workspace `by` is in, if any.
    #[serde(with = "cbor::opt_fixed")]
    pub workspace: Option<[u8; 32]>,
    /// The present participant's key bytes.
    #[serde(with = "cbor::fixed")]
    pub by: [u8; 32],
    /// The Unix time in milliseconds `by` is present as of.
    pub ts: i64,
}

/// Payload of kind `typing` (design §9; lane `M3.gossip`): `by` is typing
/// in `doc` as of `ts` (Unix milliseconds). Never stored.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Typing {
    /// The document `by` is typing in.
    pub doc: String,
    /// The typing participant's key bytes.
    #[serde(with = "cbor::fixed")]
    pub by: [u8; 32],
    /// The Unix time in milliseconds `by` is typing as of.
    pub ts: i64,
}

/// Payload of kinds `check_failed` and `check_passed` (decision 193).
///
/// A runner `by` tells `to`, an author of the failing window, that the
/// check `check` failed at the frontier `failed_at` after it last passed at
/// `passed_at` (absent when it never passed on the channel, so the window
/// starts at its genesis), or, as `check_passed`, that it passes again,
/// which ends that failing streak. It is published on `to`'s inbox topic.
/// `paths` are the documents the check's `paths` or `uses` match that `to`
/// changed in the window, each with the line runs of its changes at
/// `failed_at`; `others` are the window's other authors. `tail` is the
/// failing output's last `CHECK_OUTPUT_TAIL_BYTES` and `tests` the failing
/// test names read from it, both empty for `check_passed`. `by` must
/// equal the envelope's `Sender::Participant`, which must hold `attest`
/// on `ci`; `ts` is its Unix time in milliseconds. Owner:
/// decision 193.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckNotice {
    /// The check's id.
    #[serde(with = "cbor::fixed")]
    pub check: [u8; 32],
    /// The id of the frontier where it last passed, if it ever did.
    #[serde(with = "cbor::opt_fixed")]
    pub passed_at: Option<[u8; 32]>,
    /// The id of the frontier where it failed.
    #[serde(with = "cbor::fixed")]
    pub failed_at: [u8; 32],
    /// The author told.
    #[serde(with = "cbor::fixed")]
    pub to: [u8; 32],
    /// What `to` changed in the window that the check reads.
    pub paths: Vec<CheckPath>,
    /// The window's other authors.
    pub others: Vec<Signer>,
    /// The failing output's tail.
    pub tail: String,
    /// The failing test names read from the tail.
    pub tests: Vec<String>,
    /// The runner's participant key bytes.
    #[serde(with = "cbor::fixed")]
    pub by: [u8; 32],
    /// The runner's Unix time in milliseconds.
    pub ts: i64,
}

/// One document a [`CheckNotice`]'s recipient changed in the window: its
/// path and the line runs of its changes, 1-based and inclusive, at the
/// failing frontier (none for a binary or deleted file).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckPath {
    /// The document's path.
    pub path: String,
    /// The line runs.
    pub lines: Vec<CheckLines>,
}

/// One run of lines of a [`CheckPath`]: `first` to `last`, 1-based and
/// inclusive.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CheckLines {
    /// The run's first line.
    pub first: u64,
    /// Its last line.
    pub last: u64,
}

wire-encode

Source: crates/lvcs-net/src/wire/mod.rs.

/// The canonical CBOR encoding of `envelope` (design §5).
#[must_use]
pub fn encode(envelope: &Envelope) -> Vec<u8> {

wire-decode

Source: crates/lvcs-net/src/wire/mod.rs.

/// Decode one envelope.
///
/// It does not check the signature ([`verify`]) or the kind: a kind
/// outside the `wire_kind` set ([`known_kind`] is `None`) is ignored by the
/// receiver, never rejected (design §10).
///
/// # Errors
///
/// [`WireError::TooLarge`] past `WIRE_MAX_BYTES` before anything is
/// decoded, [`WireError::UnknownMajor`] when its major version is not
/// `WIRE_MAJOR` (read first, whatever the rest holds), and
/// [`WireError::Malformed`] for anything else that is not a canonical CBOR
/// envelope.
pub fn decode(bytes: &[u8]) -> Result<Envelope, WireError> {

wire-signing-input

Source: crates/lvcs-net/src/wire/mod.rs.

/// The bytes `Envelope::sig` covers: the canonical CBOR array
/// `[v, kind, sender, frontier, payload]` (design §5). The daemon signs
/// them with the sender's key; [`verify`] checks them.
#[must_use]
pub fn signing_input(
    version: WireVersion,
    kind: &str,
    sender: Sender,
    frontier: Option<[u8; 32]>,
    payload: &[u8],
) -> Vec<u8> {

wire-verify

Source: crates/lvcs-net/src/wire/mod.rs.

/// Check that `envelope.sig` is its sender's Ed25519 (`SIGNATURE`)
/// signature over [`signing_input`]; [`WireError::BadSignature`] otherwise.
///
/// # Errors
///
/// [`WireError::BadSignature`] when the sender's key is not an Ed25519
/// public key or the signature does not verify.
pub fn verify(envelope: &Envelope) -> Result<(), WireError> {

wire-kind

Source: crates/lvcs-net/src/wire/mod.rs.

/// The `wire_kind` value `envelope.kind` names, or `None` for a kind from a
/// newer minor version, which the receiver ignores (design §10).
#[must_use]
pub fn known_kind(envelope: &Envelope) -> Option<WireKind> {

wire-payload

Source: crates/lvcs-net/src/wire/mod.rs.

/// The canonical CBOR encoding of a payload, for `Envelope::payload`.
pub fn encode_payload<P: Serialize>(payload: &P) -> Vec<u8> {

merge

Source: crates/lvcs-ws/src/merge/mod.rs.

/// The inputs of a bundle id (decision 57), in hash order: the
/// [`BundleId`] is the `HASH` of the canonical CBOR array of these four,
/// each a byte string, so it never depends on the changes that carry it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleInputs {
    /// The workspace merged back.
    pub workspace: WorkspaceId,
    /// The merging participant.
    pub merger: ParticipantKey,
    /// The workspace's visible frontier when the merge started.
    pub workspace_frontier: FrontierId,
    /// The visible frontier of the workspace merged into, when the merge
    /// started.
    pub base_frontier: FrontierId,
}

/// Merge a workspace back (design §8, `ws.merge`).
///
/// It is a workspace event (`WorkspaceEvent::Merge`), computed once, by the
/// merging peer's workspace actor, as one `WorkspaceMerged` bundle. Per
/// override, the claim's text at claim time is diffed against the override
/// and applied as fine-grained edits at the base's current anchors when the
/// base is unchanged inside the range, and merged three-way within the
/// range otherwise; only genuinely overlapping edits become a [`Conflict`].
/// Every change is a `BundleEdit` tagged with the bundle id; merge-back is
/// the only way a workspace's work reaches its base. Answered with exactly
/// one [`MergeResult`] for `req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeRequest {
    /// The request its [`MergeResult`] answers.
    pub req: RequestId,
    /// The merging participant's origin: the author and time of every
    /// change in the bundle.
    pub origin: Origin,
    /// The bundle id's inputs.
    pub inputs: BundleInputs,
    /// The workspace merged into: always the workspace's base, `main`, or
    /// for a stacked workspace the one it stacks on, which absorbs the
    /// bundle (`StackedBundle` in `crate::workspace`).
    pub into: WorkspaceBase,
    /// Block the merge until checks pass on a preview of the merged state
    /// (design §8).
    pub require_validated: bool,
    /// Every Automerge actor the daemon can name, mapped to its participant
    /// through its key store (each local participant's `AuthorId` on this
    /// device, M2) or, for a remote author, its `ChangeSig` signer, which
    /// the documents report (`DocOutput::Signers`, M3), for
    /// [`Conflict::authors`].
    pub authors: BTreeMap<AuthorId, ParticipantKey>,
    /// Leave derived overrides out of the bundle (row 180): set with
    /// the generator, whose next counted generation covers the merged
    /// inputs, so the merging participant is offered one
    /// `derived_superseded` per path left out.
    pub leave_derived: bool,
}

/// One document a bundle changed, as its merge record lists it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergedDoc {
    /// The document's heads in the base visible frontier the merge read,
    /// so two records can be tested for concurrency: neither's base heads
    /// contain the other's target heads (decision 65).
    #[serde(with = "cbor::heads")]
    pub base: Vec<ChangeHash>,
    /// The document's heads with the bundle's changes: a peer's visible
    /// frontier advances over the bundle only once the record and every
    /// target head are present locally (decision 48).
    #[serde(with = "cbor::heads")]
    pub target: Vec<ChangeHash>,
    /// The base ranges the bundle's range overrides changed, as their claim
    /// anchors, cursor pairs anchored at the claim's heads, which `base`
    /// descends from (decision 65). Two concurrent bundles whose ranges
    /// overlap in a document open a conflict thread to both mergers
    /// (`lvcs_doc::CheckOverlap`).
    pub ranges: Vec<RangeAnchor>,
    /// The bundle made a change here that has no range (decision 108): a
    /// whole-document override's body or executable bit (its claim anchors
    /// no cursor pair, so even a text change of it names none), a
    /// workspace-local file's recreate, a binary body, or a removal. Overlap
    /// detection treats a whole entry as overlapping every range of the
    /// document, and every other whole entry. `ranges` is empty then: a
    /// document's changes come from one whole-document or workspace-local
    /// override, from a whiteout, or from range overrides, never from two
    /// of them.
    pub whole: bool,
}

/// A bundle's merge record, written to `.lvcs/registry` as a
/// `RecordBundle` holding its canonical CBOR encoding, and signed
/// (decisions 48 and 65).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeRecord {
    /// The `HASH` of `inputs`.
    pub bundle: BundleId,
    /// The bundle id's inputs, among them the workspace id.
    pub inputs: BundleInputs,
    /// Every document the bundle changed, by address.
    pub docs: BTreeMap<String, MergedDoc>,
}

/// A merge-back conflict (design §8).
///
/// It is an override's edits and the base's edits since the claim that
/// genuinely overlap, an override whose anchor was lost, or a whiteout of a
/// file the base changed since (decision 86). It opens a thread anchored on
/// `address` (`ThreadAnchor::Address`) and addressed (`ThreadOpen.to`) to
/// `authors` (design §9). The conflicts of one merge in one document share
/// that thread, since its id inputs (the anchor, the merger, the merge's
/// time) are the same; its `to` is the union of their authors. Conflicts
/// between two concurrent bundles (decision 65) are not these: they come
/// from merge records ([`MergedDoc::ranges`] and [`MergedDoc::whole`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Conflict {
    /// The base document the override claimed from, or the whited-out
    /// path.
    pub address: String,
    /// What conflicts: an override, or the path's whiteout (decision 86).
    pub subject: ConflictSubject,
    /// Why the conflict opened.
    pub cause: ConflictCause,
    /// The conflicting base range, in code points at the document's target
    /// heads; `Collapsed { at }` when that range is empty, `at` the gap
    /// where it is there (decision 92): always for an anchor-lost override,
    /// whose text the base no longer holds a place for, and `Collapsed {
    /// at: 0 }`, no range, when the base deleted the file (decision 100).
    pub span: AnchorSpan,
    /// The participants whose edits overlap, on the workspace's side and on
    /// the base's side. The daemon maps each Automerge actor (`AuthorId`) to
    /// its participant through its key store (M2) or the `ChangeSig` signer
    /// the documents report (`DocOutput::Signers`, M3), as
    /// [`MergeRequest::authors`]; an author it can't map is omitted.
    pub authors: BTreeSet<ParticipantKey>,
    /// The conflict thread, at `THREAD_ADDRESS`.
    pub thread: ThreadId,
    /// `span`'s lines in the document's text after the plan, named as the
    /// thread names a range there (decisions 160, 165, and 197), one name
    /// per conflict, never joined across conflicts; `None` for a
    /// collapsed span, a document that holds no text after the plan, an
    /// `AnchorLost` conflict, and a `Whiteout`.
    pub lines: Option<LineName>,
}

/// What a [`Conflict`] is about (decision 86). Ordered: overrides by id,
/// then the whiteout.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ConflictSubject {
    /// One of the workspace's overrides of the document.
    Override(OverrideId),
    /// The workspace's whiteout of the path.
    Whiteout,
}

/// Why a [`Conflict`] opened (decision 92).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConflictCause {
    /// The override's edits and the base's edits since the claim genuinely
    /// overlap, and the base's text was kept: also an override whose body
    /// or the base's is not text a merge needs, whose range doesn't fit
    /// the base's text, or whose claim overlaps a smaller id's (M3).
    Overlap,
    /// The base deleted the override's whole range, or the whole file
    /// (decision 100), so it has no anchor; it merged nothing.
    AnchorLost,
    /// The base changed the whited-out file since the whiteout, so the
    /// file was kept.
    Whiteout,
}

/// The answer to a [`MergeRequest`] (`WorkspaceOutput::Merged`).
///
/// It is sent once the changeset is closed (decision 94): every change of
/// the bundle is applied and rendered, its record written, and every
/// override and whiteout the merge read has left the index.
///
/// A refused merge-back (decision 88): `rejected` maps each base document
/// that refused a bundle change or a merge read to its first reason (and,
/// after a restart, each one the merge still waited on to `Failed`,
/// decision 93). When it is not empty, the merge is answered once every
/// read and change it routed is answered (at once after a restart), and no
/// merge record is written, no thread
/// opens, nothing leaves the index, and `ws.merge` answers an error;
/// `conflicts` is empty and `record` lists only the documents whose every
/// bundle change applied (none when a read was refused). M2 has no bundle
/// visibility gate (it arrives with sync in M3), so bundle changes applied
/// before the refusal stay visible in M2.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeResult {
    /// The request answered.
    pub req: RequestId,
    /// The bundle's merge record: for a refused merge, only the documents
    /// whose every bundle change applied.
    pub record: MergeRecord,
    /// The merge's conflicts, each with its thread; empty for a refused
    /// merge.
    pub conflicts: Vec<Conflict>,
    /// Each base document that refused a bundle change or a read, with its
    /// first reason; empty for a merge that applied.
    pub rejected: BTreeMap<String, RejectReason>,
    /// The path of each derived override the merge left out
    /// ([`MergeRequest::leave_derived`], row 180), for which the merging
    /// participant is offered one `derived_superseded`; empty without
    /// `leave_derived` and for a refused merge.
    pub left_derived: BTreeSet<String>,
}

/// Everything [`plan`] reads: the request, and every base document the
/// workspace's overrides and whiteouts touch, as the workspace actor holds
/// them when the merge starts.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeInput {
    /// The merge request planned.
    pub request: MergeRequest,
    /// The workspace actor's next request id for a change it routes; the
    /// plan numbers its `BundleEdit`s from it, in address order.
    pub next_req: u64,
    /// By base address (`OverrideClaim.base`, or a workspace-local file's
    /// path).
    pub docs: BTreeMap<String, BaseInput>,
}

/// One base document of a merge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BaseInput {
    /// Its current heads, at which every change of the plan applies: for a
    /// workspace-local file's path, those of the path's file document,
    /// whose body is `Deleted` at its genesis heads or after a removal
    /// (decision 98).
    #[serde(with = "cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// Its current body there. A workspace-local file is recreated over a
    /// `Deleted` body, and conflicts with any other: the base created the
    /// file meanwhile (decision 98).
    pub body: FileBody,
    /// Its current executable bit there.
    pub executable: bool,
    /// The workspace whites the path out (decision 86).
    pub whiteout: Option<WhiteoutInput>,
    /// The workspace's overrides of it.
    pub overrides: BTreeMap<OverrideId, OverrideInput>,
}

/// A whiteout of a merge (decision 86).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WhiteoutInput {
    /// The heads of the document that held the path in the base view when
    /// it was whited out (the index's `WhiteoutEntry`): the merge deletes
    /// the file only while [`BaseInput::heads`] equal them.
    #[serde(with = "cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// Who a conflict on the whiteout names: the participant who placed it
    /// and the base's editors beyond `heads` (`ReadAt` authors), mapped
    /// through [`MergeRequest::authors`], unknown authors omitted.
    pub authors: BTreeSet<ParticipantKey>,
}

/// One override of a merge.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverrideInput {
    /// The override's claim.
    pub claim: OverrideClaim,
    /// The override's body at its init heads: what the base view held in
    /// the claimed scope at claim time (read with `ReadAt`).
    pub claimed: FileBody,
    /// The override's executable bit at its init heads, the bit its init
    /// change wrote (read with the same `ReadAt`, decision 85).
    pub claimed_executable: bool,
    /// The override's body now.
    pub current: FileBody,
    /// The override's executable bit now.
    pub executable: bool,
    /// The claim's range resolved at the base's `heads` (`None` for a
    /// whole-document or workspace-local override).
    pub span: Option<AnchorSpan>,
    /// A derived save marked it derived (row 180): a whole-document or
    /// workspace-local override that merge-back writes whole at its claim's
    /// base heads, never merged three-way, or leaves out.
    pub derived: bool,
    /// Who a conflict on this override names: the claimant, the override's
    /// editors since its init, and the base's editors inside the range since
    /// the claim (`ReadAt` authors), mapped through
    /// [`MergeRequest::authors`], unknown authors omitted.
    pub authors: BTreeSet<ParticipantKey>,
}

/// What [`plan`] decided: the workspace actor routes the edits, then, from
/// their answers, completes and records the merge record, opens the
/// threads, and answers with a [`MergeResult`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergePlan {
    /// The bundle's changes per base document, in the order they apply,
    /// each at that document's `BaseInput.heads`.
    pub edits: BTreeMap<String, Vec<BundleEdit>>,
    /// The merge record, its `target` heads empty: the workspace actor
    /// fills each document's from the answer to its last `BundleEdit`.
    pub record: MergeRecord,
    /// The plan's conflicts: per document in address order, each
    /// document's by span and then subject.
    pub conflicts: Vec<Conflict>,
    /// The thread each conflicting document's conflicts open, by thread id.
    pub threads: BTreeMap<ThreadId, ThreadOpen>,
    /// The path of each derived override left out
    /// ([`MergeRequest::leave_derived`]).
    pub left_derived: BTreeSet<String>,
}

/// Plan a merge-back (design §8): a pure function of `input`, so the
/// workspace actor's action can call it and replay gets the same plan.
///
/// It reads no clock and no RNG; the only time is `request.origin.time_ms`.
///
/// Per base document, in address order:
/// - A whiteout deletes the file (`Removed`, none when the body is already
///   `Deleted`) only when the base's `heads` equal the whiteout's (compared
///   as sets): the base is as the workspace saw it when it deleted the
///   file. Otherwise the base changed the file since, and the whiteout is a
///   conflict (`Whiteout`) that keeps the base, so a base edit the
///   workspace never saw is never deleted (decision 86). Either way it
///   hides the document's overrides, which merge nothing. Of concurrent
///   whiteouts of one path (M3), the input is the entry the index
///   resolves their key to; testing any one of them is safe, since the
///   base equals its heads only while its placer has seen every base edit.
/// - A range override is merged three-way within its span, the claimed
///   text the common ancestor: the override's diff from it and the base's
///   diff from it (the base's text inside the span now) are hunks of the
///   claimed text, each one splice of the shortest code point diff (a
///   maximal run of changes between unchanged code points). When the base
///   is unchanged inside the range its diff is empty, so the override's
///   diff applies as fine-grained edits at the span. Hunks of the two sides
///   genuinely overlap when their replaced ranges share a code point, an
///   insertion lies strictly inside the other side's replaced range, or
///   both are insertions at one gap; touching hunks don't (decision 95),
///   and an insertion at a replaced range's start or end lands before or
///   after the replacement. Overlapping hunks form a cluster; one whose two
///   versions are equal is already in the base, and any other is a
///   conflict (`Overlap`) that keeps main's text there, the base's
///   (decision 99), while the override's other hunks still apply. An
///   override whose text equals its claimed text merges nothing.
/// - A range override whose span is `Collapsed` (or `None`) lost its
///   anchor: a conflict (`AnchorLost`) that changes nothing in the base
///   (decision 95). So does every range override of a base whose body is
///   `Deleted`, whatever its span: the base deleted its whole range, and
///   the conflict has no range (decision 100). One whose bodies are not
///   text, whose base body is binary, or whose span doesn't fit the base's
///   text, is a conflict (`Overlap`) that merges nothing. Range overrides
///   merge in order of their span's start, equal starts by override id.
/// - A whole-document override of a present base is merged three-way over
///   the body: written when only the override changed it (`Text` splices of
///   the base's text, `Binary`, or `Removed`), merged as text when both
///   changed text, and a conflict (`Overlap`) that keeps the base
///   otherwise, or when a text must be written over a binary or deleted
///   base, whose `/content` is not an input. Its executable bit is merged
///   three-way against `claimed_executable` (decision 85): `Executable`
///   (the override's bit) is written only when the bit differs from both
///   the claim-time bit and the base's, and the file exists after the
///   merge, so a chmod the base made after the claim is kept.
/// - A workspace-local file is written when the base body is `Deleted`, at
///   its genesis heads or after history: `Recreate` with its text (an empty
///   text included, which still creates the file), or `Binary` (decision
///   98). When the base body is not deleted (main created the file
///   meanwhile), it is a conflict (`Overlap`) that keeps the base, unless
///   both hold the same body. One the workspace deleted again merges
///   nothing. Its executable bit goes with its body: written when it
///   differs from the base's and the merge writes the body, or the base
///   already holds the same one.
/// - A derived override (row 180: `derived`, a whole-document or
///   workspace-local one, the only override of its document) is never
///   merged three-way and never conflicts. With `request.leave_derived`
///   it changes nothing, and when it would have written something its
///   path joins `left_derived`, since the next counted generation covers
///   the merged inputs. Otherwise, when its body
///   or bit changed since the claim and the base doesn't hold its body
///   already, its whole text (`Recreate`) or bytes (`Binary`), then a
///   changed bit, are written at the claim's `base_heads` (the base's
///   `heads` for a workspace-local file), each `BundleEdit` flagged
///   `derived` = `Mark`: a base change since the claim stays concurrent,
///   and the winner rule decides and tells the loser. Its record entry is
///   `whole`, with `base` = the document's `heads`.
/// - Concurrent claims that overlap (M3) give the text to the override
///   with the smaller id; the others conflict (`Overlap`).
///
/// A document's range splices, sorted and with touching splices joined, are
/// one `Text` change; an executable change follows. Every change is a
/// `BundleEdit` at the document's `heads` (a derived override's at its
/// claim's base heads), numbered from `next_req` in that
/// order. The record lists each changed document with `base` = its
/// `heads`, `target` empty, `ranges` = the claim anchors of its range
/// overrides that changed the base's text, and `whole` set when a
/// whole-document or workspace-local override or a whiteout changed it
/// (decision 108).
///
/// Each conflicting override, and a conflicting whiteout, is one
/// [`Conflict`]. Its `span` is the override's range at the target heads, or
/// for a whole-document override or a whiteout the whole target text;
/// `Collapsed { at }` when that range is empty (`at` its place at the
/// target heads: an anchor-lost override's gap, moved past the splices
/// before it), or `Collapsed { at: 0 }` when the target holds no text. A
/// document's conflicts, ordered by span (ranges by start and end, then
/// collapsed ones by `at`) and then subject, share one thread: anchored on
/// its address, `to` the union of their authors, opened by
/// `request.origin` with an `Ask` from `inputs.merger` listing each
/// conflict. Its id is `ThreadOpen::id`, and the bundle id is lvcs-doc's
/// `BundleId::of` over the four [`BundleInputs`], the one implementation,
/// which the registry checks a merge record's id with (decision 112).
#[must_use]
pub fn plan(input: MergeInput) -> MergePlan {
    planner::plan(input)
}

mirror

Source: crates/lvcs-ws/src/mirror/mod.rs.

/// Path of a file relative to the mirror root, `/`-separated.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WsPath(pub String);

/// SHA-256 of a file's bytes (`lvcs_vocab::ids::HASH`), computed by the
/// shell. Serializes as a 32-byte byte string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContentHash(#[serde(with = "cbor::fixed")] pub [u8; 32]);

/// What the shell found at a path.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiskState {
    /// A file, as the shell read it.
    Present {
        /// Its bytes.
        #[serde(with = "cbor::bytes")]
        content: Vec<u8>,
        /// SHA-256 of `content`.
        hash: ContentHash,
        /// Its executable bit.
        executable: bool,
    },
    /// No file (removed, or never written).
    Absent,
}

/// A `notify` change, a rescan after restart, or a refused
/// [`MirrorOutput::WriteFile`] or [`MirrorOutput::RemoveFile`], with what
/// the shell read.
///
/// Paths under `.lvcs/`, never-sync paths, and symbolic links never arrive.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FsChanged {
    /// The file's path.
    pub path: WsPath,
    /// What the shell read there.
    pub state: DiskState,
    /// The `Origin` of every `DiskEdit` this change produces, filled in by
    /// the shell: the daemon's default participant (decision 64) and the
    /// Unix time in milliseconds when it read the file.
    pub origin: Origin,
    /// The `heads` of the `WriteFile` or `RemoveFile` this change answers
    /// when the shell refused it because the file's hash wasn't its
    /// `expect`, and `None` for a `notify` change or a rescan (decision
    /// 75). Serializes as null or an array of 32-byte byte strings.
    #[serde(with = "cbor::opt_heads")]
    pub refused: Option<Vec<lvcs_doc::ChangeHash>>,
}

/// The shell finished a [`MirrorOutput::WriteFile`] or
/// [`MirrorOutput::RemoveFile`]; advances the disk base. After a removal
/// `hash` is `None` and `executable` is false.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WriteDone {
    /// The file's path.
    pub path: WsPath,
    /// SHA-256 of what was written; `None` after a removal.
    pub hash: Option<ContentHash>,
    /// The executable bit written; false after a removal.
    pub executable: bool,
    /// Heads of the document version that was written.
    #[serde(with = "cbor::heads")]
    pub heads: Vec<lvcs_doc::ChangeHash>,
}

/// A document's `DocOutput::Rendered`, addressed to the mirror by the shell,
/// which also hashes the rendered bytes (`None` when the body is deleted).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DocRendered {
    /// The file's path.
    pub path: WsPath,
    /// The rendered version's heads.
    #[serde(with = "cbor::heads")]
    pub heads: Vec<lvcs_doc::ChangeHash>,
    /// The rendered body.
    pub body: FileBody,
    /// SHA-256 of the rendered bytes; `None` for a deleted body.
    pub hash: Option<ContentHash>,
    /// The rendered executable bit.
    pub executable: bool,
    /// `DocOutput::Rendered.after`: the `stamp` of this mirror's `DiskEdit`
    /// whose apply produced the render, `None` for any other render
    /// (decision 75).
    pub after: Option<u64>,
}

/// The shell restarted this mirror: the first event of every incarnation
/// after the first, once the actor is rebuilt from its journal (design §2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Restarted;

/// A hook read the file at `path`, and the bytes it read hash to `hash`
/// (`track_read`, decision 117): the exact base of the participant's next
/// save of the file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Loaded {
    /// The file the hook read.
    pub path: WsPath,
    /// SHA-256 of the bytes it read.
    pub hash: ContentHash,
}

/// A hook's tool is about to edit the file at `path` (`track_edit`,
/// decision 117): which version does it edit?
///
/// Answered with exactly one [`MirrorOutput::Located`] echoing `req`, the
/// shell's id for the request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Locate {
    /// The shell's id for the request, which the answer echoes.
    pub req: RequestId,
    /// The file the tool edits.
    pub path: WsPath,
}

/// The protected-path gate held a render of the file at `path` (decision
/// 122): the document's `heads` and the render's `after`, which the
/// shell sends in place of the [`DocRendered`] it held.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Held {
    /// The file whose render was held.
    pub path: WsPath,
    /// The document's heads in the held render.
    #[serde(with = "cbor::heads")]
    pub heads: Vec<lvcs_doc::ChangeHash>,
    /// The held render's `after`.
    pub after: Option<u64>,
}

/// Mailbox events of the mirror actor.
///
/// The rules below rely on these obligations of the shell (`M1.daemon`):
/// - Only syncable paths arrive: never `.lvcs/`, never-sync paths, or
///   symbolic links. The mirror never filters.
/// - Each `Fs` event carries what the shell read, its hash, and the
///   `Origin` the shell built (decision 75). Each `DocOutput::Rendered` of a
///   mirrored file's document arrives, in the order the document emitted
///   them, as a `Doc` event with the rendered bytes' hash, or for a
///   protected path as what `protect::gate` decides (decision 122): the
///   render, a `Doc` render at the path's approved heads carrying the
///   gated render's `after`, or a `Held` carrying it. When approved heads
///   move, the shell re-gates the path's latest render, and that `Doc` or
///   `Held` carries no `after`, since the render's `after` already arrived.
///   Each render's `after` arrives exactly once, on a `Doc` or a `Held`, and
///   each `DiskEdit` reaches the document in the order the mirror emitted
///   them.
/// - A file's writes and removals run in the order the mirror emitted
///   them. Each first compares the file's hash with its `expect` and is
///   answered by exactly one `Written` or, when it is refused, one `Fs`
///   event with `refused` set to its heads (decision 75).
/// - A write's `Written` arrives before any `Fs` event the shell reads after
///   that write lands (decision 78).
/// - After a restart, `Restarted` is the first event, and the shell answers
///   the `Rescan`s before any document re-renders to the mirror (below).
/// - A `Loaded` carries the SHA-256 of the bytes a hook read from the file
///   (decision 117).
///
/// The document actor (`M1.doc`) answers each `DiskEdit` with exactly one
/// `Rendered` whose `after` is its stamp, whether the edit committed,
/// changed nothing, or faulted (decision 78).
///
/// Rules the actor keeps per file (design §8, decisions 63, 75, and 78). The
/// file's disk state is `(hash, executable)`, or absent, as last reported by
/// an `Fs` event or a `Written` confirmation, or as reset by `Restarted`; a
/// file never reported is absent.
/// - An `Fs` event that is no refusal and equals the file's disk state
///   changes nothing at all, not even the echo ledger (decision 236): a
///   repeated read of a landed write, or of a save already reported.
/// - The echo ledger holds the `(hash, executable)` pairs (`None` hash for a
///   removal) of writes the actor has emitted, oldest first, until their
///   `Written` arrives (below). An `Fs` event matching an entry is an echo,
///   unless it is a refusal that reports (below): the newest matching
///   entry and every older entry are dropped, since `notify` may coalesce
///   several writes into one event.
/// - Any other `Fs` event is reported to the document actor as the
///   `DiskEdit`s in which it differs from the disk state, all with the
///   event's `origin`: `Text` (valid UTF-8; its candidates, newest first,
///   are the heads of the file's in-flight writes, then of its unconfirmed
///   writes, then the ring's, leaving out every version that puts no file
///   on disk, such as a removal or the genesis render of a file opened
///   before it existed (design §8), and the document puts the author's
///   latest change ahead of them, decision 75) or `Binary` when the content
///   changed, `Removed` when the file is gone, and `Executable` when the bit
///   changed on a present file.
/// - `Binary`, `Executable`, and `Removed` carry as `base` the heads of the
///   file's newest disk base, the version the author last saw on disk, or
///   no heads when it has none, which the document reads as the genesis
///   heads (decision 78). No in-flight write is that base: the shell
///   delivers a write's `Written` before any `Fs` event it reads after the
///   write lands (decision 78), so an `Fs` event handled while the write is
///   in flight was read before it landed. Nor is an unconfirmed write,
///   which may never have landed.
/// - An `Fs` event with `refused` is a refusal, classified before the echo
///   test (decision 78). It ends the oldest in-flight write with those
///   heads, which never landed. If the file holds exactly what that write
///   would have put there, its version becomes a disk base as if it had
///   landed, and the event goes on to the echo test like any other `Fs`
///   event, where the write's own echo-ledger entry, while it remains,
///   makes it an echo. Any other refusal,
///   also one naming no in-flight write, drops one echo-ledger entry equal
///   to what its write would have put on disk and reports its content
///   (`Text`, `Binary`, or `Removed`) even when it equals the disk state or
///   an echo-ledger entry, plus `Executable` when the bit changed, so the
///   document decides what the file should hold (decision 75) and what the
///   shell found is never taken for an earlier write's echo. Its next
///   render is written over what the shell found unless the two are equal.
///   A write chained on the refused one expected its hash, so the shell
///   refuses that write too.
/// - Each `DiskEdit` gets the mirror's next `stamp`, one more than the
///   last, so stamps increase per mirror across all files and incarnations
///   (decision 75); an `Fs` event reporting both content and the bit emits
///   two `DiskEdit`s with consecutive stamps. The stamp joins the file's
///   pending saves (oldest first) until a render echoes it as `after`.
/// - Each disk base is `(heads, hash, executable)`; the ring keeps the last
///   `DISK_BASE_RING` distinct versions, and every `Written` confirmation
///   makes its version the newest (one already in the ring moves to the
///   front).
/// - A `Written` sets the disk state to what its write put on disk: the
///   shell delivers it before any `Fs` event it reads after the write lands
///   (decision 78), so every `Fs` event handled before it read the file
///   before the write landed, and none saw anything newer. This holds even
///   when such an event already dropped the write's echo-ledger entry, as an
///   older write's echo with the same content does. A `Written` also drops
///   the newest echo-ledger entry equal to what its write put on disk and
///   every older one (decision 236): every read after the write lands comes
///   after it, so a read equal to that state is the disk state, and one
///   that differs is a save, even one that restores those exact bytes after
///   an overwrite the watcher read first, which the entry would have taken
///   for an echo. Entries outlive their writes' `Written` only for writes
///   in flight at a `Restarted`, whose `Written` never arrives (below).
/// - A write or removal the actor emits is in flight until a `Written` or a
///   refusal with its heads arrives; each ends the oldest such write.
/// - A `Doc` render first ends the pending save its `after` names: every
///   `DiskEdit` gets exactly one render with its stamp as `after` (decision
///   78), and renders arrive in the order the document emitted them, so
///   every earlier save's render came first. While a save is still pending
///   the render is not written and changes nothing else (decision 75): a
///   render with `after` answers an earlier save, and one with no `after`
///   is a version the document produced before it applied the pending
///   save, so a render of an earlier version never overwrites a newer save
///   on disk.
/// - With no write in flight, a `Doc` render whose hash and executable bit
///   equal the file's disk state (for example, right after a save the
///   document just applied) is not written: its version becomes the newest
///   disk base, so the next save is diffed against the saved text and never
///   re-applied. While a write is in flight the disk state is about to
///   change, so a render equal to it (a revert, say) is written like any
///   other.
/// - Any other `Doc` render is written (`RemoveFile` when its body is
///   deleted), even one at the newest disk base's heads while the disk holds
///   something else, as after a `DiskEdit` that made no commit (decision
///   75): its `(hash, executable)` joins the echo ledger and the write is in
///   flight. A `WriteFile` or `RemoveFile` expects what the newest
///   in-flight write puts on disk (its hash, or no file for a removal), or
///   the disk state when none is in flight. A render with no hash for a
///   text or binary body is malformed and ignored.
/// - `Restarted`: the earlier incarnation's effects may not have run and its
///   watcher's events never arrive. For every tracked file the disk state is
///   reset to its newest disk base (absent when it has none), the echo
///   ledger is replaced by the `(hash, executable)` of its in-flight writes,
///   oldest first, those writes become unconfirmed writes (after any an
///   earlier restart left), pending saves are forgotten (the earlier
///   incarnation's `DiskEdit`s may never have reached the document, so their
///   renders may never arrive, and the re-render below has no `after`), and
///   a `Rescan` carrying that base is emitted. With a new watcher, the shell
///   then sends `Fs` for each tracked file whose `(hash, executable)`
///   differs from its `Rescan` and for each file on disk that got no
///   `Rescan`. Only after that rescan does every document re-render to the
///   mirror, so an edit made on disk while the mirror was down is reported
///   before a render could overwrite it.
/// - An unconfirmed write may have landed and been loaded by an editor, so
///   its heads stay candidates (after the in-flight writes', ahead of the
///   ring's, decision 75) until a disk base is pushed, which drops every
///   unconfirmed write. An echo of what an unconfirmed write puts on disk
///   makes it and every older unconfirmed write disk bases, oldest first,
///   as their `Written`s would have, and so drops the newer ones.
/// - So after `Restarted`, an in-flight write that landed before the crash
///   comes back in the rescan as an echo, even if its `notify` had already
///   drained it; one that never landed is written again by the re-render,
///   whose `Written` drains both entries. A confirmed write's entry is
///   dropped: the `Rescan` covers it, and its content can reappear only
///   through an edit. An entry outlives the rescan only when its version
///   never renders again, the one false-echo risk the ledger still takes.
/// - A save the document applied before the crash, whose render the mirror
///   never handled, is reported again by the rescan. Decision 75 absorbs
///   it: for a text save the document puts the heads of the author's
///   latest change first among the candidates, and a `DiskEdit` that
///   changes nothing relative to its chosen base (equal text, bytes, or
///   bit, or `Removed` on a deleted file) makes no commit. A refusal's
///   report of content the document already has at one of its candidates
///   is absorbed the same way.
/// - A `Loaded` looks its hash up among the file's text candidates, in
///   their order (its in-flight writes, newest first, then its unconfirmed
///   writes, then the ring's, leaving out every version that puts no file
///   on disk), and the first match's heads become the file's loaded heads
///   (decision 117). With no match, nothing is recorded and the file keeps
///   any loaded heads it had; an untracked file has no candidates and
///   stays untracked. The file's next `Text` report takes its loaded
///   heads, which `DiskChange::Text` carries to the document as `loaded`.
/// - A `Locate` changes nothing and is answered with one `Located`: the
///   file's loaded heads, else the heads of its newest disk base, else
///   none (an untracked file, or one no write or render has confirmed).
///   It leaves the loaded heads in place for the file's next `Text`
///   report (decision 117).
/// - A `Held` (decision 122) settles pending saves as a `Doc` render does,
///   ending the one its `after` names, and changes nothing else: it writes
///   nothing and doesn't advance the disk base, so the file keeps what it
///   holds. A release is no new event: the shell sends a `Doc` render at
///   the approved heads with the gated render's `after`, never `None`
///   in its place, so the rules above end the save it answers and write the
///   approved version over a save that committed nothing or faulted
///   (decision 78); with `None` that save would stay pending and every
///   later render of the file would be dropped. A re-gated `Doc` or `Held`
///   carries `None`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MirrorEvent {
    /// The shell read a file.
    Fs(FsChanged),
    /// The shell finished a write or removal.
    Written(WriteDone),
    /// A document rendered the file.
    Doc(DocRendered),
    /// The shell restarted the mirror.
    Restarted(Restarted),
    /// A hook read a file.
    Loaded(Loaded),
    /// A hook asks which version of a file its tool edits.
    Locate(Locate),
    /// The protected-path gate held a render.
    Held(Held),
}

/// Outputs of the mirror actor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MirrorOutput {
    /// Write `content` to `path` with the executable bit; the shell answers
    /// with [`WriteDone`]. `expect` is the content hash the mirror believes
    /// the file has when the shell runs this write (`None`: no file). The
    /// shell writes only if the file on disk has that hash; otherwise it
    /// leaves the file alone and answers with an [`FsChanged`] reporting
    /// what it found, `refused` set to `heads`, instead of [`WriteDone`], so
    /// the mirror never overwrites a save it hasn't seen (decision 75).
    WriteFile {
        /// The file to write.
        path: WsPath,
        /// The bytes to write.
        content: Vec<u8>,
        /// SHA-256 of `content`.
        hash: ContentHash,
        /// The executable bit to set.
        executable: bool,
        /// The heads of the document version written.
        heads: Vec<lvcs_doc::ChangeHash>,
        /// The content hash the file must have for the write to run;
        /// `None`: no file.
        expect: Option<ContentHash>,
    },
    /// Remove `path`. `expect` works as for `WriteFile`: when the file has
    /// hash `expect`, or for `None` when there is no file, the shell removes
    /// it (nothing to do when absent) and answers with [`WriteDone`] with no
    /// hash; otherwise it leaves the file alone and answers with an
    /// [`FsChanged`], `refused` set to `heads`, so a removal never deletes a
    /// save the mirror hasn't seen (decision 75).
    RemoveFile {
        /// The file to remove.
        path: WsPath,
        /// The heads of the document version removed, whose body is
        /// deleted.
        heads: Vec<lvcs_doc::ChangeHash>,
        /// The content hash the file must have for the removal to run;
        /// `None`: no file.
        expect: Option<ContentHash>,
    },
    /// A disk change that is not an echo, for the document actor at `path`.
    DiskEdit {
        /// The document's path.
        path: WsPath,
        /// The change.
        edit: DiskEdit,
    },
    /// The answer to [`Locate`] `req`: the version of the file at `path` a
    /// hook's tool edits (decision 117), the loaded heads or the newest
    /// disk base's, or `None` when the mirror knows neither.
    Located {
        /// The [`Locate`] request answered.
        req: RequestId,
        /// The file located.
        path: WsPath,
        /// The version the tool edits; `None` when the mirror knows none.
        heads: Option<Vec<lvcs_doc::ChangeHash>>,
    },
    /// Answers [`Restarted`] for one tracked file with its newest disk base:
    /// `hash` is `None` (and `executable` false) when the file should be
    /// absent. The shell reads `path` and sends `Fs` if it differs.
    Rescan {
        /// The tracked file.
        path: WsPath,
        /// Its newest disk base's content hash; `None` when it should be
        /// absent.
        hash: Option<ContentHash>,
        /// Its newest disk base's executable bit.
        executable: bool,
    },
}

protect

Source: crates/lvcs-ws/src/protect/mod.rs.

/// Whether the file at `path` is protected.
///
/// It is when lvcs-doc's `paths::protected()` (`DEFAULT_PROTECTED`, ASCII
/// case-insensitive) matches `path` with each `/`-separated segment in NFKC
/// and then lowercased (decision 130), because a filesystem that folds
/// Unicode case resolves `build.rſ` (U+017F) or `Cargo.loc` with U+212A
/// (KELVIN SIGN) to the protected file. A segment whose fold is not a name
/// (`.`, `..`, or holding a `/`, as U+2024 ONE DOT LEADER and U+FF0F
/// FULLWIDTH SOLIDUS can make it) keeps its own spelling, since no
/// filesystem resolves it as its fold, so the path keeps its segments and
/// every path the ASCII rule protects stays protected.
#[must_use]
pub fn is_protected(path: &str) -> bool {
    paths::protected().matches(&fold(path))
}

/// What the gate decides one render of a protected path to one mirror from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GateInput {
    /// The render's heads.
    pub render_heads: Vec<ChangeHash>,
    /// The path's approved heads, or `None` when it has none.
    pub approved: Option<Vec<ChangeHash>>,
    /// The signers of every change beyond the approved heads.
    pub signers_beyond: BTreeSet<ParticipantKey>,
    /// A change beyond the approved heads names no author id
    /// (`ReadAtOutcome::Read.unattributed`, or one the shell can't map).
    pub unattributed: bool,
    /// A change beyond the approved heads carries a bundle in its record
    /// (`ReadAtOutcome::Read.bundled`).
    pub bundled: bool,
    /// The mirror's author.
    pub mirror_author: ParticipantKey,
}

/// What the shell sends the mirror in place of the render.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GateDecision {
    /// The render itself.
    Deliver,
    /// A render at the approved heads, carrying the gated render's `after`.
    Release,
    /// A `Held`, carrying the gated render's `after`.
    Hold,
}

/// Decision 122's gate over one render of a protected path to one mirror.
///
/// `Hold` when `approved` is `None` or `unattributed` is set; `Deliver`
/// when `render_heads` equals `approved` (as sets of heads), or when every
/// signer beyond the approved heads is `mirror_author` and `bundled` is
/// false; `Release` when none of them is `mirror_author`; otherwise `Hold`.
#[must_use]
pub fn gate(input: &GateInput) -> GateDecision {
    let listed = match &input.approved {
        Some(listed) if !input.unattributed => listed,
        _ => return GateDecision::Hold,
    };
    let approved: BTreeSet<&ChangeHash> = listed.iter().collect();
    let render: BTreeSet<&ChangeHash> = input.render_heads.iter().collect();
    let author = &input.mirror_author;
    let authors_own = input.signers_beyond.iter().all(|signer| signer == author);
    if render == approved || (authors_own && !input.bundled) {
        GateDecision::Deliver
    } else if !input.signers_beyond.contains(author) {
        GateDecision::Release
    } else {
        GateDecision::Hold
    }
}

workspace-actor

Source: crates/lvcs-ws/src/workspace/actor.rs.

/// The workspace actor for one forked workspace.
///
/// Built once per incarnation; a restarted workspace actor is rebuilt by
/// replaying every earlier incarnation's journal entries into a fresh one
/// with effects discarded (`lvcs_actor::rebuild`, design §6).
pub type WorkspaceActor = WorkspaceViewStateMachine<WorkspaceContext>;

workspace

Source: crates/lvcs-ws/src/workspace/mod.rs.

/// The document a span of a view comes from. Ordered (decision 89): the
/// base, then overrides by workspace id and then override id.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Layer {
    /// The `file` document at the path: the bottom of the stack.
    Base,
    /// An override document of this workspace or of one it stacks on,
    /// with the id of the workspace it belongs to, so a stacked workspace
    /// can address any layer exactly (`OVERRIDE_ADDRESS` with both ids).
    Override {
        /// The workspace the override belongs to.
        workspace: WorkspaceId,
        /// The override's id.
        id: OverrideId,
    },
}

/// One run of a view's text taken from one layer.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
    /// Code points of the view text; never 0.
    pub len: usize,
    /// The layer the run is taken from.
    pub layer: Layer,
    /// Where the run starts in the layer's `/content`, in code points at
    /// that layer's heads in the view's frontier, so a workspace stacked on
    /// this one can place anchors made in any layer.
    pub from: usize,
}

/// Which layer each part of a view's body came from (design §8), so a save
/// through the mirror reaches the right layer and a file with no overrides
/// can be materialized as a clone of the base file.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpanMap {
    /// A text body: spans covering it in order, their lengths summing to
    /// its code points (none for an empty text).
    Text(Vec<Span>),
    /// A binary body, taken whole from one layer (binary files have no
    /// ranges, design §3).
    Binary(Layer),
    /// A deleted body: the base file is deleted and this workspace holds
    /// no range override of it, or a whiteout in this workspace or one it
    /// stacks on hides it.
    Deleted,
}

/// A workspace's composed view of one file (design §8, decision 82).
///
/// It is the base view with each claimed range swapped for its override's
/// text, whiteouts deleted, and workspace-local files added. A claim whose
/// whole
/// range the base deleted (`AnchorSpan::Collapsed { at }`) keeps its
/// override's text at the gap where the range was, overrides collapsed at
/// one gap in claim-time start order and then id (decisions 92 and 99).
/// While the base file is deleted, its anchor spans are ignored and the
/// file stays present as this workspace's range overrides of it, their
/// texts in claim-time start order and then id (decision 92).
/// `DocRendered`-shaped:
/// the shell hashes `body` and hands it to this workspace's mirror as a
/// `DocRendered`, and to every workspace stacked on this one as its base
/// render. `heads` is the view's frontier: the sorted, deduplicated union of
/// the heads of every document composed (the base view's and each
/// override's), and `layers` maps each of those documents, as the layer it
/// is, to its own heads (decision 89): the base view's layers and each
/// override of this workspace the view composes, so a claim on any layer
/// is made at the heads of the document that holds its characters. `after`
/// keeps the mirror's save rule (decisions 75 and 78):
/// the workspace emits exactly one render whose `after` is the `stamp` of
/// each [`ViewSave`], once every layer edit that save produced is answered
/// (for a save a part of which could not be routed, with the next save of
/// its path that lands whole), and `None` for every other render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewRendered {
    /// The file's path.
    pub path: WsPath,
    /// The view's frontier.
    #[serde(with = "cbor::heads")]
    pub heads: Vec<ChangeHash>,
    /// The composed body.
    pub body: FileBody,
    /// The view's executable bit.
    pub executable: bool,
    /// The `stamp` of the [`ViewSave`] this render answers; `None` for
    /// every other render.
    pub after: Option<u64>,
    /// Which layer each part of `body` came from.
    pub spans: SpanMap,
    /// Each document composed, as the layer it is, with its own heads.
    #[serde(with = "cbor::head_map")]
    pub layers: BTreeMap<Layer, Vec<ChangeHash>>,
}

/// An `edit` of `path` through this workspace's view (design §8).
///
/// `edit.base` is a view frontier (a [`ViewRendered`]'s `heads`, so it
/// mixes the heads of several documents), and the splices are code points
/// of the view text there. Copy-on-write (design §8): a splice spanning
/// several layers is split at their boundaries, its inserted text going to
/// the layer where it starts (decision 100); the part of the edit inside an
/// existing claim goes to that override; the parts outside every claim that
/// fall in one maximal unclaimed run of a layer, a run that a collapsed
/// override's gap bounds, so no claim holds one strictly inside it
/// (decision 100), are claimed first, together, as one range (`Claim`, a
/// new override) and go there; nothing reaches a base document (decision
/// 99). An insertion at a gap where overrides of this workspace with empty
/// text sit (emptied, or collapsed, decision 92) goes to the first of them
/// in claim-time start order, then id; else one at a gap that touches this
/// workspace's claims goes to the override whose claim ends just before the
/// gap, else to the one that starts just after it (an override whose text
/// is empty keeps its claim); only a gap touching none of them is claimed
/// (decisions 95 and 99). An edit or save that creates the file where the
/// base has none, or a deleted one (at genesis or after history, whited out
/// or not), makes it a workspace-local file, which merge-back recreates
/// (decision 98). One that creates the file in a view showing none of this
/// workspace's claims of it (whited out, or a binary base) claims the whole
/// document. Either way the claims of the path that the view hides leave
/// the index with it (range claims, and whole-document ones under a
/// concurrent whiteout, M3), since claims never overlap (decision 84). The
/// edit applies whole or not at all (decision 90): its parts outside every
/// claim are claimed and their new overrides initialized first, its claims
/// to one document one at a time (each after the previous `Claimed` and the
/// watch that includes it), and its parts are routed together only once
/// every claim has succeeded; a refused claim answers the edit `Rejected`
/// with nothing routed (an override already created keeps its claim-time
/// text). The edits and saves of one path run one at a time, each starting
/// once the one before is answered, and an edit whose `base` is older than
/// the path's newest view is rebased onto that view (the changes in between
/// are kept, as for concurrent edits) and split through its span map. An
/// edit that arrives during a merge-back (after its `ws.merge` request)
/// waits for the merge's answer, one that arrived before the request runs
/// before the merge, and one whose `base` is a view from before the last
/// merge-back of its path is rejected `UnknownBase` (decision 94).
/// `participant` is the connection's participant: the claimant key of any
/// override the edit claims. `edit.req` is unique in the workspace
/// (decision 91): the shell assigns it, increasing per workspace, and maps
/// the answer back to the connection's own request id, since a workspace
/// answers out of order across claim round trips; the actor rejects
/// (`Failed`) a `ViewEdit` that starts while an edit with its `req` is in
/// flight (started and not yet answered). Answered with exactly one
/// [`WorkspaceOutput::Answer`] for `edit.req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewEdit {
    /// The file edited.
    pub path: WsPath,
    /// The connection's participant: the claimant of any override the edit
    /// claims.
    pub participant: ParticipantKey,
    /// The edit, its splices in the view's code points at `edit.base`.
    pub edit: Edit,
}

/// A save through this workspace's mirror: the mirror's `DiskEdit` for
/// `path`, which the shell routes here instead of to a document (decision
/// 82).
///
/// It waits for its path like a [`ViewEdit`], is diffed against the view it
/// was made against (of the views named by the path's latest landed save,
/// its `candidates`, and the newest view, the one whose text differs least
/// from its own, ties to the earliest: the mirror's rule, decision 78; when
/// that is a view from before the last merge-back of its path, the first
/// view after the merge instead, reported as stale with
/// [`WorkspaceOutput::StaleSave`], decision 94), rebased onto the newest
/// view, split through the span map, and
/// routed like a [`ViewEdit`]: to the overrides it touches, claiming new
/// ones outside every claim, never to a base document. Each part reaches
/// its override as a text save whose `loaded` heads are the override's
/// heads in the view the part was made from, so the override diffs it
/// against exactly that text, as a view edit's part is applied at its
/// base, and never against its author's latest change there. A removal of
/// a file the view shows whites the path out, recording the heads of the
/// base render that the view its `base` names was composed from (decision 86;
/// none when it names no view the workspace emitted), so merge-back never
/// deletes a base edit that never reached disk; this workspace's range
/// claims of the path leave the index with it, so a whiteout and range
/// claims never coexist (decision 97). Concurrent whiteouts of one path
/// (M3: participants on two peers removing it) write one index key, and
/// the entry the index resolves it to is the path's whiteout, whose heads
/// govern merge-back and whose participant its conflict names; the
/// removal is done once the index shows the path whited out by anyone.
/// Such a whiteout may hide a whole-document claim of the path (a
/// removal of a view showing one goes to that override instead), which
/// leaves the index when the path is created again, as for a
/// [`ViewEdit`].
/// A derived save (row 180: `edit.derived` is `Mark`) is never split: one
/// that changes the file goes to the view's whole-document or
/// workspace-local override when it has one, and otherwise claims the
/// whole file, as a non-text body's save does, retiring the path's range
/// claims; either way the override takes its whole text with the save's
/// flag, and an override document treats a `Keep` save as derived when its
/// own `/meta/derived` is true. `participant` is
/// the daemon's default participant, the mirror's author (decision 64).
/// Like a view edit it applies whole or not at all. Answered with exactly
/// one [`ViewRendered`] whose `after` is `edit.stamp`. A save that can't
/// be routed (a claim refused or lost) is answered with the render of the
/// next save of its path that lands whole, which holds its text too, since
/// a save is the whole file: so the mirror never writes a view without a
/// save over it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewSave {
    /// The file saved.
    pub path: WsPath,
    /// The daemon's default participant, the mirror's author.
    pub participant: ParticipantKey,
    /// The mirror's `DiskEdit`.
    pub edit: DiskEdit,
}

/// How a document, or this workspace's view, answered an edit.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditOutcome {
    /// Applied; the document's heads (for a view, the view frontier) after
    /// it.
    Applied {
        /// The heads after the edit.
        #[serde(with = "cbor::heads")]
        heads: Vec<ChangeHash>,
    },
    /// Refused, for this reason.
    Rejected(RejectReason),
}

/// A document answered an `Edit` or `BundleEdit` this workspace routed to
/// it.
///
/// It is the document's `DocOutput::Changed` for that `req` (`Applied`) or
/// `DocOutput::Rejected`, which the shell routes back here by `address` and
/// `req`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LayerAnswer {
    /// The document that answered.
    pub address: String,
    /// The routed edit's request.
    pub req: RequestId,
    /// How the document answered.
    pub outcome: EditOutcome,
}

/// A stacked workspace's merge-back, landed on the workspace it stacks on
/// (design §8, decision 101's M3 deferral; frozen at the M3 freeze for
/// `M3.workspace`).
///
/// The base workspace absorbs it copy-on-write, as view edits of its own
/// overrides (claiming where its view's text comes from a lower layer, as
/// for a [`ViewEdit`]), so the work reaches `main` only through the base
/// workspace's own merge-back, and nothing is written to any base document
/// or the registry. `from` is the merging workspace and `req` its merge's
/// request, which [`Absorbed`] echoes; `origin` and `merger` are the
/// merging participant's (the claimant of any override the absorb claims).
/// `paths` holds each path the merge changes: `base` is the base
/// workspace's view frontier of the path the merging workspace composed
/// against (its base render's `heads`), and `changes` are written over the
/// base workspace's view there, splices in view code points, as a
/// document's [`BundleEdit`]s are over it.
///
/// The base workspace absorbs each path's changes in order, as ops of the
/// path that run after the view edits and saves that arrived before the
/// bundle, by `merger`: `Text` and `Recreate` as a view edit at `base`
/// (rebased onto the path's newest view, like any view edit; a `Recreate`
/// inserts its text into the view there, which shows the file deleted),
/// and `Binary`, `Executable`, and `Removed` as the matching save (a
/// removal whites the path out, recording the base render the view at
/// `base` was composed from). The path's first change is refused
/// `UnknownBase` when `base` is no view the base workspace emitted since
/// the last merge-back of the path, and `Failed` when it replaces the
/// whole file (`Recreate`, `Binary`, `Removed`) and the path's newest view
/// is no longer `base`, so it never drops an edit the merging workspace
/// didn't see (a recreate onto a view that shows the file again is a
/// conflict that keeps the base, decision 98), and `Failed` when the path
/// sets a binary body or an executable bit that the base workspace's view
/// would never take: one that swaps in range claims of the base workspace,
/// or would once the path's text edit claims ranges in it, shows its
/// base's body and bit (claims never combine, decision 84). A later change
/// that can't land then (after a restart) is refused `Failed` too. A
/// refused change is the path's outcome, and its later changes
/// never run; otherwise the path is `Applied` at the view frontier after
/// its last change, answered (except at a restart) only once the base
/// workspace has rendered that view, so the merging workspace's base
/// render already shows it.
///
/// `paths` is never empty: a stacked merge whose plan changes nothing
/// finishes without a bundle, so no `Absorbed` with no outcome answers a
/// merge, and the merging workspace counts a path an `Absorbed` doesn't
/// name as `Rejected(Failed)`. (The base workspace answers a bundle with
/// no path, which no workspace sends, at once with no outcome, and a path
/// with no change `Rejected(Failed)`.)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StackedBundle {
    /// The merging workspace.
    pub from: WorkspaceId,
    /// Its merge's request, which [`Absorbed`] echoes.
    pub req: RequestId,
    /// The merging participant's origin.
    pub origin: Origin,
    /// The merging participant: the claimant of any override the absorb
    /// claims.
    pub merger: ParticipantKey,
    /// Each path the merge changes, with its changes.
    pub paths: BTreeMap<WsPath, StackedChange>,
}

/// One path's changes in a [`StackedBundle`]: in the order they apply,
/// as `merge::plan` gives one document's (a body change, then an
/// executable bit), never empty.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StackedChange {
    /// The base workspace's view frontier of the path that the merging
    /// workspace composed against.
    #[serde(with = "cbor::heads")]
    pub base: Vec<ChangeHash>,
    /// The changes, in order, written over the view there.
    pub changes: Vec<BundleChange>,
}

/// The base workspace's answer to a [`StackedBundle`] from `from` for its
/// merge `req`.
///
/// It holds each path's outcome: `Applied` with the base workspace's view
/// frontier after it, or `Rejected` (`UnknownBase` for a `base` the base
/// workspace emitted no view at since the last merge-back of the path,
/// `Failed` when it can't absorb now, and otherwise the refusal of the
/// change's claim or routed edit, as for a view edit). Each change applies
/// whole or not at all, like a view edit, and a path's changes after a
/// refused one never run (a restart can end a path between its body change
/// and its executable bit); the stacked workspace closes its changeset only
/// when every path applied, and otherwise answers its merge with the
/// refused paths as `rejected` (decision 88).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Absorbed {
    /// The merging workspace.
    pub from: WorkspaceId,
    /// Its merge's request.
    pub req: RequestId,
    /// Each path's outcome.
    pub outcomes: BTreeMap<WsPath, EditOutcome>,
}

/// Mailbox events of the workspace actor.
///
/// The shell delivers each document's outputs to it in the order the
/// document emitted them (decision 96): a document's answer to a routed
/// edit (`Answered`) before the `Anchors` and the render (`Base` or
/// `Override`) that follow it. So an edit is shown by its override's first
/// render after the answer, and each render composes with the anchor
/// resolutions of its own heads.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum WorkspaceEvent {
    /// The base view of a path rendered. With a `main` base this is the
    /// `file` document's `DocOutput::Rendered`, which the shell wraps with
    /// the path, `after` = `None`, a span map of `Base` alone (one span
    /// from 0 over a text, `Binary(Base)`, or `Deleted`), and `layers` =
    /// `{ Base: heads }`. With a stacked base it is that workspace's
    /// [`ViewRendered`], whose `after` this workspace ignores. The shell
    /// delivers the base render of every path the index names (decision
    /// 96), a workspace-local file's included: the shell spawns its file
    /// document, which holding only its genesis renders `Deleted` at its
    /// genesis heads, where a merge-back creates the file. A merge-back
    /// starts only once every path it reads has rendered, so its changes
    /// apply at heads the document has.
    Base(ViewRendered),
    /// One of this workspace's override documents rendered; the shell adds
    /// its id.
    Override {
        /// The override document.
        id: OverrideId,
        /// Its render.
        render: OverrideRendered,
    },
    /// A watched document resolved this workspace's anchors.
    Anchors(AnchorsResolved),
    /// A document answered this workspace's claim: an anchored claim
    /// creates its override at once from the range the answer names, at
    /// the claim's heads (decision 87).
    Claimed(Claimed),
    /// This workspace's index document changed (or rendered).
    Index(IndexRendered),
    /// A document answered a routed edit or bundle edit.
    Answered(LayerAnswer),
    /// A document answered a [`WorkspaceOutput::ReadAt`]; the shell adds
    /// its address.
    Read {
        /// The document that answered.
        address: String,
        /// Its answer.
        done: ReadAtDone,
    },
    /// A client's edit through the view.
    Edit(ViewEdit),
    /// A save through this workspace's mirror.
    Save(ViewSave),
    /// A workspace stacked on this one merges back into it
    /// ([`WorkspaceOutput::MergeInto`]): absorb the bundle as
    /// [`StackedBundle`] says and answer with exactly one
    /// [`WorkspaceOutput::Absorbed`]. At `Restarted`, an absorbed change in
    /// flight is answered like a view edit, and the changes still waiting
    /// run afterwards.
    Absorb(StackedBundle),
    /// The answer of the workspace this one stacks on to this workspace's
    /// [`WorkspaceOutput::MergeInto`], which the running merge waits for;
    /// one for no merge waiting (a merge a restart ended) is ignored.
    Absorbed(Absorbed),
    /// Merge this workspace back (`ws.merge`): the actor reads what
    /// `merge::plan` needs, runs it, routes its changes, writes the record,
    /// closes the changeset (merge-back retires every override and
    /// whiteout it read, decision 94), and answers with
    /// [`WorkspaceOutput::Merged`]. A workspace stacked on another plans
    /// per path over that workspace's views and sends its changes there as
    /// one [`WorkspaceOutput::MergeInto`] (none when the plan changes
    /// nothing) instead of routing bundle edits, waits for the
    /// [`Absorbed`] answer, and writes no record. The view edits and saves
    /// that arrived before it run first: it starts once each of them is
    /// answered. Those that arrive during the merge wait for its answer.
    Merge(MergeRequest),
    /// The shell rebuilt this actor from its journal (`lvcs_actor::rebuild`):
    /// the first event of every incarnation after the first (decision 93).
    /// The documents the earlier incarnation's claims, routed edits and
    /// saves, reads, and bundle edits went to are new incarnations that
    /// will never answer them, and their watches ended (a watch lasts one
    /// document incarnation). So a view edit every part of which was
    /// answered `Applied` is answered `Applied` with the view frontier it
    /// settled at: the path's view as it composes now, each override the
    /// edit applied to at the heads its answer gave (decision 97); every
    /// other view edit in flight is answered `Rejected(Failed)`, and one
    /// whose parts were routed but not answered may have applied, so a
    /// client re-reads before retrying; every save in flight, or whose
    /// render is due or waits for the next save of its path, gets its one
    /// render with `after` = its stamp, showing the view as it composes
    /// now; a merge
    /// that has not written its record is answered with `rejected` naming
    /// the base documents it still waited on, `Failed` (no record is
    /// written), and one that has written it retires the overrides and
    /// whiteouts it read, sending again the retire changes the index
    /// render doesn't show yet, and is answered with its record once it
    /// shows them; pending claims and override
    /// initializations are cleared (an override already created keeps its
    /// claim-time text), every override this workspace created that has
    /// not rendered its claim gets its `CreateOverride` again (`OverrideInit`
    /// is idempotent, decision 97), and every `IndexChange` of the ended
    /// edits and saves (and of an earlier restart's) that the index render
    /// doesn't show yet is sent again in the order it was first sent (one
    /// already applied makes no commit); and every `Watch` is re-sent.
    /// Edits, saves, and merges still waiting start afterwards, an edit or
    /// save of a path once the index render shows that path's index
    /// changes, and a merge once it shows all of them.
    Restarted,
}

/// Outputs of the workspace actor.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceOutput {
    /// The composed view of one file, for this workspace's mirror and every
    /// workspace stacked on this one.
    Rendered(ViewRendered),
    /// Route `edit` to the override document at `address`, one of this
    /// workspace's (copy-on-write, design §8: an edit inside an existing
    /// claim goes to that override, and one outside every claim to the new
    /// override its claim created; nothing a workspace writes reaches a base
    /// document before merge-back). `edit.req` is chosen by the workspace
    /// actor, increasing per workspace; the shell keeps it apart from
    /// connection requests and answers with [`WorkspaceEvent::Answered`].
    Edit {
        /// The override document.
        address: String,
        /// The edit routed.
        edit: Edit,
    },
    /// Route part of a [`ViewSave`] to the override document at `address`,
    /// one of this workspace's, carrying the save's `stamp`: a text save of
    /// the override's whole new text, or a binary, executable-bit, or
    /// removal change of a whole-document or workspace-local override. The
    /// override answers with exactly one `OverrideRendered` whose `after` is
    /// that stamp. Never routed to a base document: nothing a workspace
    /// writes reaches one before merge-back.
    Save {
        /// The override document.
        address: String,
        /// The save's change, with its stamp.
        edit: DiskEdit,
    },
    /// Ask the document at `address`, which holds the claimed characters in
    /// the base view, to anchor a claim at that document's heads in the
    /// view (its layer's entry in [`ViewRendered::layers`], decision 89); it
    /// answers with `Claimed`.
    Claim {
        /// The document holding the claimed characters.
        address: String,
        /// The claim.
        claim: Claim,
    },
    /// Create the override document at `address` (`OVERRIDE_ADDRESS` with
    /// this workspace's id and the new override's id) and deliver `init` as
    /// its first event.
    CreateOverride {
        /// The new override document's address.
        address: String,
        /// Its first event.
        init: OverrideInit,
    },
    /// Set this workspace's watched anchors in the document at `address`.
    Watch {
        /// The watched document.
        address: String,
        /// This workspace's anchors in it.
        watch: WatchAnchors,
    },
    /// Change this workspace's index document (`WORKSPACE_INDEX_ADDRESS`).
    Index(IndexChange),
    /// Answer a [`ViewEdit`] with its `edit.req`.
    Answer {
        /// The answered edit's `edit.req`.
        req: RequestId,
        /// How the view answered it.
        outcome: EditOutcome,
    },
    /// A text [`ViewSave`] made against a view from before the last
    /// merge-back of its path is diffed against the first view after the
    /// merge instead (decision 94), mirroring `DocOutput::StaleSave`, for
    /// the daemon's `stale_save` push: emitted when the save starts, before
    /// anything it routes, with `chosen_base` the frontier of the view the
    /// save is diffed against and `newest_base` that of the newest view of
    /// `path` then.
    StaleSave {
        /// The saved file.
        path: WsPath,
        /// The frontier of the view the save is diffed against.
        chosen_base: Vec<ChangeHash>,
        /// The frontier of the path's newest view.
        newest_base: Vec<ChangeHash>,
    },
    /// Read the document at `address` at past heads, for a merge-back's
    /// inputs; `read.req` is chosen like a routed edit's, and the answer
    /// comes back as [`WorkspaceEvent::Read`].
    ReadAt {
        /// The document read.
        address: String,
        /// What to read.
        read: ReadAt,
    },
    /// Route one merge-back change of the plan to the base document at
    /// `address`; answered with [`WorkspaceEvent::Answered`].
    Bundle {
        /// The base document.
        address: String,
        /// The change.
        edit: BundleEdit,
    },
    /// Write the bundle's merge record to `.lvcs/registry`
    /// (`REGISTRY_ADDRESS`), once every change of the bundle is answered
    /// and applied. A bundle a change of which was rejected gets no record,
    /// so none of it becomes visible (decision 48), and opens no thread.
    Record(RecordBundle),
    /// Create the thread document at `address` (`THREAD_ADDRESS`) for a
    /// merge-back conflict and deliver `open` as its first event.
    OpenThread {
        /// The new thread document's address.
        address: String,
        /// Its first event.
        open: ThreadOpen,
    },
    /// A stacked merge-back: the shell delivers `bundle` to the workspace
    /// `into` (this workspace's base) as [`WorkspaceEvent::Absorb`], or,
    /// when that workspace isn't running, answers this one with an
    /// [`Absorbed`] refusing every path `Failed`. `bundle.paths` is never
    /// empty.
    MergeInto {
        /// The workspace this one stacks on.
        into: WorkspaceId,
        /// The bundle it absorbs.
        bundle: StackedBundle,
    },
    /// The answer to a [`WorkspaceEvent::Absorb`], which the shell delivers
    /// to `from` as [`WorkspaceEvent::Absorbed`].
    Absorbed(Absorbed),
    /// Answer a [`MergeRequest`]. A merge that writes its record closes the
    /// changeset first (decision 94): once every document it changed has
    /// rendered the change, every override and whiteout it read leaves the
    /// index (`IndexEdit::RemoveOverride`, `RemoveLocal` for a
    /// workspace-local file, `ClearWhiteout`), and the merge is answered
    /// when the index render shows that, after the views of its paths,
    /// which fall through to the merged base. (A clear is shown once the
    /// path no longer holds the whiteout the merge read: another
    /// participant's concurrent whiteout of the path, which the clear never
    /// saw, stays for the next merge-back.) A conflict's thread names the
    /// override documents, which keep their text. A merge a read of which
    /// was refused, or a bundle change of which was rejected, writes no
    /// record, opens no thread, and retires nothing: it is answered with
    /// [`MergeResult::rejected`] naming those documents (decision 88). A
    /// stacked merge closes its changeset the same way once every path it
    /// sent applied and its base renders show them, and is answered with
    /// a record it never writes, its documents the paths it changed, each
    /// with the workspace below's view frontiers before and after; a path
    /// refused or missing from the `Absorbed` answer is `rejected`.
    Merged(MergeResult),
}