sprawling-kernel 0.0.11

Every decision of a sprawling city - addresses, events, gates, plans and errors - with nothing that touches a disk.
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright (c) 2026 2youg1 and the sprawling contributors

//! The three registry books: Artifact, Asset,
//! Resident census. The load-bearing line is the Claim/Artifact split —
//! an unverified result is testimony, and `Artifact::verify` is the only
//! door between the two (player–referee in the type). The registry is a
//! value, not a store: state lives with the caller and is rebuilt from
//! the Ledger.
//! The part `crates/kernel/spec/Registry.lean` specifies this module.

use std::collections::{BTreeMap, BTreeSet};

use serde::{Deserialize, Serialize};

use crate::completion::{CITABLE, citable_words};
use crate::error::{AxCode, AxError};
use crate::event::EventRef;
use crate::locator::Locator;

/// Non-empty resident identity; the `role@building.n` grammar tightens
/// with city::resident.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ResidentId(String);

impl ResidentId {
    pub fn new(raw: impl Into<String>) -> Option<ResidentId> {
        let raw = raw.into();
        if raw.is_empty() {
            None
        } else {
            Some(ResidentId(raw))
        }
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

/// Testimony: a result nobody has verified yet. Claims never enter the
/// registry; they only carry what to verify.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Claim {
    pub locator: Locator,
    pub by: String,
}

/// A verified product. Private fields: the only constructor demands
/// verification evidence of a kind in `completion::CITABLE`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Artifact {
    locator: Locator,
    verified_by: EventRef,
}

impl Artifact {
    /// Sole constructor: player–referee in the type. Verification evidence
    /// must cite a kind in `completion::CITABLE`, else E_EVIDENCE_MISSING.
    pub fn verify(claim: Claim, evidence: EventRef) -> Result<Artifact, AxError> {
        if CITABLE.contains(&evidence.kind()) {
            return Ok(Artifact {
                locator: claim.locator,
                verified_by: evidence,
            });
        }
        Err(AxError::failure(
            AxCode::EvidenceMissing,
            "verify claim",
            claim.locator.to_string(),
        )
        .with_recovery(format!(
            "evidence must be a {} ref, got {:?}; run the verification and cite its result",
            citable_words()?,
            evidence.kind()
        )))
    }

    pub fn locator(&self) -> &Locator {
        &self.locator
    }

    pub fn verified_by(&self) -> &EventRef {
        &self.verified_by
    }
}

/// Deliberately exhaustive registration verdict.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegisterVerdict {
    Registered,
    AlreadyRegistered,
}

/// The three books. Keys are canonical locator spellings (Display is the
/// canonical form by the locator round-trip law).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Registry {
    artifacts: BTreeMap<String, Artifact>,
    assets: BTreeSet<String>,
    residents: BTreeSet<ResidentId>,
}

impl Registry {
    pub fn new() -> Registry {
        Registry::default()
    }

    pub fn register_artifact(&mut self, artifact: Artifact) -> RegisterVerdict {
        let key = artifact.locator.to_string();
        match self.artifacts.entry(key) {
            std::collections::btree_map::Entry::Occupied(_) => RegisterVerdict::AlreadyRegistered,
            std::collections::btree_map::Entry::Vacant(slot) => {
                slot.insert(artifact);
                RegisterVerdict::Registered
            }
        }
    }

    /// Promotion registers standing only; scoring is an instrument's (citysim). An
    /// unregistered locator cannot be an asset — assets are promoted
    /// artifacts, not free-floating paths.
    pub fn promote_asset(&mut self, locator: &Locator) -> Result<RegisterVerdict, AxError> {
        let key = locator.to_string();
        if !self.artifacts.contains_key(&key) {
            return Err(AxError::failure(AxCode::PathNotFound, "promote asset", key)
                .with_nearby(self.artifacts.keys().cloned().collect())
                .with_recovery("register the artifact first, then promote it"));
        }
        if self.assets.insert(key) {
            Ok(RegisterVerdict::Registered)
        } else {
            Ok(RegisterVerdict::AlreadyRegistered)
        }
    }

    pub fn register_resident(&mut self, id: ResidentId) -> RegisterVerdict {
        if self.residents.insert(id) {
            RegisterVerdict::Registered
        } else {
            RegisterVerdict::AlreadyRegistered
        }
    }

    pub fn artifact(&self, locator: &Locator) -> Option<&Artifact> {
        self.artifacts.get(&locator.to_string())
    }

    /// Whether this locator was promoted. The Discard door does not ask:
    /// a planned discard of an asset is as reversible as any other
    /// (`crates/kernel/spec/Discard.lean` §8-26).
    pub fn is_asset(&self, locator: &Locator) -> bool {
        self.assets.contains(&locator.to_string())
    }

    /// Address-level asset membership: a path is an
    /// asset when any registered asset is a `file:` locator at exactly
    /// this address (content pins differ, the place is the same).
    pub fn is_asset_at(&self, addr: &crate::address::Address) -> bool {
        self.assets.iter().any(|key| {
            matches!(
                Locator::parse(key),
                Ok(Locator::File { address, .. }) if address == *addr
            )
        })
    }

    pub fn is_resident(&self, id: &ResidentId) -> bool {
        self.residents.contains(id)
    }
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    reason = "test code"
)]
mod tests {
    use super::*;
    use crate::event::{EventDraft, EventKind, EventRecord, Payload, RunId, Seq, TimeMs};
    use crate::ledger::GENESIS_PREV;

    fn evidence(kind: EventKind) -> EventRef {
        let draft = EventDraft {
            run: RunId::CITY,
            t: TimeMs::new(0),
            who: "city".into(),
            addr: None,
            kind,
            data: Payload::empty(),
            ig: false,
        };
        EventRecord::from_draft(draft, Seq::FIRST, GENESIS_PREV).to_ref()
    }

    fn locator() -> Locator {
        Locator::parse(&format!("cas:b3-{}", "cd".repeat(32))).unwrap()
    }

    fn claim() -> Claim {
        Claim {
            locator: locator(),
            by: "worker@sim.1".into(),
        }
    }

    #[test]
    fn verification_accepts_only_citable_kinds() {
        assert!(Artifact::verify(claim(), evidence(EventKind::ToolResult)).is_ok());
        assert!(Artifact::verify(claim(), evidence(EventKind::ModelReturned)).is_ok());
        let err = Artifact::verify(claim(), evidence(EventKind::RunStarted)).unwrap_err();
        assert_eq!(err.code(), &AxCode::EvidenceMissing);
    }

    #[test]
    fn a_harness_answer_verifies_a_claim() {
        let verified = Artifact::verify(claim(), evidence(EventKind::HarnessAnswered));
        assert!(verified.is_ok(), "{verified:?}");
        assert_eq!(
            verified.unwrap().verified_by().kind(),
            EventKind::HarnessAnswered
        );
    }

    #[test]
    fn promotion_requires_prior_registration() {
        let mut registry = Registry::new();
        let err = registry.promote_asset(&locator()).unwrap_err();
        assert_eq!(err.code(), &AxCode::PathNotFound);
        let artifact = Artifact::verify(claim(), evidence(EventKind::ToolResult)).unwrap();
        assert_eq!(
            registry.register_artifact(artifact.clone()),
            RegisterVerdict::Registered
        );
        assert_eq!(
            registry.register_artifact(artifact),
            RegisterVerdict::AlreadyRegistered
        );
        assert_eq!(
            registry.promote_asset(&locator()).unwrap(),
            RegisterVerdict::Registered
        );
        assert!(registry.is_asset(&locator()));
    }

    /// The registry answers yes only for what it holds, and what it holds
    /// moves one step at a time: a registered artifact is found but is no
    /// asset until promoted, and a promoted `file:` asset makes its own
    /// address an asset and no other.
    #[test]
    fn each_query_answers_for_what_the_registry_holds_and_nothing_else() {
        let held = Locator::parse(&format!("file:lab/notes.md@{}", "ab".repeat(20))).unwrap();
        let place = crate::address::Address::parse("lab/notes.md").unwrap();
        let elsewhere = crate::address::Address::parse("lab/other.md").unwrap();
        let resident = ResidentId::new("worker@sim.1").unwrap();
        let mut registry = Registry::new();
        let ask = |registry: &Registry| {
            (
                registry.artifact(&held).cloned(),
                registry.is_asset(&held),
                registry.is_asset_at(&place),
                registry.is_asset_at(&elsewhere),
                registry.is_resident(&resident),
            )
        };
        assert_eq!(ask(&registry), (None, false, false, false, false));
        let artifact = Artifact::verify(
            Claim {
                locator: held.clone(),
                by: "worker@sim.1".into(),
            },
            evidence(EventKind::ToolResult),
        )
        .unwrap();
        registry.register_artifact(artifact.clone());
        assert_eq!(
            ask(&registry),
            (Some(artifact.clone()), false, false, false, false)
        );
        registry.promote_asset(&held).unwrap();
        assert_eq!(
            ask(&registry),
            (Some(artifact.clone()), true, true, false, false)
        );
        registry.register_resident(ResidentId::new("someone@else").unwrap());
        assert_eq!(ask(&registry), (Some(artifact), true, true, false, false));
    }

    #[test]
    fn the_census_deduplicates() {
        let mut registry = Registry::new();
        let id = ResidentId::new("worker@sim.1").unwrap();
        assert_eq!(
            registry.register_resident(id.clone()),
            RegisterVerdict::Registered
        );
        assert_eq!(
            registry.register_resident(id.clone()),
            RegisterVerdict::AlreadyRegistered
        );
        assert!(registry.is_resident(&id));
    }
}