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    /// Whether this locator was promoted. The Discard door does not ask:
153    /// a planned discard of an asset is as reversible as any other
154    /// (`crates/kernel/spec/Discard.lean` §8-26).
155    pub fn is_asset(&self, locator: &Locator) -> bool {
156        self.assets.contains(&locator.to_string())
157    }
158
159    /// Address-level asset membership: a path is an
160    /// asset when any registered asset is a `file:` locator at exactly
161    /// this address (content pins differ, the place is the same).
162    pub fn is_asset_at(&self, addr: &crate::address::Address) -> bool {
163        self.assets.iter().any(|key| {
164            matches!(
165                Locator::parse(key),
166                Ok(Locator::File { address, .. }) if address == *addr
167            )
168        })
169    }
170
171    pub fn is_resident(&self, id: &ResidentId) -> bool {
172        self.residents.contains(id)
173    }
174}
175
176#[cfg(test)]
177#[allow(
178    clippy::unwrap_used,
179    clippy::expect_used,
180    clippy::panic,
181    clippy::indexing_slicing,
182    reason = "test code"
183)]
184mod tests {
185    use super::*;
186    use crate::event::{EventDraft, EventKind, EventRecord, Payload, RunId, Seq, TimeMs};
187    use crate::ledger::GENESIS_PREV;
188
189    fn evidence(kind: EventKind) -> EventRef {
190        let draft = EventDraft {
191            run: RunId::CITY,
192            t: TimeMs::new(0),
193            who: "city".into(),
194            addr: None,
195            kind,
196            data: Payload::empty(),
197            ig: false,
198        };
199        EventRecord::from_draft(draft, Seq::FIRST, GENESIS_PREV).to_ref()
200    }
201
202    fn locator() -> Locator {
203        Locator::parse(&format!("cas:b3-{}", "cd".repeat(32))).unwrap()
204    }
205
206    fn claim() -> Claim {
207        Claim {
208            locator: locator(),
209            by: "worker@sim.1".into(),
210        }
211    }
212
213    #[test]
214    fn verification_accepts_only_citable_kinds() {
215        assert!(Artifact::verify(claim(), evidence(EventKind::ToolResult)).is_ok());
216        assert!(Artifact::verify(claim(), evidence(EventKind::ModelReturned)).is_ok());
217        let err = Artifact::verify(claim(), evidence(EventKind::RunStarted)).unwrap_err();
218        assert_eq!(err.code(), &AxCode::EvidenceMissing);
219    }
220
221    #[test]
222    fn a_harness_answer_verifies_a_claim() {
223        let verified = Artifact::verify(claim(), evidence(EventKind::HarnessAnswered));
224        assert!(verified.is_ok(), "{verified:?}");
225        assert_eq!(
226            verified.unwrap().verified_by().kind(),
227            EventKind::HarnessAnswered
228        );
229    }
230
231    #[test]
232    fn promotion_requires_prior_registration() {
233        let mut registry = Registry::new();
234        let err = registry.promote_asset(&locator()).unwrap_err();
235        assert_eq!(err.code(), &AxCode::PathNotFound);
236        let artifact = Artifact::verify(claim(), evidence(EventKind::ToolResult)).unwrap();
237        assert_eq!(
238            registry.register_artifact(artifact.clone()),
239            RegisterVerdict::Registered
240        );
241        assert_eq!(
242            registry.register_artifact(artifact),
243            RegisterVerdict::AlreadyRegistered
244        );
245        assert_eq!(
246            registry.promote_asset(&locator()).unwrap(),
247            RegisterVerdict::Registered
248        );
249        assert!(registry.is_asset(&locator()));
250    }
251
252    /// The registry answers yes only for what it holds, and what it holds
253    /// moves one step at a time: a registered artifact is found but is no
254    /// asset until promoted, and a promoted `file:` asset makes its own
255    /// address an asset and no other.
256    #[test]
257    fn each_query_answers_for_what_the_registry_holds_and_nothing_else() {
258        let held = Locator::parse(&format!("file:lab/notes.md@{}", "ab".repeat(20))).unwrap();
259        let place = crate::address::Address::parse("lab/notes.md").unwrap();
260        let elsewhere = crate::address::Address::parse("lab/other.md").unwrap();
261        let resident = ResidentId::new("worker@sim.1").unwrap();
262        let mut registry = Registry::new();
263        let ask = |registry: &Registry| {
264            (
265                registry.artifact(&held).cloned(),
266                registry.is_asset(&held),
267                registry.is_asset_at(&place),
268                registry.is_asset_at(&elsewhere),
269                registry.is_resident(&resident),
270            )
271        };
272        assert_eq!(ask(&registry), (None, false, false, false, false));
273        let artifact = Artifact::verify(
274            Claim {
275                locator: held.clone(),
276                by: "worker@sim.1".into(),
277            },
278            evidence(EventKind::ToolResult),
279        )
280        .unwrap();
281        registry.register_artifact(artifact.clone());
282        assert_eq!(
283            ask(&registry),
284            (Some(artifact.clone()), false, false, false, false)
285        );
286        registry.promote_asset(&held).unwrap();
287        assert_eq!(
288            ask(&registry),
289            (Some(artifact.clone()), true, true, false, false)
290        );
291        registry.register_resident(ResidentId::new("someone@else").unwrap());
292        assert_eq!(ask(&registry), (Some(artifact), true, true, false, false));
293    }
294
295    #[test]
296    fn the_census_deduplicates() {
297        let mut registry = Registry::new();
298        let id = ResidentId::new("worker@sim.1").unwrap();
299        assert_eq!(
300            registry.register_resident(id.clone()),
301            RegisterVerdict::Registered
302        );
303        assert_eq!(
304            registry.register_resident(id.clone()),
305            RegisterVerdict::AlreadyRegistered
306        );
307        assert!(registry.is_resident(&id));
308    }
309}