jacquardSnapshot

← snapshot

7095 bytes
//! Process-lifetime state: hosted repos, identities, and the one registry.

use std::collections::BTreeMap;
use std::num::NonZeroU64;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};

use jac_core::{
    AgentId, AgentIdentity, Author, Clock as _, HumanId, HumanIdentity, OrgId, SystemClock,
};
use jac_decision::MemoryLedger;
use jac_object::MemoryObjectStore;
use jac_rendezvous::MemoryRegistry;
use jac_repo::{MemoryRefStore, RefName, Repo};

/// The engine specialised to the in-memory adapters this surface hosts.
pub type ServeRepo =
    Repo<MemoryObjectStore, MemoryRefStore, MemoryLedger, crate::hostclock::HostClock>;

/// One hosted repository and the identities registered against it.
#[derive(Debug)]
pub struct RepoEntry {
    /// The engine instance.
    pub repo: ServeRepo,
    /// URL-safe name, unique across the process.
    pub slug: String,
    /// The owning organisation's id.
    pub org_id: OrgId,
    /// The organisation's display name (the core has no org identity struct).
    pub org_name: String,
    /// The ref promotions land on by default.
    pub default_ref: RefName,
    /// Registered people. Declared, never authenticated.
    pub humans: BTreeMap<HumanId, HumanIdentity>,
    /// Registered agents.
    pub agents: BTreeMap<AgentId, AgentIdentity>,
    /// When the repo was initialised, in epoch milliseconds.
    pub created_at: i64,
    /// Review remarks raised against this repo, oldest first by id.
    pub remarks: BTreeMap<u64, Remark>,
}

/// What a reviewer meant by speaking up.
///
/// The distinction is load-bearing in the UI: a suggestion proposes a change
/// and can become a decision, a question asks for something a person knows and
/// cannot, and a note is context that should be recorded but demands nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemarkKind {
    /// "We should do X." Can be drafted into a decision.
    Suggestion,
    /// "Why is X?" Wants an answer, not a change.
    Question,
    /// "Worth knowing: X." Demands nothing.
    Note,
}

impl RemarkKind {
    /// Parses the wire label, rejecting anything unrecognised.
    #[must_use]
    pub fn parse(label: &str) -> Option<Self> {
        match label {
            "suggestion" => Some(Self::Suggestion),
            "question" => Some(Self::Question),
            "note" => Some(Self::Note),
            _ => None,
        }
    }

    /// The wire label.
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::Suggestion => "suggestion",
            Self::Question => "question",
            Self::Note => "note",
        }
    }
}

/// What the reviewer was looking at when they spoke.
///
/// The quote matters as much as the target: a suggestion reads very
/// differently next to the exact sentence that prompted it.
#[derive(Debug, Clone)]
pub struct RemarkAnchor {
    /// `decision`, `file`, `snapshot`, or `gate`.
    pub kind: String,
    /// Decision hex, file path, snapshot hex, or ref name.
    pub id: String,
    /// The exact span the reviewer had selected, when they had one.
    pub quote: Option<String>,
}

/// How a remark was settled.
#[derive(Debug, Clone)]
pub enum RemarkState {
    /// Still waiting on someone.
    Open,
    /// Turned into a decision, which now carries the work forward.
    Drafted {
        /// The decision that was proposed from this remark.
        decision: String,
    },
    /// Considered and closed without a change.
    Declined {
        /// Why, in the closer's own words.
        because: String,
    },
}

/// Something a reviewer said that should be acted on.
///
/// Deliberately *not* an engine object. Jacquard's objects carry provenance
/// hashed into their identity and only a `HumanIdentity` may attest to one; a
/// remark is conversation about the work rather than a claim about it. The
/// honest path from a remark to something durable is to draft it into a
/// decision, which a human then attests — so remarks live for the process
/// lifetime, like everything else this surface holds.
#[derive(Debug, Clone)]
pub struct Remark {
    /// Process-unique, monotonic.
    pub id: u64,
    /// What the reviewer was looking at when they spoke.
    pub anchor: RemarkAnchor,
    /// The reviewer's own words, verbatim.
    pub body: String,
    /// What they meant by speaking up.
    pub kind: RemarkKind,
    /// Who raised it. Declared, never authenticated.
    pub by: HumanId,
    /// Epoch milliseconds.
    pub at: i64,
    /// Open, or how it was settled.
    pub state: RemarkState,
}

impl RepoEntry {
    /// Display name for an author, when the actor is registered here.
    #[must_use]
    pub fn display_name(&self, author: Author) -> Option<String> {
        match author {
            Author::Human(id) => self.humans.get(&id).map(|h| h.display_name.clone()),
            Author::Agent(id) => self.agents.get(&id).map(|a| a.model.clone()),
            _ => None,
        }
    }
}

/// Everything the surface holds. In-memory only, by design: milestone 1 has
/// no persistent stores, and this surface does not pretend otherwise.
///
/// Locking invariant: handlers do purely synchronous work under these locks
/// and never hold a guard across an `.await`.
#[derive(Debug)]
pub struct AppState {
    /// Hosted repos, keyed by slug.
    pub repos: RwLock<BTreeMap<String, RepoEntry>>,
    /// The one shared rendezvous registry, standing in for the network.
    pub registry: Mutex<MemoryRegistry>,
    /// Source of all scalar ids (orgs, humans, agents), so every minted id
    /// is distinct across the process.
    next_id: AtomicU64,
    /// Telemetry handle. Disabled (a silent no-op) unless `S10_INGEST_URL`
    /// and `S10_INGEST_KEY` are set — see [`jac_telemetry::S10Config`].
    pub telemetry: jac_telemetry::Telemetry,
}

/// Cheaply cloneable handle to [`AppState`].
pub type SharedState = Arc<AppState>;

impl AppState {
    /// Empty state with telemetry disabled, ids starting from 1.
    #[must_use]
    pub const fn new() -> Self {
        Self::with_telemetry(jac_telemetry::Telemetry::disabled())
    }

    /// Empty state reporting through the given telemetry handle.
    #[must_use]
    pub const fn with_telemetry(telemetry: jac_telemetry::Telemetry) -> Self {
        Self {
            repos: RwLock::new(BTreeMap::new()),
            registry: Mutex::new(MemoryRegistry::new()),
            next_id: AtomicU64::new(1),
            telemetry,
        }
    }

    /// Wall clock, for the handlers that have no repo to ask.
    #[must_use]
    pub fn now_ms(&self) -> i64 {
        SystemClock.now().as_millis()
    }

    /// Mints a process-unique non-zero id.
    ///
    /// The counter starts at 1, so the fallback to `MIN` is unreachable; it
    /// exists solely to keep this panic-free.
    #[must_use]
    pub fn mint_id(&self) -> NonZeroU64 {
        NonZeroU64::new(self.next_id.fetch_add(1, Ordering::Relaxed)).unwrap_or(NonZeroU64::MIN)
    }
}

impl Default for AppState {
    fn default() -> Self {
        Self::new()
    }
}