sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit fde2294678edc117625e94c6735fac1250fe6b5c
parent 9e1462ae7e9acd785ca09acf0cc81049a68d262d
Author: triesap <tyson@radroots.org>
Date:   Sun,  5 Jul 2026 20:39:57 +0000

knowledge-sdk: add fluent knowledge builders

Diffstat:
Mcrates/events_codec_wasm/src/lib.rs | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/sdk/examples/sdk_v1_knowledge_prepare.rs | 43+++++++++++++++++++++----------------------
Mcrates/sdk/src/knowledge.rs | 798+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mcrates/sdk/src/lib.rs | 26+++++++++++++++-----------
Mcrates/sdk/tests/knowledge_public_api.rs | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/trade_bindings/src/dto.rs | 4++--
Mcrates/trade_bindings/src/lib.rs | 6+++---
Mpackages/events-bindings/src/generated/types.ts | 6++++--
Mpackages/trade-bindings/src/generated/types.ts | 6+++---
Mtools/xtask/src/dto_roots.rs | 9++++-----
Mtools/xtask/src/smoke.rs | 42+++++++++++++++++++++---------------------
11 files changed, 1136 insertions(+), 136 deletions(-)

diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs @@ -597,6 +597,7 @@ mod tests { RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, + RadrootsWikiArticleVersionRef, }; use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct}; use radroots_events::relay_auth::RadrootsRelayAuth; @@ -719,7 +720,10 @@ mod tests { summary: Some("Living soil basics".to_string()), topics: vec!["soil".to_string()], references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)], - forked_from: Vec::new(), + forked_from: vec![RadrootsWikiArticleVersionRef { + event_id: synthetic_event_id('b'), + address_ref: knowledge_address_ref(), + }], deferred_to: None, } } @@ -727,13 +731,7 @@ mod tests { fn sample_wiki_redirect() -> RadrootsWikiRedirect { RadrootsWikiRedirect { d_tag: "soil".to_string(), - target: radroots_events::RadrootsNostrEventRef { - id: synthetic_event_id('b'), - author: synthetic_pubkey('a'), - kind: KIND_WIKI_ARTICLE, - d_tag: Some("soil-health".to_string()), - relays: Some(vec!["wss://relay.example.test".to_string()]), - }, + target: knowledge_address_ref(), } } @@ -1241,6 +1239,14 @@ mod tests { }) } + fn has_exact_tag(tags: &[Vec<String>], expected: &[&str]) -> bool { + tags.iter().any(|tag| { + tag.iter() + .map(|entry| entry.as_str()) + .eq(expected.iter().copied()) + }) + } + #[test] fn bindings_reject_invalid_json() { let bindings: [fn(&str) -> Result<String, RadrootsJsValue>; 46] = [ @@ -1389,19 +1395,57 @@ mod tests { &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"), )); assert!(has_tag(&article_tags, "title", "Soil health")); + let fork_address = format!( + "{}:{}:soil-health", + KIND_WIKI_ARTICLE, + synthetic_pubkey('a') + ); + let fork_event_id = synthetic_event_id('b'); + assert!(has_exact_tag( + &article_tags, + &[ + "a", + fork_address.as_str(), + "wss://relay.example.test", + "fork" + ] + )); + assert!(has_exact_tag( + &article_tags, + &[ + "e", + fork_event_id.as_str(), + "wss://relay.example.test", + "fork" + ] + )); let redirect_tags = tags_json(wiki_redirect_tags( &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"), )); - assert!( - redirect_tags - .iter() - .any(|tag| tag.first() == Some(&"a".to_string())) + let redirect_address = format!( + "{}:{}:soil-health", + KIND_WIKI_ARTICLE, + synthetic_pubkey('a') ); + assert!(has_exact_tag( + &redirect_tags, + &["a", redirect_address.as_str(), "wss://relay.example.test"] + )); - assert_tags_json(wiki_merge_request_tags( + let merge_request = sample_wiki_merge_request(); + let merge_parts = + radroots_events_codec::knowledge::wiki_merge_request_to_wire_parts(&merge_request) + .expect("merge request parts"); + assert_eq!(merge_parts.content, "Merge synthetic soil article updates"); + let merge_tags = tags_json(wiki_merge_request_tags( &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"), )); + let source_event_id = synthetic_event_id('f'); + assert!(has_exact_tag( + &merge_tags, + &["e", source_event_id.as_str(), "", "source"] + )); let source_tags = tags_json(knowledge_source_tags( &serde_json::to_string(&sample_knowledge_source()).expect("source json"), @@ -1445,15 +1489,18 @@ mod tests { let manifest = contract_manifest_json().expect("manifest"); let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json"); - assert_eq!(manifest["schema_version"], 1); + assert_eq!(manifest["schema_version"], 2); assert_eq!(manifest["contract_count"], 11); - assert!( - manifest["contracts"] - .as_array() - .expect("contracts") - .iter() - .any(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) - ); + let claim_contract = manifest["contracts"] + .as_array() + .expect("contracts") + .iter() + .find(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) + .expect("claim contract"); + assert_eq!(claim_contract["sdk_builder_support"], true); + assert_eq!(claim_contract["sdk_draft_support"], true); + assert_eq!(claim_contract["wasm_tag_builder_support"], true); + assert_eq!(claim_contract["wasm_verified_decode_support"], true); } #[test] diff --git a/crates/sdk/examples/sdk_v1_knowledge_prepare.rs b/crates/sdk/examples/sdk_v1_knowledge_prepare.rs @@ -6,28 +6,9 @@ const CREATED_AT: u32 = 1_800_000_000; const RELAY: &str = "wss://relay.radroots.example"; fn main() -> Result<(), Box<dyn std::error::Error>> { - let claim = RadrootsKnowledgeClaim { - schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_owned(), - schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, - claim_type: "practice_effect".to_owned(), - text: "Cover crops improve soil structure.".to_owned(), - citation_spans: vec![RadrootsKnowledgeCitationSpan { - source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), - artifact_ref: None, - page_start: Some(12), - page_end: Some(13), - section_path: vec!["chapter-1".to_owned()], - quote_hash: Some(hex_64('5')), - chunk_id: Some("chunk-1".to_owned()), - }], - topics: vec!["cover-crops".to_owned()], - applies_to: vec!["local-food".to_owned()], - author_asserted_confidence: Some("medium".to_owned()), - supersedes: Vec::new(), - }; - - let parts = KnowledgeEventBuilder::new().knowledge_claim(&claim)?; - let draft = KnowledgeDraftBuilder::new(public_key_hex(), CREATED_AT).knowledge_claim(&claim)?; + let claim = claim_builder().build()?; + let parts = claim_builder().build_event()?; + let draft = claim_builder().build_draft(public_key_hex(), CREATED_AT)?; let signed = sign_parts(parts)?; let decoded = KnowledgeCodec::new().verify_and_decode_radroots_event(signed)?; let manifest = contract_manifest(); @@ -99,3 +80,21 @@ fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef { relays: Some(vec![RELAY.to_owned()]), } } + +fn claim_builder() -> RadrootsKnowledgeClaimBuilder { + RadrootsKnowledgeClaimBuilder::new() + .claim_type("practice_effect") + .text("Cover crops improve soil structure.") + .citation_span(RadrootsKnowledgeCitationSpan { + source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_owned()], + quote_hash: Some(hex_64('5')), + chunk_id: Some("chunk-1".to_owned()), + }) + .topic("cover-crops") + .applies_to("local-food") + .author_asserted_confidence("medium") +} diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs @@ -1,7 +1,13 @@ #[cfg(not(feature = "std"))] -use alloc::string::{String, ToString}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; #[cfg(feature = "std")] -use std::string::{String, ToString}; +use std::{ + string::{String, ToString}, + vec::Vec, +}; use core::fmt; @@ -26,8 +32,8 @@ pub use radroots_events::{ RadrootsKnowledgeObservationValue, RadrootsKnowledgeRelation, RadrootsKnowledgeReview, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, RadrootsRightsAssertion, RadrootsWikiArticle, - RadrootsWikiDTagError, RadrootsWikiMergeRequest, RadrootsWikiRedirect, - normalize_wiki_d_tag, validate_wiki_d_tag, + RadrootsWikiArticleVersionRef, RadrootsWikiDTagError, RadrootsWikiMergeRequest, + RadrootsWikiRedirect, normalize_wiki_d_tag, validate_wiki_d_tag, }, }; pub use radroots_events_codec::{ @@ -69,6 +75,7 @@ pub const KNOWLEDGE_FIELD_REPORT_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_FIELD_RE #[derive(Debug)] pub enum RadrootsSdkKnowledgeError { + Builder(RadrootsKnowledgeBuilderError), Encode(RadrootsEncodeError), Draft(RadrootsDraftError), Decode(RadrootsDecodeError), @@ -78,6 +85,7 @@ pub enum RadrootsSdkKnowledgeError { impl RadrootsSdkKnowledgeError { pub const fn code(&self) -> &'static str { match self { + Self::Builder(_) => "knowledge_builder", Self::Encode(_) => "knowledge_encode", Self::Draft(_) => "knowledge_draft", Self::Decode(_) => "knowledge_decode", @@ -87,6 +95,7 @@ impl RadrootsSdkKnowledgeError { pub fn inner_code(&self) -> &'static str { match self { + Self::Builder(error) => error.code(), Self::Encode(error) => error.code(), Self::Draft(error) => draft_error_code(error), Self::Decode(error) => error.code(), @@ -98,6 +107,7 @@ impl RadrootsSdkKnowledgeError { impl fmt::Display for RadrootsSdkKnowledgeError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::Builder(_) => formatter.write_str("knowledge event builder failed"), Self::Encode(_) => formatter.write_str("knowledge event encoding failed"), Self::Draft(_) => formatter.write_str("knowledge event draft preparation failed"), Self::Decode(_) => formatter.write_str("knowledge event verification or decode failed"), @@ -112,6 +122,7 @@ impl fmt::Display for RadrootsSdkKnowledgeError { impl std::error::Error for RadrootsSdkKnowledgeError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { + Self::Builder(error) => Some(error), Self::Encode(error) => Some(error), Self::Draft(error) => Some(error), Self::Decode(error) => Some(error), @@ -120,6 +131,50 @@ impl std::error::Error for RadrootsSdkKnowledgeError { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsKnowledgeBuilderError { + MissingField(&'static str), + InvalidField(&'static str), +} + +impl RadrootsKnowledgeBuilderError { + pub const fn code(&self) -> &'static str { + match self { + Self::MissingField(_) => "missing_field", + Self::InvalidField(_) => "invalid_field", + } + } + + pub const fn field(&self) -> &'static str { + match self { + Self::MissingField(field) | Self::InvalidField(field) => field, + } + } +} + +impl fmt::Display for RadrootsKnowledgeBuilderError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingField(field) => write!( + formatter, + "knowledge builder missing required field {field}" + ), + Self::InvalidField(field) => { + write!(formatter, "knowledge builder invalid field {field}") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsKnowledgeBuilderError {} + +impl From<RadrootsKnowledgeBuilderError> for RadrootsSdkKnowledgeError { + fn from(value: RadrootsKnowledgeBuilderError) -> Self { + Self::Builder(value) + } +} + impl From<RadrootsEncodeError> for RadrootsSdkKnowledgeError { fn from(value: RadrootsEncodeError) -> Self { Self::Encode(value) @@ -144,6 +199,691 @@ impl From<serde_json::Error> for RadrootsSdkKnowledgeError { } } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsWikiArticleBuilder { + d_tag: String, + title: Option<String>, + content_djot: Option<String>, + summary: Option<String>, + topics: Vec<String>, + references: Vec<RadrootsNostrEventRef>, + forked_from: Vec<RadrootsWikiArticleVersionRef>, + deferred_to: Option<RadrootsWikiArticleVersionRef>, +} + +impl RadrootsWikiArticleBuilder { + pub fn new(d_tag: impl Into<String>) -> Self { + Self { + d_tag: d_tag.into(), + ..Self::default() + } + } + + pub fn title(mut self, title: impl Into<String>) -> Self { + self.title = Some(title.into()); + self + } + + pub fn content_djot(mut self, content_djot: impl Into<String>) -> Self { + self.content_djot = Some(content_djot.into()); + self + } + + pub fn summary(mut self, summary: impl Into<String>) -> Self { + self.summary = Some(summary.into()); + self + } + + pub fn topic(mut self, topic: impl Into<String>) -> Self { + self.topics.push(topic.into()); + self + } + + pub fn reference(mut self, reference: RadrootsNostrEventRef) -> Self { + self.references.push(reference); + self + } + + pub fn forked_from(mut self, version_ref: RadrootsWikiArticleVersionRef) -> Self { + self.forked_from.push(version_ref); + self + } + + pub fn deferred_to(mut self, version_ref: RadrootsWikiArticleVersionRef) -> Self { + self.deferred_to = Some(version_ref); + self + } + + pub fn build(self) -> Result<RadrootsWikiArticle, RadrootsKnowledgeBuilderError> { + validate_builder_wiki_d_tag(&self.d_tag)?; + Ok(RadrootsWikiArticle { + d_tag: self.d_tag, + title: builder_required_string(self.title, "title")?, + content_djot: builder_required_string(self.content_djot, "content_djot")?, + summary: self.summary, + topics: self.topics, + references: self.references, + forked_from: self.forked_from, + deferred_to: self.deferred_to, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_wiki_article_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_wiki_article_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsWikiRedirectBuilder { + d_tag: String, + target: Option<RadrootsAddressableRef>, +} + +impl RadrootsWikiRedirectBuilder { + pub fn new(d_tag: impl Into<String>) -> Self { + Self { + d_tag: d_tag.into(), + target: None, + } + } + + pub fn target(mut self, target: RadrootsAddressableRef) -> Self { + self.target = Some(target); + self + } + + pub fn build(self) -> Result<RadrootsWikiRedirect, RadrootsKnowledgeBuilderError> { + validate_builder_wiki_d_tag(&self.d_tag)?; + Ok(RadrootsWikiRedirect { + d_tag: self.d_tag, + target: builder_required(self.target, "target")?, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_wiki_redirect_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_wiki_redirect_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsWikiMergeRequestBuilder { + target_article: Option<RadrootsAddressableRef>, + destination_pubkey: Option<String>, + base_version_event_id: Option<String>, + source_version_event_id: Option<String>, + explanation: Option<String>, +} + +impl RadrootsWikiMergeRequestBuilder { + pub fn new() -> Self { + Self { + target_article: None, + destination_pubkey: None, + base_version_event_id: None, + source_version_event_id: None, + explanation: None, + } + } + + pub fn target_article(mut self, target_article: RadrootsAddressableRef) -> Self { + self.target_article = Some(target_article); + self + } + + pub fn destination_pubkey(mut self, destination_pubkey: impl Into<String>) -> Self { + self.destination_pubkey = Some(destination_pubkey.into()); + self + } + + pub fn base_version_event_id(mut self, base_version_event_id: impl Into<String>) -> Self { + self.base_version_event_id = Some(base_version_event_id.into()); + self + } + + pub fn source_version_event_id(mut self, source_version_event_id: impl Into<String>) -> Self { + self.source_version_event_id = Some(source_version_event_id.into()); + self + } + + pub fn explanation(mut self, explanation: impl Into<String>) -> Self { + self.explanation = Some(explanation.into()); + self + } + + pub fn build(self) -> Result<RadrootsWikiMergeRequest, RadrootsKnowledgeBuilderError> { + Ok(RadrootsWikiMergeRequest { + target_article: builder_required(self.target_article, "target_article")?, + destination_pubkey: builder_required_string( + self.destination_pubkey, + "destination_pubkey", + )?, + base_version_event_id: self.base_version_event_id, + source_version_event_id: builder_required_string( + self.source_version_event_id, + "source_version_event_id", + )?, + explanation: self.explanation, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_wiki_merge_request_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_wiki_merge_request_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsKnowledgeSourceBuilder { + d_tag: String, + title: Option<String>, + source_type: Option<String>, + authors: Vec<String>, + publisher: Option<String>, + publication_year: Option<u16>, + edition: Option<String>, + canonical_url: Option<String>, + artifact_refs: Vec<RadrootsNostrEventRef>, + author_asserted_rights: Option<RadrootsRightsAssertion>, + topics: Vec<String>, + summary: Option<String>, +} + +impl RadrootsKnowledgeSourceBuilder { + pub fn new(d_tag: impl Into<String>) -> Self { + Self { + d_tag: d_tag.into(), + ..Self::default() + } + } + + pub fn title(mut self, title: impl Into<String>) -> Self { + self.title = Some(title.into()); + self + } + + pub fn source_type(mut self, source_type: impl Into<String>) -> Self { + self.source_type = Some(source_type.into()); + self + } + + pub fn author(mut self, author: impl Into<String>) -> Self { + self.authors.push(author.into()); + self + } + + pub fn publisher(mut self, publisher: impl Into<String>) -> Self { + self.publisher = Some(publisher.into()); + self + } + + pub fn publication_year(mut self, publication_year: u16) -> Self { + self.publication_year = Some(publication_year); + self + } + + pub fn edition(mut self, edition: impl Into<String>) -> Self { + self.edition = Some(edition.into()); + self + } + + pub fn canonical_url(mut self, canonical_url: impl Into<String>) -> Self { + self.canonical_url = Some(canonical_url.into()); + self + } + + pub fn artifact_ref(mut self, artifact_ref: RadrootsNostrEventRef) -> Self { + self.artifact_refs.push(artifact_ref); + self + } + + pub fn author_asserted_rights(mut self, rights: RadrootsRightsAssertion) -> Self { + self.author_asserted_rights = Some(rights); + self + } + + pub fn topic(mut self, topic: impl Into<String>) -> Self { + self.topics.push(topic.into()); + self + } + + pub fn summary(mut self, summary: impl Into<String>) -> Self { + self.summary = Some(summary.into()); + self + } + + pub fn build(self) -> Result<RadrootsKnowledgeSource, RadrootsKnowledgeBuilderError> { + validate_builder_wiki_d_tag(&self.d_tag)?; + Ok(RadrootsKnowledgeSource { + schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + d_tag: self.d_tag, + title: builder_required_string(self.title, "title")?, + source_type: builder_required_string(self.source_type, "source_type")?, + authors: self.authors, + publisher: self.publisher, + publication_year: self.publication_year, + edition: self.edition, + canonical_url: self.canonical_url, + artifact_refs: self.artifact_refs, + author_asserted_rights: self.author_asserted_rights, + topics: self.topics, + summary: self.summary, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_knowledge_source_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_knowledge_source_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsKnowledgeClaimBuilder { + claim_type: Option<String>, + text: Option<String>, + citation_spans: Vec<RadrootsKnowledgeCitationSpan>, + topics: Vec<String>, + applies_to: Vec<String>, + author_asserted_confidence: Option<String>, + supersedes: Vec<RadrootsNostrEventRef>, +} + +impl RadrootsKnowledgeClaimBuilder { + pub fn new() -> Self { + Self { + claim_type: None, + text: None, + citation_spans: Vec::new(), + topics: Vec::new(), + applies_to: Vec::new(), + author_asserted_confidence: None, + supersedes: Vec::new(), + } + } + + pub fn claim_type(mut self, claim_type: impl Into<String>) -> Self { + self.claim_type = Some(claim_type.into()); + self + } + + pub fn text(mut self, text: impl Into<String>) -> Self { + self.text = Some(text.into()); + self + } + + pub fn citation_span(mut self, citation_span: RadrootsKnowledgeCitationSpan) -> Self { + self.citation_spans.push(citation_span); + self + } + + pub fn topic(mut self, topic: impl Into<String>) -> Self { + self.topics.push(topic.into()); + self + } + + pub fn applies_to(mut self, applies_to: impl Into<String>) -> Self { + self.applies_to.push(applies_to.into()); + self + } + + pub fn author_asserted_confidence( + mut self, + author_asserted_confidence: impl Into<String>, + ) -> Self { + self.author_asserted_confidence = Some(author_asserted_confidence.into()); + self + } + + pub fn supersedes(mut self, supersedes: RadrootsNostrEventRef) -> Self { + self.supersedes.push(supersedes); + self + } + + pub fn build(self) -> Result<RadrootsKnowledgeClaim, RadrootsKnowledgeBuilderError> { + Ok(RadrootsKnowledgeClaim { + schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + claim_type: builder_required_string(self.claim_type, "claim_type")?, + text: builder_required_string(self.text, "text")?, + citation_spans: self.citation_spans, + topics: self.topics, + applies_to: self.applies_to, + author_asserted_confidence: self.author_asserted_confidence, + supersedes: self.supersedes, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_knowledge_claim_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_knowledge_claim_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsKnowledgeRelationBuilder { + subject: Option<RadrootsKnowledgeNodeRef>, + predicate: Option<String>, + object: Option<RadrootsKnowledgeNodeRef>, + support_refs: Vec<RadrootsNostrEventRef>, + author_asserted_confidence: Option<String>, + supersedes: Vec<RadrootsNostrEventRef>, +} + +impl RadrootsKnowledgeRelationBuilder { + pub fn new() -> Self { + Self { + subject: None, + predicate: None, + object: None, + support_refs: Vec::new(), + author_asserted_confidence: None, + supersedes: Vec::new(), + } + } + + pub fn subject(mut self, subject: RadrootsKnowledgeNodeRef) -> Self { + self.subject = Some(subject); + self + } + + pub fn predicate(mut self, predicate: impl Into<String>) -> Self { + self.predicate = Some(predicate.into()); + self + } + + pub fn object(mut self, object: RadrootsKnowledgeNodeRef) -> Self { + self.object = Some(object); + self + } + + pub fn support_ref(mut self, support_ref: RadrootsNostrEventRef) -> Self { + self.support_refs.push(support_ref); + self + } + + pub fn author_asserted_confidence( + mut self, + author_asserted_confidence: impl Into<String>, + ) -> Self { + self.author_asserted_confidence = Some(author_asserted_confidence.into()); + self + } + + pub fn supersedes(mut self, supersedes: RadrootsNostrEventRef) -> Self { + self.supersedes.push(supersedes); + self + } + + pub fn build(self) -> Result<RadrootsKnowledgeRelation, RadrootsKnowledgeBuilderError> { + Ok(RadrootsKnowledgeRelation { + schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + subject: builder_required(self.subject, "subject")?, + predicate: builder_required_string(self.predicate, "predicate")?, + object: builder_required(self.object, "object")?, + support_refs: self.support_refs, + author_asserted_confidence: self.author_asserted_confidence, + supersedes: self.supersedes, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_knowledge_relation_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_knowledge_relation_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsKnowledgeReviewBuilder { + target: Option<RadrootsKnowledgeReviewTarget>, + reviewer_role: Option<String>, + verdict: Option<String>, + scores: Vec<RadrootsKnowledgeReviewScore>, + notes: Option<String>, + evidence_refs: Vec<RadrootsNostrEventRef>, +} + +impl RadrootsKnowledgeReviewBuilder { + pub fn new() -> Self { + Self { + target: None, + reviewer_role: None, + verdict: None, + scores: Vec::new(), + notes: None, + evidence_refs: Vec::new(), + } + } + + pub fn target(mut self, target: RadrootsKnowledgeReviewTarget) -> Self { + self.target = Some(target); + self + } + + pub fn reviewer_role(mut self, reviewer_role: impl Into<String>) -> Self { + self.reviewer_role = Some(reviewer_role.into()); + self + } + + pub fn verdict(mut self, verdict: impl Into<String>) -> Self { + self.verdict = Some(verdict.into()); + self + } + + pub fn score(mut self, score: RadrootsKnowledgeReviewScore) -> Self { + self.scores.push(score); + self + } + + pub fn notes(mut self, notes: impl Into<String>) -> Self { + self.notes = Some(notes.into()); + self + } + + pub fn evidence_ref(mut self, evidence_ref: RadrootsNostrEventRef) -> Self { + self.evidence_refs.push(evidence_ref); + self + } + + pub fn build(self) -> Result<RadrootsKnowledgeReview, RadrootsKnowledgeBuilderError> { + Ok(RadrootsKnowledgeReview { + schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + target: builder_required(self.target, "target")?, + reviewer_role: builder_required_string(self.reviewer_role, "reviewer_role")?, + verdict: builder_required_string(self.verdict, "verdict")?, + scores: self.scores, + notes: self.notes, + evidence_refs: self.evidence_refs, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_knowledge_review_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_knowledge_review_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RadrootsKnowledgeFieldReportBuilder { + report_type: Option<String>, + title: Option<String>, + summary: Option<String>, + context: Option<RadrootsKnowledgeFieldContext>, + observations: Vec<RadrootsKnowledgeObservation>, + artifact_refs: Vec<RadrootsNostrEventRef>, + related_refs: Vec<RadrootsNostrEventRef>, + limitations: Vec<String>, +} + +impl RadrootsKnowledgeFieldReportBuilder { + pub fn new() -> Self { + Self { + report_type: None, + title: None, + summary: None, + context: None, + observations: Vec::new(), + artifact_refs: Vec::new(), + related_refs: Vec::new(), + limitations: Vec::new(), + } + } + + pub fn report_type(mut self, report_type: impl Into<String>) -> Self { + self.report_type = Some(report_type.into()); + self + } + + pub fn title(mut self, title: impl Into<String>) -> Self { + self.title = Some(title.into()); + self + } + + pub fn summary(mut self, summary: impl Into<String>) -> Self { + self.summary = Some(summary.into()); + self + } + + pub fn context(mut self, context: RadrootsKnowledgeFieldContext) -> Self { + self.context = Some(context); + self + } + + pub fn observation(mut self, observation: RadrootsKnowledgeObservation) -> Self { + self.observations.push(observation); + self + } + + pub fn artifact_ref(mut self, artifact_ref: RadrootsNostrEventRef) -> Self { + self.artifact_refs.push(artifact_ref); + self + } + + pub fn related_ref(mut self, related_ref: RadrootsNostrEventRef) -> Self { + self.related_refs.push(related_ref); + self + } + + pub fn limitation(mut self, limitation: impl Into<String>) -> Self { + self.limitations.push(limitation.into()); + self + } + + pub fn build(self) -> Result<RadrootsKnowledgeFieldReport, RadrootsKnowledgeBuilderError> { + if self.observations.is_empty() { + return Err(RadrootsKnowledgeBuilderError::MissingField("observations")); + } + Ok(RadrootsKnowledgeFieldReport { + schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + report_type: builder_required_string(self.report_type, "report_type")?, + title: builder_required_string(self.title, "title")?, + summary: self.summary, + context: builder_required(self.context, "context")?, + observations: self.observations, + artifact_refs: self.artifact_refs, + related_refs: self.related_refs, + limitations: self.limitations, + }) + } + + pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { + Ok(build_knowledge_field_report_event(&self.build()?)?) + } + + pub fn build_draft( + self, + expected_pubkey: impl AsRef<str>, + created_at: u32, + ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> { + Ok(prepare_knowledge_field_report_draft( + &self.build()?, + expected_pubkey, + created_at, + )?) + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct KnowledgeEventBuilder; @@ -520,6 +1260,37 @@ fn draft_error_code(error: &RadrootsDraftError) -> &'static str { } } +fn builder_required<T>( + value: Option<T>, + field: &'static str, +) -> Result<T, RadrootsKnowledgeBuilderError> { + value.ok_or(RadrootsKnowledgeBuilderError::MissingField(field)) +} + +fn builder_required_string( + value: Option<String>, + field: &'static str, +) -> Result<String, RadrootsKnowledgeBuilderError> { + builder_non_empty_string(builder_required(value, field)?, field) +} + +fn builder_non_empty_string( + value: String, + field: &'static str, +) -> Result<String, RadrootsKnowledgeBuilderError> { + if value.trim().is_empty() { + Err(RadrootsKnowledgeBuilderError::MissingField(field)) + } else { + Ok(value) + } +} + +fn validate_builder_wiki_d_tag(value: &str) -> Result<(), RadrootsKnowledgeBuilderError> { + validate_wiki_d_tag(value) + .map(|_| ()) + .map_err(|_| RadrootsKnowledgeBuilderError::InvalidField("d_tag")) +} + pub mod prelude { pub use super::{ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, @@ -535,19 +1306,24 @@ pub mod prelude { RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RADROOTS_WIKI_D_TAG_MAX_LEN, RadrootsAddressableRef, RadrootsContractValidatedEvent, RadrootsDecodeError, RadrootsDecodedEvent, RadrootsDraftError, RadrootsEncodeError, RadrootsFrozenEventDraft, RadrootsIdVerifiedEvent, - RadrootsKnowledgeChangeProposal, RadrootsKnowledgeCitationSpan, RadrootsKnowledgeClaim, + RadrootsKnowledgeBuilderError, RadrootsKnowledgeChangeProposal, + RadrootsKnowledgeCitationSpan, RadrootsKnowledgeClaim, RadrootsKnowledgeClaimBuilder, RadrootsKnowledgeContractManifest, RadrootsKnowledgeContractManifestEntry, - RadrootsKnowledgeFieldContext, RadrootsKnowledgeFieldReport, RadrootsKnowledgeLocation, + RadrootsKnowledgeFieldContext, RadrootsKnowledgeFieldReport, + RadrootsKnowledgeFieldReportBuilder, RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeManifestCodecSupport, RadrootsKnowledgeManifestDiscriminator, RadrootsKnowledgeManifestTagContract, RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, - RadrootsKnowledgeRelation, RadrootsKnowledgeReview, RadrootsKnowledgeReviewScope, - RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, + RadrootsKnowledgeRelation, RadrootsKnowledgeRelationBuilder, RadrootsKnowledgeReview, + RadrootsKnowledgeReviewBuilder, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, + RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, RadrootsKnowledgeSourceBuilder, RadrootsNip01VerificationError, RadrootsNostrEvent, RadrootsNostrEventRef, RadrootsRightsAssertion, RadrootsSdkKnowledgeError, RadrootsSignatureVerifiedEvent, - RadrootsWikiArticle, RadrootsWikiDTagError, RadrootsWikiMergeRequest, RadrootsWikiRedirect, - WIKI_ARTICLE_CONTRACT_ID, WIKI_MERGE_REQUEST_CONTRACT_ID, WIKI_REDIRECT_CONTRACT_ID, - WireEventParts, build_knowledge_claim_event, build_knowledge_field_report_event, + RadrootsWikiArticle, RadrootsWikiArticleBuilder, RadrootsWikiArticleVersionRef, + RadrootsWikiDTagError, RadrootsWikiMergeRequest, RadrootsWikiMergeRequestBuilder, + RadrootsWikiRedirect, RadrootsWikiRedirectBuilder, WIKI_ARTICLE_CONTRACT_ID, + WIKI_MERGE_REQUEST_CONTRACT_ID, WIKI_REDIRECT_CONTRACT_ID, WireEventParts, + build_knowledge_claim_event, build_knowledge_field_report_event, build_knowledge_relation_event, build_knowledge_review_event, build_knowledge_source_event, build_wiki_article_event, build_wiki_merge_request_event, build_wiki_redirect_event, contract_manifest, contract_manifest_json, contract_manifest_sha256, normalize_wiki_d_tag, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -107,18 +107,22 @@ pub use crate::knowledge::{ RADROOTS_KNOWLEDGE_SCHEMA_VERSION, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RADROOTS_WIKI_D_TAG_MAX_LEN, RadrootsAddressableRef, RadrootsContractValidatedEvent, RadrootsDecodeError, RadrootsDecodedEvent, RadrootsDraftError, RadrootsEncodeError, - RadrootsFrozenEventDraft, RadrootsIdVerifiedEvent, RadrootsKnowledgeChangeProposal, - RadrootsKnowledgeCitationSpan, RadrootsKnowledgeClaim, RadrootsKnowledgeContractManifest, + RadrootsFrozenEventDraft, RadrootsIdVerifiedEvent, RadrootsKnowledgeBuilderError, + RadrootsKnowledgeChangeProposal, RadrootsKnowledgeCitationSpan, RadrootsKnowledgeClaim, + RadrootsKnowledgeClaimBuilder, RadrootsKnowledgeContractManifest, RadrootsKnowledgeContractManifestEntry, RadrootsKnowledgeFieldContext, - RadrootsKnowledgeFieldReport, RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, - RadrootsKnowledgeManifestCodecSupport, RadrootsKnowledgeManifestDiscriminator, - RadrootsKnowledgeManifestTagContract, RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation, - RadrootsKnowledgeObservationValue, RadrootsKnowledgeRelation, RadrootsKnowledgeReview, - RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, - RadrootsKnowledgeSource, RadrootsNip01VerificationError, RadrootsNostrEvent, - RadrootsNostrEventRef, RadrootsRightsAssertion, RadrootsSdkKnowledgeError, - RadrootsSignatureVerifiedEvent, RadrootsWikiArticle, RadrootsWikiDTagError, - RadrootsWikiMergeRequest, RadrootsWikiRedirect, WIKI_ARTICLE_CONTRACT_ID, + RadrootsKnowledgeFieldReport, RadrootsKnowledgeFieldReportBuilder, RadrootsKnowledgeLocation, + RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeManifestCodecSupport, + RadrootsKnowledgeManifestDiscriminator, RadrootsKnowledgeManifestTagContract, + RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, + RadrootsKnowledgeRelation, RadrootsKnowledgeRelationBuilder, RadrootsKnowledgeReview, + RadrootsKnowledgeReviewBuilder, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, + RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, RadrootsKnowledgeSourceBuilder, + RadrootsNip01VerificationError, RadrootsNostrEvent, RadrootsNostrEventRef, + RadrootsRightsAssertion, RadrootsSdkKnowledgeError, RadrootsSignatureVerifiedEvent, + RadrootsWikiArticle, RadrootsWikiArticleBuilder, RadrootsWikiArticleVersionRef, + RadrootsWikiDTagError, RadrootsWikiMergeRequest, RadrootsWikiMergeRequestBuilder, + RadrootsWikiRedirect, RadrootsWikiRedirectBuilder, WIKI_ARTICLE_CONTRACT_ID, WIKI_MERGE_REQUEST_CONTRACT_ID, WIKI_REDIRECT_CONTRACT_ID, WireEventParts, build_knowledge_claim_event, build_knowledge_field_report_event, build_knowledge_relation_event, build_knowledge_review_event, build_knowledge_source_event, diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs @@ -9,26 +9,16 @@ const RELAY: &str = "wss://relay.radroots.example"; #[test] fn knowledge_prelude_builds_mvp_wire_parts_without_codec_imports() { - let builder = KnowledgeEventBuilder::new(); - - let article = builder.wiki_article(&wiki_article()).expect("article"); - let redirect = builder.wiki_redirect(&wiki_redirect()).expect("redirect"); - let merge_request = builder - .wiki_merge_request(&wiki_merge_request()) + let article = article_builder().build_event().expect("article"); + let redirect = redirect_builder().build_event().expect("redirect"); + let merge_request = merge_request_builder() + .build_event() .expect("merge request"); - let source = builder - .knowledge_source(&knowledge_source()) - .expect("source"); - let claim = builder.knowledge_claim(&knowledge_claim()).expect("claim"); - let relation = builder - .knowledge_relation(&knowledge_relation()) - .expect("relation"); - let review = builder - .knowledge_review(&knowledge_review()) - .expect("review"); - let field_report = builder - .knowledge_field_report(&knowledge_field_report()) - .expect("field report"); + let source = source_builder().build_event().expect("source"); + let claim = claim_builder().build_event().expect("claim"); + let relation = relation_builder().build_event().expect("relation"); + let review = review_builder().build_event().expect("review"); + let field_report = field_report_builder().build_event().expect("field report"); assert_eq!(article.kind, KIND_WIKI_ARTICLE); assert_eq!(redirect.kind, KIND_WIKI_REDIRECT); @@ -41,12 +31,74 @@ fn knowledge_prelude_builds_mvp_wire_parts_without_codec_imports() { } #[test] +fn fluent_builders_auto_fill_schema_and_prepare_drafts() { + let article = article_builder().build().expect("article"); + let redirect = redirect_builder().build().expect("redirect"); + let merge_request = merge_request_builder().build().expect("merge request"); + let source = source_builder().build().expect("source"); + let claim = claim_builder().build().expect("claim"); + let relation = relation_builder().build().expect("relation"); + let review = review_builder().build().expect("review"); + let field_report = field_report_builder().build().expect("field report"); + + assert_eq!(article.d_tag, "soil-health"); + assert_eq!(redirect.target, address_ref()); + assert_eq!( + merge_request.explanation.as_deref(), + Some("Merge synthetic soil article updates") + ); + assert_eq!(source.schema, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA); + assert_eq!(claim.schema, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA); + assert_eq!(relation.schema, RADROOTS_KNOWLEDGE_RELATION_SCHEMA); + assert_eq!(review.schema, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA); + assert_eq!(field_report.schema, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA); + assert_eq!(claim.schema_version, RADROOTS_KNOWLEDGE_SCHEMA_VERSION); + + let article_draft = article_builder() + .build_draft(public_key_hex(), CREATED_AT) + .expect("article draft"); + let claim_draft = claim_builder() + .build_draft(public_key_hex(), CREATED_AT) + .expect("claim draft"); + assert_eq!(article_draft.contract_id, WIKI_ARTICLE_CONTRACT_ID); + assert_eq!(claim_draft.contract_id, KNOWLEDGE_CLAIM_CONTRACT_ID); +} + +#[test] +fn fluent_builders_reject_missing_and_invalid_required_fields() { + let missing_text = RadrootsKnowledgeClaimBuilder::new() + .claim_type("practice_effect") + .build() + .expect_err("missing text"); + assert_eq!( + missing_text, + RadrootsKnowledgeBuilderError::MissingField("text") + ); + + let invalid_d_tag = RadrootsWikiArticleBuilder::new("Soil Health") + .title("Soil health") + .content_djot("# Soil health") + .build() + .expect_err("invalid d tag"); + assert_eq!( + invalid_d_tag, + RadrootsKnowledgeBuilderError::InvalidField("d_tag") + ); +} + +#[test] fn knowledge_draft_builder_freezes_mvp_drafts_without_runtime() { let draft_builder = KnowledgeDraftBuilder::new(public_key_hex(), CREATED_AT); let article = draft_builder .wiki_article(&wiki_article()) .expect("article draft"); + let redirect = draft_builder + .wiki_redirect(&wiki_redirect()) + .expect("redirect draft"); + let merge_request = draft_builder + .wiki_merge_request(&wiki_merge_request()) + .expect("merge request draft"); let source = draft_builder .knowledge_source(&knowledge_source()) .expect("source draft"); @@ -64,6 +116,8 @@ fn knowledge_draft_builder_freezes_mvp_drafts_without_runtime() { .expect("field report draft"); assert_eq!(article.contract_id, WIKI_ARTICLE_CONTRACT_ID); + assert_eq!(redirect.contract_id, WIKI_REDIRECT_CONTRACT_ID); + assert_eq!(merge_request.contract_id, WIKI_MERGE_REQUEST_CONTRACT_ID); assert_eq!(source.contract_id, KNOWLEDGE_SOURCE_CONTRACT_ID); assert_eq!(claim.contract_id, KNOWLEDGE_CLAIM_CONTRACT_ID); assert_eq!(relation.contract_id, KNOWLEDGE_RELATION_CONTRACT_ID); @@ -90,12 +144,17 @@ fn knowledge_codec_exposes_manifest_and_verified_decode() { .iter() .any(|contract| contract.contract_id == KNOWLEDGE_CLAIM_CONTRACT_ID) ); + let claim_contract = manifest + .contracts + .iter() + .find(|contract| contract.contract_id == KNOWLEDGE_CLAIM_CONTRACT_ID) + .expect("claim manifest contract"); + assert!(claim_contract.sdk_builder_support); + assert!(claim_contract.sdk_draft_support); + assert!(claim_contract.wasm_tag_builder_support); + assert!(claim_contract.wasm_verified_decode_support); - let signed = sign_parts( - KnowledgeEventBuilder::new() - .knowledge_claim(&knowledge_claim()) - .expect("claim parts"), - ); + let signed = sign_parts(claim_builder().build_event().expect("claim parts")); let decoded = codec .verify_and_decode_radroots_event(signed) .expect("decoded claim"); @@ -172,16 +231,6 @@ fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef { } } -fn article_ref() -> RadrootsNostrEventRef { - RadrootsNostrEventRef { - id: hex_64('b'), - author: hex_64('a'), - kind: KIND_WIKI_ARTICLE, - d_tag: Some("soil-health".to_owned()), - relays: Some(vec![RELAY.to_owned()]), - } -} - fn address_ref() -> RadrootsAddressableRef { RadrootsAddressableRef { kind: KIND_WIKI_ARTICLE, @@ -207,7 +256,14 @@ fn wiki_article() -> RadrootsWikiArticle { fn wiki_redirect() -> RadrootsWikiRedirect { RadrootsWikiRedirect { d_tag: "soil".to_owned(), - target: article_ref(), + target: address_ref(), + } +} + +fn article_version_ref() -> RadrootsWikiArticleVersionRef { + RadrootsWikiArticleVersionRef { + event_id: hex_64('b'), + address_ref: address_ref(), } } @@ -221,6 +277,123 @@ fn wiki_merge_request() -> RadrootsWikiMergeRequest { } } +fn article_builder() -> RadrootsWikiArticleBuilder { + RadrootsWikiArticleBuilder::new("soil-health") + .title("Soil health") + .content_djot("# Soil health") + .summary("Living soil basics") + .topic("soil") + .topic("local-food") + .reference(event_ref('1', KIND_KNOWLEDGE_SOURCE)) + .forked_from(article_version_ref()) +} + +fn redirect_builder() -> RadrootsWikiRedirectBuilder { + RadrootsWikiRedirectBuilder::new("soil").target(address_ref()) +} + +fn merge_request_builder() -> RadrootsWikiMergeRequestBuilder { + RadrootsWikiMergeRequestBuilder::new() + .target_article(address_ref()) + .destination_pubkey(hex_64('a')) + .base_version_event_id(hex_64('e')) + .source_version_event_id(hex_64('f')) + .explanation("Merge synthetic soil article updates") +} + +fn source_builder() -> RadrootsKnowledgeSourceBuilder { + RadrootsKnowledgeSourceBuilder::new("soil-source") + .title("Soil Source") + .source_type("book") + .author("A. Example") + .publisher("Radroots Synthetic Press") + .publication_year(2026) + .canonical_url("https://source.example.test/soil-source") + .artifact_ref(event_ref('3', KIND_FILE_METADATA)) + .topic("soil") + .summary("Synthetic source for SDK coverage") +} + +fn claim_builder() -> RadrootsKnowledgeClaimBuilder { + RadrootsKnowledgeClaimBuilder::new() + .claim_type("practice_effect") + .text("Cover crops improve soil structure.") + .citation_span(RadrootsKnowledgeCitationSpan { + source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_owned()], + quote_hash: Some(hex_64('5')), + chunk_id: Some("chunk-1".to_owned()), + }) + .topic("cover-crops") + .applies_to("local-food") + .author_asserted_confidence("medium") +} + +fn relation_builder() -> RadrootsKnowledgeRelationBuilder { + RadrootsKnowledgeRelationBuilder::new() + .subject(knowledge_node_ref("cover crops")) + .predicate("supports") + .object(knowledge_node_ref("soil structure")) + .support_ref(event_ref('7', KIND_KNOWLEDGE_CLAIM)) + .author_asserted_confidence("medium") +} + +fn review_builder() -> RadrootsKnowledgeReviewBuilder { + RadrootsKnowledgeReviewBuilder::new() + .target(RadrootsKnowledgeReviewTarget { + event_id: hex_64('8'), + author_pubkey: hex_64('a'), + kind: KIND_KNOWLEDGE_CLAIM, + address: None, + relays: vec![RELAY.to_owned()], + review_scope: RadrootsKnowledgeReviewScope::SpecificVersion, + }) + .reviewer_role("peer") + .verdict("needs_more_evidence") + .score(RadrootsKnowledgeReviewScore { + dimension: "evidence".to_owned(), + value: "partial".to_owned(), + note: None, + }) + .notes("Synthetic review") + .evidence_ref(event_ref('9', KIND_KNOWLEDGE_SOURCE)) +} + +fn field_report_builder() -> RadrootsKnowledgeFieldReportBuilder { + RadrootsKnowledgeFieldReportBuilder::new() + .report_type("observation") + .title("Field observation") + .summary("Observed cover crop residue.") + .context(RadrootsKnowledgeFieldContext { + location_precision: RadrootsKnowledgeLocationPrecision::CoarseGeohash, + public_location: Some(RadrootsKnowledgeLocation { + label: Some("watershed".to_owned()), + region: Some("synthetic-region".to_owned()), + locality: None, + geohash: Some("c23".to_owned()), + }), + private_location_ref: None, + topics: vec!["field".to_owned()], + context_tags: vec!["observation".to_owned()], + }) + .observation(RadrootsKnowledgeObservation { + observation_type: "residue".to_owned(), + text: "Residue was visible across beds.".to_owned(), + observed_at: Some("2026-07-05".to_owned()), + values: vec![RadrootsKnowledgeObservationValue { + key: "coverage".to_owned(), + value: "medium".to_owned(), + unit: None, + }], + }) + .artifact_ref(event_ref('c', KIND_FILE_METADATA)) + .related_ref(event_ref('d', KIND_KNOWLEDGE_CLAIM)) + .limitation("single observer") +} + fn knowledge_source() -> RadrootsKnowledgeSource { RadrootsKnowledgeSource { schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_owned(), diff --git a/crates/trade_bindings/src/dto.rs b/crates/trade_bindings/src/dto.rs @@ -4,7 +4,7 @@ use dto_bindgen_core::{ VariantShape, WireFieldNames, }; use radroots_trade::{ - listing::model::RadrootsTradeListingTotal, order::RadrootsTradeOrderWorkflowProjection, + listing::model::RadrootsTradeListingTotal, order::RadrootsOrderWorkflowProjection, workflow::RadrootsTradeWorkflowState, }; @@ -319,7 +319,7 @@ pub struct RadrootsTradeOrderBackofficeQuery { #[derive(dto_bindgen::Dto)] pub struct RadrootsTradeOrderBackofficeView { - pub order: RadrootsTradeOrderWorkflowProjection, + pub order: RadrootsOrderWorkflowProjection, pub marketplace: RadrootsTradeMarketplaceOrderSummary, pub overlay: Option<RadrootsTradeOrderBackofficeOverlay>, pub requires_review: bool, diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs @@ -69,7 +69,7 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[ local_shape("RadrootsTradeOrderQuery"), local_shape("RadrootsTradeOrderSort"), local_shape("RadrootsTradeOrderSortField"), - source_root("RadrootsTradeOrderWorkflowProjection"), + source_root("RadrootsOrderWorkflowProjection"), local_shape("RadrootsTradeReviewPriority"), local_shape("RadrootsTradeReviewQueueEntry"), local_shape("RadrootsTradeReviewStatus"), @@ -204,7 +204,7 @@ mod tests { "RadrootsTradeOrderQuery", "RadrootsTradeOrderSort", "RadrootsTradeOrderSortField", - "RadrootsTradeOrderWorkflowProjection", + "RadrootsOrderWorkflowProjection", "RadrootsTradeReviewPriority", "RadrootsTradeReviewQueueEntry", "RadrootsTradeReviewStatus", @@ -250,7 +250,7 @@ mod tests { "RadrootsTradeListing", "RadrootsTradeListingSubtotal", "RadrootsTradeListingTotal", - "RadrootsTradeOrderWorkflowProjection", + "RadrootsOrderWorkflowProjection", "RadrootsTradeWorkflowState" ] ); diff --git a/packages/events-bindings/src/generated/types.ts b/packages/events-bindings/src/generated/types.ts @@ -222,8 +222,10 @@ export type RadrootsTradeListingValidateResult = { valid: boolean, errors: Array export type RadrootsTradeValidationListingError = { kind: "invalid_kind", amount: { kind: number, }, } | { kind: "missing_listing_id", } | { kind: "listing_event_not_found", amount: { listing_addr: string, }, } | { kind: "listing_event_fetch_failed", amount: { listing_addr: string, }, } | { kind: "parse_error", amount: { error: RadrootsListingParseError, }, } | { kind: "invalid_seller", } | { kind: "missing_farm_profile", } | { kind: "missing_farm_record", } | { kind: "missing_title", } | { kind: "missing_description", } | { kind: "missing_product_type", } | { kind: "missing_bins", } | { kind: "missing_primary_bin", } | { kind: "invalid_bin", } | { kind: "missing_price", } | { kind: "invalid_price", } | { kind: "missing_inventory", } | { kind: "invalid_inventory", } | { kind: "missing_availability", } | { kind: "missing_location", } | { kind: "missing_location_locality", } | { kind: "missing_location_geohash", } | { kind: "invalid_location_geohash", } | { kind: "missing_delivery_method", }; -export type RadrootsWikiArticle = { d_tag: string, title: string, content_djot: string, summary?: string | null, topics: Array<string>, references: Array<RadrootsNostrEventRef>, forked_from: Array<RadrootsNostrEventRef>, deferred_to?: RadrootsNostrEventRef | null, }; +export type RadrootsWikiArticle = { d_tag: string, title: string, content_djot: string, summary?: string | null, topics: Array<string>, references: Array<RadrootsNostrEventRef>, forked_from: Array<RadrootsWikiArticleVersionRef>, deferred_to?: RadrootsWikiArticleVersionRef | null, }; + +export type RadrootsWikiArticleVersionRef = { event_id: string, address_ref: RadrootsAddressableRef, }; export type RadrootsWikiMergeRequest = { target_article: RadrootsAddressableRef, destination_pubkey: string, base_version_event_id?: string | null, source_version_event_id: string, explanation?: string | null, }; -export type RadrootsWikiRedirect = { d_tag: string, target: RadrootsNostrEventRef, }; +export type RadrootsWikiRedirect = { d_tag: string, target: RadrootsAddressableRef, }; diff --git a/packages/trade-bindings/src/generated/types.ts b/packages/trade-bindings/src/generated/types.ts @@ -5,6 +5,8 @@ import type { RadrootsFarmRef, RadrootsListing, RadrootsListingAvailability, Rad export type RadrootsOrderIssue = "missing_request" | { multiple_requests: { event_ids: Array<string>, }, } | { request_payload_invalid: { event_id: string, }, } | { request_order_id_mismatch: { event_id: string, }, } | { request_author_mismatch: { event_id: string, }, } | { request_listing_address_invalid: { event_id: string, }, } | { request_seller_listing_mismatch: { event_id: string, }, } | { decision_payload_invalid: { event_id: string, }, } | { decision_order_id_mismatch: { event_id: string, }, } | { decision_author_mismatch: { event_id: string, }, } | { decision_counterparty_mismatch: { event_id: string, }, } | { decision_buyer_mismatch: { event_id: string, }, } | { decision_seller_mismatch: { event_id: string, }, } | { decision_listing_address_invalid: { event_id: string, }, } | { decision_listing_mismatch: { event_id: string, }, } | { decision_root_mismatch: { event_id: string, }, } | { decision_previous_mismatch: { event_id: string, }, } | { decision_missing_inventory_commitments: { event_id: string, }, } | { decision_inventory_commitment_mismatch: { event_id: string, }, } | { decision_missing_reason: { event_id: string, }, } | { conflicting_decisions: { event_ids: Array<string>, }, } | { revision_proposal_payload_invalid: { event_id: string, }, } | { revision_proposal_order_id_mismatch: { event_id: string, }, } | { revision_proposal_author_mismatch: { event_id: string, }, } | { revision_proposal_counterparty_mismatch: { event_id: string, }, } | { revision_proposal_buyer_mismatch: { event_id: string, }, } | { revision_proposal_seller_mismatch: { event_id: string, }, } | { revision_proposal_listing_address_invalid: { event_id: string, }, } | { revision_proposal_listing_mismatch: { event_id: string, }, } | { revision_proposal_root_mismatch: { event_id: string, }, } | { revision_proposal_previous_mismatch: { event_id: string, }, } | { revision_decision_without_proposal: { event_id: string, }, } | { revision_decision_payload_invalid: { event_id: string, }, } | { revision_decision_order_id_mismatch: { event_id: string, }, } | { revision_decision_author_mismatch: { event_id: string, }, } | { revision_decision_counterparty_mismatch: { event_id: string, }, } | { revision_decision_buyer_mismatch: { event_id: string, }, } | { revision_decision_seller_mismatch: { event_id: string, }, } | { revision_decision_listing_address_invalid: { event_id: string, }, } | { revision_decision_listing_mismatch: { event_id: string, }, } | { revision_decision_root_mismatch: { event_id: string, }, } | { revision_decision_previous_mismatch: { event_id: string, }, } | { revision_decision_revision_id_mismatch: { event_id: string, }, } | { cancellation_without_cancellable_order: { event_id: string, }, } | { cancellation_payload_invalid: { event_id: string, }, } | { cancellation_order_id_mismatch: { event_id: string, }, } | { cancellation_author_mismatch: { event_id: string, }, } | { cancellation_counterparty_mismatch: { event_id: string, }, } | { cancellation_buyer_mismatch: { event_id: string, }, } | { cancellation_seller_mismatch: { event_id: string, }, } | { cancellation_listing_address_invalid: { event_id: string, }, } | { cancellation_listing_mismatch: { event_id: string, }, } | { cancellation_root_mismatch: { event_id: string, }, } | { cancellation_previous_mismatch: { event_id: string, }, } | { forked_lifecycle: { event_ids: Array<string>, }, } | { validation_receipt_without_pending_agreement: { event_id: string, }, } | { validation_receipt_order_id_mismatch: { event_id: string, }, } | { validation_receipt_type_mismatch: { event_id: string, }, } | { validation_receipt_root_mismatch: { event_id: string, }, } | { validation_receipt_target_mismatch: { event_id: string, }, } | { validation_receipt_listing_mismatch: { event_id: string, }, } | { conflicting_validation_receipts: { event_ids: Array<string>, }, } | { deterministic_validation_failure: { event_id: string, reason: string, }, } | { stale_listing_event: { expected_event_id: string, current_event_id: string, }, }; +export type RadrootsOrderWorkflowProjection = { order_id: string, status: RadrootsTradeWorkflowState, request_event_id?: string | null, decision_event_id?: string | null, cancellation_event_id?: string | null, validation_receipt_event_id?: string | null, lifecycle_terminal: boolean, economics?: RadrootsOrderEconomics | null, agreement_event_id?: string | null, pending_revision_event_id?: string | null, pending_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, committed_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, listing_addr?: string | null, buyer_pubkey?: string | null, seller_pubkey?: string | null, last_event_id?: string | null, issues: Array<RadrootsOrderIssue>, }; + export type RadrootsTradeFacetCount = { key: string, count: number, }; export type RadrootsTradeListing = { listing_id: string, listing_addr: string, seller_pubkey: string, title: string, description: string, product_type: string, primary_bin_id: string, bin_quantity: RadrootsCoreQuantity, unit: RadrootsCoreUnit, unit_price: RadrootsCoreMoney, inventory_available: RadrootsCoreDecimal, availability: RadrootsListingAvailability, location: RadrootsListingPublicLocation, delivery_method: RadrootsListingDeliveryMethod, listing: RadrootsListing, }; @@ -47,7 +49,7 @@ export type RadrootsTradeOrderBackofficeOverlay = { order_id: string, review_que export type RadrootsTradeOrderBackofficeQuery = { order: RadrootsTradeOrderQuery, requires_review?: boolean | null, has_open_moderation_flags?: boolean | null, }; -export type RadrootsTradeOrderBackofficeView = { order: RadrootsTradeOrderWorkflowProjection, marketplace: RadrootsTradeMarketplaceOrderSummary, overlay?: RadrootsTradeOrderBackofficeOverlay | null, requires_review: boolean, open_moderation_flag_count: number, }; +export type RadrootsTradeOrderBackofficeView = { order: RadrootsOrderWorkflowProjection, marketplace: RadrootsTradeMarketplaceOrderSummary, overlay?: RadrootsTradeOrderBackofficeOverlay | null, requires_review: boolean, open_moderation_flag_count: number, }; export type RadrootsTradeOrderFacets = { buyer_pubkeys: Array<RadrootsTradeFacetCount>, seller_pubkeys: Array<RadrootsTradeFacetCount>, listing_addrs: Array<RadrootsTradeFacetCount>, statuses: Array<RadrootsTradeFacetCount>, }; @@ -57,8 +59,6 @@ export type RadrootsTradeOrderSort = { field: RadrootsTradeOrderSortField, direc export type RadrootsTradeOrderSortField = "order_id" | "listing_addr" | "buyer_pubkey" | "seller_pubkey" | "status" | "last_message_type" | "total_bin_count"; -export type RadrootsTradeOrderWorkflowProjection = { order_id: string, status: RadrootsTradeWorkflowState, request_event_id?: string | null, decision_event_id?: string | null, cancellation_event_id?: string | null, validation_receipt_event_id?: string | null, lifecycle_terminal: boolean, economics?: RadrootsOrderEconomics | null, agreement_event_id?: string | null, pending_revision_event_id?: string | null, pending_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, committed_inventory_reservations: Array<RadrootsOrderInventoryCommitment>, listing_addr?: string | null, buyer_pubkey?: string | null, seller_pubkey?: string | null, last_event_id?: string | null, issues: Array<RadrootsOrderIssue>, }; - export type RadrootsTradeReviewPriority = "low" | "normal" | "high" | "critical"; export type RadrootsTradeReviewQueueEntry = { queue: string, priority: RadrootsTradeReviewPriority, status: RadrootsTradeReviewStatus, assigned_operator?: string | null, reason?: string | null, }; diff --git a/tools/xtask/src/dto_roots.rs b/tools/xtask/src/dto_roots.rs @@ -697,6 +697,7 @@ mod tests { "RadrootsTradeListingValidateResult", "RadrootsTradeValidationListingError", "RadrootsWikiArticle", + "RadrootsWikiArticleVersionRef", "RadrootsWikiMergeRequest", "RadrootsWikiRedirect", ]; @@ -710,6 +711,7 @@ mod tests { ]; const TRADE_TYPE_INVENTORY: &[&str] = &[ "RadrootsOrderIssue", + "RadrootsOrderWorkflowProjection", "RadrootsTradeFacetCount", "RadrootsTradeListing", "RadrootsTradeListingBackofficeOverlay", @@ -736,7 +738,6 @@ mod tests { "RadrootsTradeOrderQuery", "RadrootsTradeOrderSort", "RadrootsTradeOrderSortField", - "RadrootsTradeOrderWorkflowProjection", "RadrootsTradeReviewPriority", "RadrootsTradeReviewQueueEntry", "RadrootsTradeReviewStatus", @@ -1041,10 +1042,8 @@ mod tests { let order_query = type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsTradeOrderQuery"); assert!(order_query.contains("status?: RadrootsTradeWorkflowState | null")); - let workflow_projection = type_declaration( - TRADE_BINDINGS_TYPES_TS, - "RadrootsTradeOrderWorkflowProjection", - ); + let workflow_projection = + type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsOrderWorkflowProjection"); assert!(workflow_projection.contains("status: RadrootsTradeWorkflowState")); assert!(workflow_projection.contains("request_event_id?: string | null")); assert!(workflow_projection.contains("last_event_id?: string | null")); diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs @@ -61,27 +61,9 @@ const SECRET_KEY_HEX: &str = "01010101010101010101010101010101010101010101010101 const CREATED_AT: u32 = 1_800_000_000; fn main() -> Result<(), Box<dyn std::error::Error>> { - let claim = RadrootsKnowledgeClaim { - schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_owned(), - schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, - claim_type: "practice_effect".to_owned(), - text: "Cover crops improve soil structure.".to_owned(), - citation_spans: vec![RadrootsKnowledgeCitationSpan { - source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), - artifact_ref: None, - page_start: Some(12), - page_end: Some(13), - section_path: vec!["chapter-1".to_owned()], - quote_hash: Some(hex_64('5')), - chunk_id: Some("chunk-1".to_owned()), - }], - topics: vec!["cover-crops".to_owned()], - applies_to: vec!["local-food".to_owned()], - author_asserted_confidence: Some("medium".to_owned()), - supersedes: Vec::new(), - }; - let parts = KnowledgeEventBuilder::new().knowledge_claim(&claim)?; - let draft = KnowledgeDraftBuilder::new(public_key_hex(), CREATED_AT).knowledge_claim(&claim)?; + let claim = claim_builder().build()?; + let parts = claim_builder().build_event()?; + let draft = claim_builder().build_draft(public_key_hex(), CREATED_AT)?; let decoded = verify_and_decode_radroots_event(sign_parts(parts)?)?; let manifest = contract_manifest(); @@ -141,4 +123,22 @@ fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef { relays: Some(vec!["wss://relay.radroots.example".to_owned()]), } } + +fn claim_builder() -> RadrootsKnowledgeClaimBuilder { + RadrootsKnowledgeClaimBuilder::new() + .claim_type("practice_effect") + .text("Cover crops improve soil structure.") + .citation_span(RadrootsKnowledgeCitationSpan { + source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_owned()], + quote_hash: Some(hex_64('5')), + chunk_id: Some("chunk-1".to_owned()), + }) + .topic("cover-crops") + .applies_to("local-food") + .author_asserted_confidence("medium") +} "#;