1use 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Claim {
48 pub locator: Locator,
49 pub by: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Artifact {
56 locator: Locator,
57 verified_by: EventRef,
58}
59
60impl Artifact {
61 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum RegisterVerdict {
94 Registered,
95 AlreadyRegistered,
96}
97
98#[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 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 pub fn is_asset(&self, locator: &Locator) -> bool {
156 self.assets.contains(&locator.to_string())
157 }
158
159 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 #[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(®istry), (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(®istry),
284 (Some(artifact.clone()), false, false, false, false)
285 );
286 registry.promote_asset(&held).unwrap();
287 assert_eq!(
288 ask(®istry),
289 (Some(artifact.clone()), true, true, false, false)
290 );
291 registry.register_resident(ResidentId::new("someone@else").unwrap());
292 assert_eq!(ask(®istry), (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}