sdk

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

commit 5ba3d4d817e8fd17e20b791dcaae9a6869d483e9
parent e9d7d77cc105113afc209cc80949bcfb93ef4aa9
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Jul 2026 04:38:37 +0000

sdk: validate knowledge builders with core models

- route SDK knowledge builder outputs through matching core validators
- map core empty and invalid fields onto existing builder errors
- cover invalid nested redirect, merge, source, relation, review, and field report models
- keep knowledge public API tests, SDK feature tests, and local smoke green

Diffstat:
Mcrates/sdk/src/knowledge.rs | 60++++++++++++++++++++++++++++++++----------------------------
Mcrates/sdk/tests/knowledge_public_api.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 187 insertions(+), 28 deletions(-)

diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs @@ -52,6 +52,10 @@ pub use radroots_events_codec::{ wire::WireEventParts, }; +use radroots_events::knowledge::{ + validate_knowledge_field_report, validate_knowledge_relation, validate_knowledge_review, + validate_knowledge_source, validate_wiki_merge_request, validate_wiki_redirect, +}; use radroots_events_codec::{ contract_manifest_json as codec_contract_manifest_json, contract_manifest_sha256 as codec_contract_manifest_sha256, @@ -256,7 +260,6 @@ impl RadrootsWikiArticleBuilder { } pub fn build(self) -> Result<RadrootsWikiArticle, RadrootsKnowledgeBuilderError> { - validate_builder_wiki_d_tag(&self.d_tag)?; let article = RadrootsWikiArticle { d_tag: self.d_tag, title: self.title, @@ -267,8 +270,7 @@ impl RadrootsWikiArticleBuilder { forked_from: self.forked_from, deferred_to: self.deferred_to, }; - validate_wiki_article(&article).map_err(builder_validation_error)?; - Ok(article) + builder_validated(article, validate_wiki_article) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -308,11 +310,11 @@ impl RadrootsWikiRedirectBuilder { } pub fn build(self) -> Result<RadrootsWikiRedirect, RadrootsKnowledgeBuilderError> { - validate_builder_wiki_d_tag(&self.d_tag)?; - Ok(RadrootsWikiRedirect { + let redirect = RadrootsWikiRedirect { d_tag: self.d_tag, target: builder_required(self.target, "target")?, - }) + }; + builder_validated(redirect, validate_wiki_redirect) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -378,7 +380,7 @@ impl RadrootsWikiMergeRequestBuilder { } pub fn build(self) -> Result<RadrootsWikiMergeRequest, RadrootsKnowledgeBuilderError> { - Ok(RadrootsWikiMergeRequest { + let request = RadrootsWikiMergeRequest { target_article: builder_required(self.target_article, "target_article")?, destination_pubkey: builder_required_string( self.destination_pubkey, @@ -390,7 +392,8 @@ impl RadrootsWikiMergeRequestBuilder { "source_version_event_id", )?, explanation: self.explanation, - }) + }; + builder_validated(request, validate_wiki_merge_request) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -490,8 +493,7 @@ impl RadrootsKnowledgeSourceBuilder { } pub fn build(self) -> Result<RadrootsKnowledgeSource, RadrootsKnowledgeBuilderError> { - validate_builder_wiki_d_tag(&self.d_tag)?; - Ok(RadrootsKnowledgeSource { + let source = RadrootsKnowledgeSource { schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, d_tag: self.d_tag, @@ -506,7 +508,8 @@ impl RadrootsKnowledgeSourceBuilder { author_asserted_rights: self.author_asserted_rights, topics: self.topics, summary: self.summary, - }) + }; + builder_validated(source, validate_knowledge_source) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -600,8 +603,7 @@ impl RadrootsKnowledgeClaimBuilder { author_asserted_confidence: self.author_asserted_confidence, supersedes: self.supersedes, }; - validate_knowledge_claim(&claim).map_err(builder_validation_error)?; - Ok(claim) + builder_validated(claim, validate_knowledge_claim) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -677,7 +679,7 @@ impl RadrootsKnowledgeRelationBuilder { } pub fn build(self) -> Result<RadrootsKnowledgeRelation, RadrootsKnowledgeBuilderError> { - Ok(RadrootsKnowledgeRelation { + let relation = RadrootsKnowledgeRelation { schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, subject: builder_required(self.subject, "subject")?, @@ -686,7 +688,8 @@ impl RadrootsKnowledgeRelationBuilder { support_refs: self.support_refs, author_asserted_confidence: self.author_asserted_confidence, supersedes: self.supersedes, - }) + }; + builder_validated(relation, validate_knowledge_relation) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -759,7 +762,7 @@ impl RadrootsKnowledgeReviewBuilder { } pub fn build(self) -> Result<RadrootsKnowledgeReview, RadrootsKnowledgeBuilderError> { - Ok(RadrootsKnowledgeReview { + let review = RadrootsKnowledgeReview { schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, target: builder_required(self.target, "target")?, @@ -768,7 +771,8 @@ impl RadrootsKnowledgeReviewBuilder { scores: self.scores, notes: self.notes, evidence_refs: self.evidence_refs, - }) + }; + builder_validated(review, validate_knowledge_review) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -855,10 +859,7 @@ impl RadrootsKnowledgeFieldReportBuilder { } pub fn build(self) -> Result<RadrootsKnowledgeFieldReport, RadrootsKnowledgeBuilderError> { - if self.observations.is_empty() { - return Err(RadrootsKnowledgeBuilderError::MissingField("observations")); - } - Ok(RadrootsKnowledgeFieldReport { + let report = 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")?, @@ -869,7 +870,8 @@ impl RadrootsKnowledgeFieldReportBuilder { artifact_refs: self.artifact_refs, related_refs: self.related_refs, limitations: self.limitations, - }) + }; + builder_validated(report, validate_knowledge_field_report) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -1290,12 +1292,6 @@ fn builder_non_empty_string( } } -fn validate_builder_wiki_d_tag(value: &str) -> Result<(), RadrootsKnowledgeBuilderError> { - validate_wiki_d_tag(value) - .map(|_| ()) - .map_err(|_| RadrootsKnowledgeBuilderError::InvalidField("d_tag")) -} - fn builder_validation_error( error: RadrootsKnowledgeValidationError, ) -> RadrootsKnowledgeBuilderError { @@ -1309,6 +1305,14 @@ fn builder_validation_error( } } +fn builder_validated<T>( + value: T, + validate: fn(&T) -> Result<(), RadrootsKnowledgeValidationError>, +) -> Result<T, RadrootsKnowledgeBuilderError> { + validate(&value).map_err(builder_validation_error)?; + Ok(value) +} + pub mod prelude { pub use super::{ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs @@ -87,6 +87,155 @@ fn fluent_builders_reject_missing_and_invalid_required_fields() { } #[test] +fn fluent_builders_reject_core_invalid_nested_models() { + let mut non_wiki_target = address_ref(); + non_wiki_target.kind = KIND_KNOWLEDGE_SOURCE; + let redirect_error = RadrootsWikiRedirectBuilder::new("soil") + .target(non_wiki_target) + .build() + .expect_err("non wiki redirect target"); + assert_eq!( + redirect_error, + RadrootsKnowledgeBuilderError::InvalidField("wiki_redirect.target") + ); + + let destination_error = merge_request_builder() + .destination_pubkey("bad") + .build() + .expect_err("invalid destination pubkey"); + assert_eq!( + destination_error, + RadrootsKnowledgeBuilderError::InvalidField("destination_pubkey") + ); + + let source_version_error = merge_request_builder() + .source_version_event_id("bad") + .build() + .expect_err("invalid source version id"); + assert_eq!( + source_version_error, + RadrootsKnowledgeBuilderError::InvalidField("source_version_event_id") + ); + + let blank_author_error = source_builder() + .author(" ") + .build() + .expect_err("blank author"); + assert_eq!( + blank_author_error, + RadrootsKnowledgeBuilderError::MissingField("authors") + ); + + let malformed_artifact_error = source_builder() + .artifact_ref(malformed_event_ref(KIND_FILE_METADATA)) + .build() + .expect_err("malformed artifact ref"); + assert_eq!( + malformed_artifact_error, + RadrootsKnowledgeBuilderError::InvalidField("artifact_refs") + ); + + let mut invalid_subject = knowledge_node_ref("cover crops"); + invalid_subject.external_id = Some("duplicate".to_owned()); + let invalid_node_error = RadrootsKnowledgeRelationBuilder::new() + .subject(invalid_subject) + .predicate("supports") + .object(knowledge_node_ref("soil structure")) + .support_ref(event_ref('7', KIND_KNOWLEDGE_CLAIM)) + .build() + .expect_err("invalid relation node"); + assert_eq!( + invalid_node_error, + RadrootsKnowledgeBuilderError::InvalidField("subject") + ); + + let malformed_support_error = relation_builder() + .support_ref(malformed_event_ref(KIND_KNOWLEDGE_CLAIM)) + .build() + .expect_err("malformed support ref"); + assert_eq!( + malformed_support_error, + RadrootsKnowledgeBuilderError::InvalidField("support_refs") + ); + + let zero_kind_target = RadrootsKnowledgeReviewTarget { + event_id: hex_64('8'), + author_pubkey: hex_64('a'), + kind: 0, + address: None, + relays: vec![RELAY.to_owned()], + review_scope: RadrootsKnowledgeReviewScope::SpecificVersion, + }; + let zero_kind_error = review_builder() + .target(zero_kind_target) + .build() + .expect_err("zero target kind"); + assert_eq!( + zero_kind_error, + RadrootsKnowledgeBuilderError::InvalidField("review_target") + ); + + let malformed_pubkey_target = RadrootsKnowledgeReviewTarget { + event_id: hex_64('8'), + author_pubkey: "bad".to_owned(), + kind: KIND_KNOWLEDGE_CLAIM, + address: None, + relays: vec![RELAY.to_owned()], + review_scope: RadrootsKnowledgeReviewScope::SpecificVersion, + }; + let malformed_pubkey_error = review_builder() + .target(malformed_pubkey_target) + .build() + .expect_err("malformed target pubkey"); + assert_eq!( + malformed_pubkey_error, + RadrootsKnowledgeBuilderError::InvalidField("review_target") + ); + + let blank_score_dimension_error = review_builder() + .score(RadrootsKnowledgeReviewScore { + dimension: " ".to_owned(), + value: "partial".to_owned(), + note: None, + }) + .build() + .expect_err("blank score dimension"); + assert_eq!( + blank_score_dimension_error, + RadrootsKnowledgeBuilderError::MissingField("scores") + ); + + let exact_private_without_ref_error = field_report_builder() + .context(RadrootsKnowledgeFieldContext { + location_precision: RadrootsKnowledgeLocationPrecision::ExactPrivateReference, + public_location: None, + private_location_ref: None, + topics: vec!["field".to_owned()], + context_tags: vec!["observation".to_owned()], + }) + .build() + .expect_err("missing private location ref"); + assert_eq!( + exact_private_without_ref_error, + RadrootsKnowledgeBuilderError::MissingField("private_location_ref") + ); + + let blank_observation_error = field_report_builder() + .observation(RadrootsKnowledgeObservation { + observation_type: " ".to_owned(), + text: "Residue was visible across beds.".to_owned(), + observed_at: Some("2026-07-05".to_owned()), + values: Vec::new(), + }) + .build() + .expect_err("blank observation data"); + assert_eq!( + blank_observation_error, + RadrootsKnowledgeBuilderError::MissingField("observations") + ); +} + +#[test] fn wiki_article_builder_accepts_missing_title_but_rejects_blank_title() { let article = RadrootsWikiArticleBuilder::new("soil-health") .content_djot("# Soil health") @@ -296,6 +445,12 @@ fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef { } } +fn malformed_event_ref(kind: u32) -> RadrootsNostrEventRef { + let mut reference = event_ref('f', kind); + reference.id = "bad".to_owned(); + reference +} + fn address_ref() -> RadrootsAddressableRef { RadrootsAddressableRef { kind: KIND_WIKI_ARTICLE,