commit 3bbc182d4c51815e8f9bdedf7f32d5c2950f9546
parent 31e0f07fa55a06289eb4b7f7e0d7b5262f1cbafb
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 20:39:57 +0000
knowledge-sdk: add fluent knowledge builders
Diffstat:
8 files changed, 1061 insertions(+), 110 deletions(-)
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/tools/sdk_xtask_import/src/dto_roots.rs b/tools/sdk_xtask_import/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/sdk_xtask_import/src/smoke.rs b/tools/sdk_xtask_import/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")
+}
"#;