lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 741c42a66bf32960176408e1a58af920add9b9c8
parent df5ed1acf9e5a2f854d57b420eded2bd2a450dcc
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Jul 2026 01:37:23 +0000

knowledge-sdk: enforce claim citation rules

Diffstat:
Mcrates/sdk/src/knowledge.rs | 44++++++++++++++++++++++++++++++--------------
Mcrates/sdk/tests/knowledge_public_api.rs | 35+++++++++++++++++++++++++++++++++++
2 files changed, 65 insertions(+), 14 deletions(-)

diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs @@ -31,9 +31,10 @@ pub use radroots_events::{ RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, RadrootsKnowledgeRelation, RadrootsKnowledgeReview, RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, - RadrootsKnowledgeSource, RadrootsRightsAssertion, RadrootsWikiArticle, - RadrootsWikiArticleVersionRef, RadrootsWikiDTagError, RadrootsWikiMergeRequest, - RadrootsWikiRedirect, normalize_wiki_d_tag, validate_wiki_d_tag, + RadrootsKnowledgeSource, RadrootsKnowledgeValidationError, RadrootsRightsAssertion, + RadrootsWikiArticle, RadrootsWikiArticleVersionRef, RadrootsWikiDTagError, + RadrootsWikiMergeRequest, RadrootsWikiRedirect, normalize_wiki_d_tag, + validate_knowledge_claim, validate_wiki_d_tag, }, }; pub use radroots_events_codec::{ @@ -586,7 +587,7 @@ impl RadrootsKnowledgeClaimBuilder { } pub fn build(self) -> Result<RadrootsKnowledgeClaim, RadrootsKnowledgeBuilderError> { - Ok(RadrootsKnowledgeClaim { + let claim = RadrootsKnowledgeClaim { schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, claim_type: builder_required_string(self.claim_type, "claim_type")?, @@ -596,7 +597,9 @@ impl RadrootsKnowledgeClaimBuilder { applies_to: self.applies_to, author_asserted_confidence: self.author_asserted_confidence, supersedes: self.supersedes, - }) + }; + validate_knowledge_claim(&claim).map_err(builder_validation_error)?; + Ok(claim) } pub fn build_event(self) -> Result<WireEventParts, RadrootsSdkKnowledgeError> { @@ -1291,6 +1294,19 @@ fn validate_builder_wiki_d_tag(value: &str) -> Result<(), RadrootsKnowledgeBuild .map_err(|_| RadrootsKnowledgeBuilderError::InvalidField("d_tag")) } +fn builder_validation_error( + error: RadrootsKnowledgeValidationError, +) -> RadrootsKnowledgeBuilderError { + match error { + RadrootsKnowledgeValidationError::EmptyField(field) => { + RadrootsKnowledgeBuilderError::MissingField(field) + } + RadrootsKnowledgeValidationError::InvalidField(field) => { + RadrootsKnowledgeBuilderError::InvalidField(field) + } + } +} + pub mod prelude { pub use super::{ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, @@ -1317,20 +1333,20 @@ pub mod prelude { 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, + RadrootsKnowledgeValidationError, 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, 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, prepare_knowledge_claim_draft, prepare_knowledge_field_report_draft, prepare_knowledge_relation_draft, prepare_knowledge_review_draft, prepare_knowledge_source_draft, prepare_wiki_article_draft, - prepare_wiki_merge_request_draft, prepare_wiki_redirect_draft, validate_wiki_d_tag, - verify_and_decode_radroots_event, + prepare_wiki_merge_request_draft, prepare_wiki_redirect_draft, validate_knowledge_claim, + validate_wiki_d_tag, verify_and_decode_radroots_event, }; } diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs @@ -87,6 +87,41 @@ fn fluent_builders_reject_missing_and_invalid_required_fields() { } #[test] +fn knowledge_claim_builder_enforces_citation_rules() { + let missing_citations = RadrootsKnowledgeClaimBuilder::new() + .claim_type("practice_effect") + .text("Cover crops improve soil structure.") + .build() + .expect_err("missing citations"); + assert_eq!( + missing_citations, + RadrootsKnowledgeBuilderError::MissingField("citation_spans") + ); + + assert!(claim_builder().build().is_ok()); + + for claim_type in ["hypothesis", "observation", "question"] { + let claim = RadrootsKnowledgeClaimBuilder::new() + .claim_type(claim_type) + .text("Synthetic uncited claim.") + .build() + .expect("uncited claim"); + assert_eq!(claim.claim_type, claim_type); + assert!(claim.citation_spans.is_empty()); + } + + let capitalized = RadrootsKnowledgeClaimBuilder::new() + .claim_type("Hypothesis") + .text("Synthetic uncited claim.") + .build() + .expect_err("capitalized claim type requires citations"); + assert_eq!( + capitalized, + RadrootsKnowledgeBuilderError::MissingField("citation_spans") + ); +} + +#[test] fn knowledge_draft_builder_freezes_mvp_drafts_without_runtime() { let draft_builder = KnowledgeDraftBuilder::new(public_key_hex(), CREATED_AT);