Skip to main content

kernel/
registry.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The three registry books: Artifact, Asset,
7//! Resident census. The load-bearing line is the Claim/Artifact split —
8//! an unverified result is testimony, and `Artifact::verify` is the only
9//! door between the two (player–referee in the type). The registry is a
10//! value, not a store: state lives with the caller and is rebuilt from
11//! the Ledger.
12//! The part `crates/kernel/spec/Registry.lean` specifies this module.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use serde::{Deserialize, Serialize};
17
18use crate::completion::{CITABLE, citable_words};
19use crate::error::{AxCode, AxError};
20use crate::event::EventRef;
21use crate::locator::Locator;
22
23/// Non-empty resident identity; the `role@building.n` grammar tightens
24/// with city::resident.
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub struct ResidentId(String);
28
29impl ResidentId {
30    pub fn new(raw: impl Into<String>) -> Option<ResidentId> {
31        let raw = raw.into();
32        if raw.is_empty() {
33            None
34        } else {
35            Some(ResidentId(raw))
36        }
37    }
38
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44/// Testimony: a result nobody has verified yet. Claims never enter the
45/// registry; they only carry what to verify.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Claim {
48    pub locator: Locator,
49    pub by: String,
50}
51
52/// A verified product. Private fields: the only constructor demands
53/// verification evidence of a kind in `completion::CITABLE`.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Artifact {
56    locator: Locator,
57    verified_by: EventRef,
58}
59
60impl Artifact {
61    /// Sole constructor: player–referee in the type. Verification evidence
62    /// must cite a kind in `completion::CITABLE`, else E_EVIDENCE_MISSING.
63    pub fn verify(claim: Claim, evidence: EventRef) -> Result<Artifact, AxError> {
64        if CITABLE.contains(&evidence.kind()) {
65            return Ok(Artifact {
66                locator: claim.locator,
67                verified_by: evidence,
68            });
69        }
70        Err(AxError::failure(
71            AxCode::EvidenceMissing,
72            "verify claim",
73            claim.locator.to_string(),
74        )
75        .with_recovery(format!(
76            "evidence must be a {} ref, got {:?}; run the verification and cite its result",
77            citable_words()?,
78            evidence.kind()
79        )))
80    }
81
82    pub fn locator(&self) -> &Locator {
83        &self.locator
84    }
85
86    pub fn verified_by(&self) -> &EventRef {
87        &self.verified_by
88    }
89}
90
91/// Deliberately exhaustive registration verdict.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum RegisterVerdict {
94    Registered,
95    AlreadyRegistered,
96}
97
98/// The three books. Keys are canonical locator spellings (Display is the
99/// canonical form by the locator round-trip law).
100#[derive(Debug, Clone, Default, PartialEq, Eq)]
101pub struct Registry {
102    artifacts: BTreeMap<String, Artifact>,
103    assets: BTreeSet<String>,
104    residents: BTreeSet<ResidentId>,
105}
106
107impl Registry {
108    pub fn new() -> Registry {
109        Registry::default()
110    }
111
112    pub fn register_artifact(&mut self, artifact: Artifact) -> RegisterVerdict {
113        let key = artifact.locator.to_string();
114        match self.artifacts.entry(key) {
115            std::collections::btree_map::Entry::Occupied(_) => RegisterVerdict::AlreadyRegistered,
116            std::collections::btree_map::Entry::Vacant(slot) => {
117                slot.insert(artifact);
118                RegisterVerdict::Registered
119            }
120        }
121    }
122
123    /// Promotion registers standing only; scoring is an instrument's (citysim). An
124    /// unregistered locator cannot be an asset — assets are promoted
125    /// artifacts, not free-floating paths.
126    pub fn promote_asset(&mut self, locator: &Locator) -> Result<RegisterVerdict, AxError> {
127        let key = locator.to_string();
128        if !self.artifacts.contains_key(&key) {
129            return Err(AxError::failure(AxCode::PathNotFound, "promote asset", key)
130                .with_nearby(self.artifacts.keys().cloned().collect())
131                .with_recovery("register the artifact first, then promote it"));
132        }
133        if self.assets.insert(key) {
134            Ok(RegisterVerdict::Registered)
135        } else {
136            Ok(RegisterVerdict::AlreadyRegistered)
137        }
138    }
139
140    pub fn register_resident(&mut self, id: ResidentId) -> RegisterVerdict {
141        if self.residents.insert(id) {
142            RegisterVerdict::Registered
143        } else {
144            RegisterVerdict::AlreadyRegistered
145        }
146    }
147
148    pub fn artifact(&self, locator: &Locator) -> Option<&Artifact> {
149        self.artifacts.get(&locator.to_string())
150    }
151
152    /// The Discard door's query face (7.2: hitting a registry asset
153    /// escalates).
154    pub fn is_asset(&self, locator: &Locator) -> bool {
155        self.assets.contains(&locator.to_string())
156    }
157
158    /// Address-level asset membership for the Discard door: a path is an
159    /// asset when any registered asset is a `file:` locator at exactly
160    /// this address (content pins differ, the place is the same).
161    pub fn is_asset_at(&self, addr: &crate::address::Address) -> bool {
162        self.assets.iter().any(|key| {
163            matches!(
164                Locator::parse(key),
165                Ok(Locator::File { address, .. }) if address == *addr
166            )
167        })
168    }
169
170    pub fn is_resident(&self, id: &ResidentId) -> bool {
171        self.residents.contains(id)
172    }
173}
174
175#[cfg(test)]
176#[allow(
177    clippy::unwrap_used,
178    clippy::expect_used,
179    clippy::panic,
180    clippy::indexing_slicing,
181    reason = "test code"
182)]
183mod tests {
184    use super::*;
185    use crate::event::{EventDraft, EventKind, EventRecord, Payload, RunId, Seq, TimeMs};
186    use crate::ledger::GENESIS_PREV;
187
188    fn evidence(kind: EventKind) -> EventRef {
189        let draft = EventDraft {
190            run: RunId::CITY,
191            t: TimeMs::new(0),
192            who: "city".into(),
193            addr: None,
194            kind,
195            data: Payload::empty(),
196            ig: false,
197        };
198        EventRecord::from_draft(draft, Seq::FIRST, GENESIS_PREV).to_ref()
199    }
200
201    fn locator() -> Locator {
202        Locator::parse(&format!("cas:b3-{}", "cd".repeat(32))).unwrap()
203    }
204
205    fn claim() -> Claim {
206        Claim {
207            locator: locator(),
208            by: "worker@sim.1".into(),
209        }
210    }
211
212    #[test]
213    fn verification_accepts_only_citable_kinds() {
214        assert!(Artifact::verify(claim(), evidence(EventKind::ToolResult)).is_ok());
215        assert!(Artifact::verify(claim(), evidence(EventKind::ModelReturned)).is_ok());
216        let err = Artifact::verify(claim(), evidence(EventKind::RunStarted)).unwrap_err();
217        assert_eq!(err.code(), &AxCode::EvidenceMissing);
218    }
219
220    #[test]
221    fn a_harness_answer_verifies_a_claim() {
222        let verified = Artifact::verify(claim(), evidence(EventKind::HarnessAnswered));
223        assert!(verified.is_ok(), "{verified:?}");
224        assert_eq!(
225            verified.unwrap().verified_by().kind(),
226            EventKind::HarnessAnswered
227        );
228    }
229
230    #[test]
231    fn promotion_requires_prior_registration() {
232        let mut registry = Registry::new();
233        let err = registry.promote_asset(&locator()).unwrap_err();
234        assert_eq!(err.code(), &AxCode::PathNotFound);
235        let artifact = Artifact::verify(claim(), evidence(EventKind::ToolResult)).unwrap();
236        assert_eq!(
237            registry.register_artifact(artifact.clone()),
238            RegisterVerdict::Registered
239        );
240        assert_eq!(
241            registry.register_artifact(artifact),
242            RegisterVerdict::AlreadyRegistered
243        );
244        assert_eq!(
245            registry.promote_asset(&locator()).unwrap(),
246            RegisterVerdict::Registered
247        );
248        assert!(registry.is_asset(&locator()));
249    }
250
251    #[test]
252    fn the_census_deduplicates() {
253        let mut registry = Registry::new();
254        let id = ResidentId::new("worker@sim.1").unwrap();
255        assert_eq!(
256            registry.register_resident(id.clone()),
257            RegisterVerdict::Registered
258        );
259        assert_eq!(
260            registry.register_resident(id.clone()),
261            RegisterVerdict::AlreadyRegistered
262        );
263        assert!(registry.is_resident(&id));
264    }
265}