//! 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, SystemClock>;
/// 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()
}
}