commit 4638baeffcb0be18c0ee32b799d88422f6edb7d8
parent ede93b05499af38054f92f827207a37a2d190cdc
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 06:29:56 +0000
sdk: add knowledge event surface
Diffstat:
9 files changed, 1220 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1806,11 +1806,13 @@ dependencies = [
name = "radroots_events_codec"
version = "0.1.0-alpha.2"
dependencies = [
+ "hex",
"nostr",
"radroots_core",
"radroots_events",
"serde",
"serde_json",
+ "sha2",
]
[[package]]
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -23,6 +23,13 @@ serde_json = [
"radroots_trade/serde_json",
]
nostr = ["radroots_events_codec/nostr"]
+knowledge = [
+ "serde",
+ "serde_json",
+ "radroots_events/knowledge-nip54",
+ "radroots_events_codec/contract-manifest",
+ "radroots_events_codec/knowledge-nip54",
+]
identity-models = [
"dep:radroots_identity",
"radroots_identity/profile",
@@ -148,6 +155,11 @@ path = "examples/sdk_v1_listing_prepare.rs"
required-features = ["runtime"]
[[example]]
+name = "sdk_v1_knowledge_prepare"
+path = "examples/sdk_v1_knowledge_prepare.rs"
+required-features = ["knowledge"]
+
+[[example]]
name = "sdk_v1_local_enqueue_and_mock_sync"
path = "examples/sdk_v1_local_enqueue_and_mock_sync.rs"
required-features = ["runtime", "signer-adapters"]
diff --git a/crates/sdk/README b/crates/sdk/README
@@ -82,12 +82,16 @@ Compile the SDK v1 product examples with:
```bash
cargo check -p radroots_sdk --example sdk_v1_listing_prepare --features runtime
+cargo check -p radroots_sdk --example sdk_v1_knowledge_prepare --features knowledge
cargo check -p radroots_sdk --example sdk_v1_local_enqueue_and_mock_sync --features runtime,signer-adapters
cargo check -p radroots_sdk --example sdk_v1_myc_nip46_signer_setup --features runtime,signer-adapters
```
`sdk_v1_listing_prepare` shows `RadrootsClient::builder()`,
`ListingPreparePublishRequest`, and `ListingPublishPlan`.
+`sdk_v1_knowledge_prepare` shows `radroots_sdk::knowledge`, typed event builders, frozen draft
+preparation, signed-event verification, decoded event matching, and manifest hash access without
+local runtime storage.
`sdk_v1_local_enqueue_and_mock_sync` shows localhost relay target selection, configured local-key
signing, prepared listing enqueue, `push_outbox_with_adapter(...)` with a mock relay adapter, and
`TradeStatusRequest`. `sdk_v1_myc_nip46_signer_setup` shows Myc NIP-46 signer provider setup and
diff --git a/crates/sdk/examples/sdk_v1_knowledge_prepare.rs b/crates/sdk/examples/sdk_v1_knowledge_prepare.rs
@@ -0,0 +1,101 @@
+use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
+use radroots_sdk::knowledge::prelude::*;
+
+const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
+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 signed = sign_parts(parts)?;
+ let decoded = KnowledgeCodec::new().verify_and_decode_radroots_event(signed)?;
+ let manifest = contract_manifest();
+ let manifest_hash = contract_manifest_sha256()?;
+
+ assert_eq!(draft.contract_id, KNOWLEDGE_CLAIM_CONTRACT_ID);
+ assert_eq!(manifest.contract_count, 11);
+ assert_eq!(manifest_hash.len(), 64);
+
+ match decoded {
+ RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
+ assert_eq!(parsed.data.data.text, claim.text);
+ }
+ _ => return Err("expected knowledge claim".into()),
+ }
+
+ println!(
+ "prepared knowledge claim draft: {}",
+ draft.expected_event_id
+ );
+ println!("knowledge manifest sha256: {manifest_hash}");
+ Ok(())
+}
+
+fn sign_parts(parts: WireEventParts) -> Result<RadrootsNostrEvent, Box<dyn std::error::Error>> {
+ let tags = parts
+ .tags
+ .into_iter()
+ .map(Tag::parse)
+ .collect::<Result<Vec<_>, _>>()?;
+ let keys = Keys::parse(SECRET_KEY_HEX)?;
+ let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
+ .tags(tags)
+ .custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
+ .sign_with_keys(&keys)?;
+ Ok(RadrootsNostrEvent {
+ id: event.id.to_hex(),
+ author: event.pubkey.to_hex(),
+ created_at: event.created_at.as_secs() as u32,
+ kind: u32::from(event.kind.as_u16()),
+ tags: event
+ .tags
+ .as_slice()
+ .iter()
+ .map(|tag| tag.as_slice().to_vec())
+ .collect(),
+ content: event.content,
+ sig: event.sig.to_string(),
+ })
+}
+
+fn public_key_hex() -> String {
+ Keys::parse(SECRET_KEY_HEX)
+ .expect("keys")
+ .public_key()
+ .to_hex()
+}
+
+fn hex_64(character: char) -> String {
+ character.to_string().repeat(64)
+}
+
+fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef {
+ RadrootsNostrEventRef {
+ id: hex_64(character),
+ author: hex_64('a'),
+ kind,
+ d_tag: None,
+ relays: Some(vec![RELAY.to_owned()]),
+ }
+}
diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs
@@ -0,0 +1,560 @@
+#[cfg(not(feature = "std"))]
+use alloc::string::{String, ToString};
+#[cfg(feature = "std")]
+use std::string::{String, ToString};
+
+use core::fmt;
+
+pub use radroots_events::{
+ RadrootsNostrEvent, RadrootsNostrEventRef,
+ draft::{RadrootsDraftError, RadrootsFrozenEventDraft},
+ kinds::{
+ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT,
+ KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE,
+ KIND_WIKI_MERGE_REQUEST, KIND_WIKI_REDIRECT,
+ },
+ knowledge::{
+ RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA,
+ RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA,
+ RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
+ RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, RADROOTS_WIKI_D_TAG_MAX_LEN, RadrootsAddressableRef,
+ RadrootsContributionAttestation, RadrootsEvidenceBounty, RadrootsKnowledgeChangeProposal,
+ RadrootsKnowledgeCitationSpan, RadrootsKnowledgeClaim, RadrootsKnowledgeFieldContext,
+ RadrootsKnowledgeFieldReport, RadrootsKnowledgeLocation,
+ RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef, RadrootsKnowledgeObservation,
+ RadrootsKnowledgeObservationValue, RadrootsKnowledgeRelation, RadrootsKnowledgeReview,
+ RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget,
+ RadrootsKnowledgeSource, RadrootsRightsAssertion, RadrootsWikiArticle,
+ RadrootsWikiDTagError, RadrootsWikiMergeRequest, RadrootsWikiRedirect,
+ normalize_wiki_d_tag, validate_wiki_d_tag,
+ },
+};
+pub use radroots_events_codec::{
+ RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION,
+ error::RadrootsEncodeError,
+ manifest::{
+ RadrootsKnowledgeContractManifest, RadrootsKnowledgeContractManifestEntry,
+ RadrootsKnowledgeManifestCodecSupport, RadrootsKnowledgeManifestDiscriminator,
+ RadrootsKnowledgeManifestTagContract,
+ },
+ verification::{
+ RadrootsContractValidatedEvent, RadrootsDecodeError, RadrootsDecodedEvent,
+ RadrootsIdVerifiedEvent, RadrootsNip01VerificationError, RadrootsSignatureVerifiedEvent,
+ },
+ wire::WireEventParts,
+};
+
+use radroots_events_codec::{
+ contract_manifest_json as codec_contract_manifest_json,
+ contract_manifest_sha256 as codec_contract_manifest_sha256,
+ knowledge::{
+ knowledge_claim_to_wire_parts, knowledge_field_report_to_wire_parts,
+ knowledge_relation_to_wire_parts, knowledge_review_to_wire_parts,
+ knowledge_source_to_wire_parts, wiki_article_to_wire_parts,
+ wiki_merge_request_to_wire_parts, wiki_redirect_to_wire_parts,
+ },
+ knowledge_contract_manifest, verify_and_decode_radroots_event as codec_verify_and_decode,
+ wire::to_frozen_draft,
+};
+
+pub const WIKI_ARTICLE_CONTRACT_ID: &str = "radroots.wiki.article.v1";
+pub const WIKI_REDIRECT_CONTRACT_ID: &str = "radroots.wiki.redirect.v1";
+pub const WIKI_MERGE_REQUEST_CONTRACT_ID: &str = "radroots.wiki.merge_request.v1";
+pub const KNOWLEDGE_SOURCE_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_SOURCE_SCHEMA;
+pub const KNOWLEDGE_CLAIM_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_CLAIM_SCHEMA;
+pub const KNOWLEDGE_RELATION_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_RELATION_SCHEMA;
+pub const KNOWLEDGE_REVIEW_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_REVIEW_SCHEMA;
+pub const KNOWLEDGE_FIELD_REPORT_CONTRACT_ID: &str = RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA;
+
+#[derive(Debug)]
+pub enum RadrootsSdkKnowledgeError {
+ Encode(RadrootsEncodeError),
+ Draft(RadrootsDraftError),
+ Decode(RadrootsDecodeError),
+ Manifest(serde_json::Error),
+}
+
+impl RadrootsSdkKnowledgeError {
+ pub const fn code(&self) -> &'static str {
+ match self {
+ Self::Encode(_) => "knowledge_encode",
+ Self::Draft(_) => "knowledge_draft",
+ Self::Decode(_) => "knowledge_decode",
+ Self::Manifest(_) => "knowledge_manifest",
+ }
+ }
+
+ pub fn inner_code(&self) -> &'static str {
+ match self {
+ Self::Encode(error) => error.code(),
+ Self::Draft(error) => draft_error_code(error),
+ Self::Decode(error) => error.code(),
+ Self::Manifest(_) => "json",
+ }
+ }
+}
+
+impl fmt::Display for RadrootsSdkKnowledgeError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ 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"),
+ Self::Manifest(_) => {
+ formatter.write_str("knowledge contract manifest rendering failed")
+ }
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsSdkKnowledgeError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Self::Encode(error) => Some(error),
+ Self::Draft(error) => Some(error),
+ Self::Decode(error) => Some(error),
+ Self::Manifest(error) => Some(error),
+ }
+ }
+}
+
+impl From<RadrootsEncodeError> for RadrootsSdkKnowledgeError {
+ fn from(value: RadrootsEncodeError) -> Self {
+ Self::Encode(value)
+ }
+}
+
+impl From<RadrootsDraftError> for RadrootsSdkKnowledgeError {
+ fn from(value: RadrootsDraftError) -> Self {
+ Self::Draft(value)
+ }
+}
+
+impl From<RadrootsDecodeError> for RadrootsSdkKnowledgeError {
+ fn from(value: RadrootsDecodeError) -> Self {
+ Self::Decode(value)
+ }
+}
+
+impl From<serde_json::Error> for RadrootsSdkKnowledgeError {
+ fn from(value: serde_json::Error) -> Self {
+ Self::Manifest(value)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct KnowledgeEventBuilder;
+
+impl KnowledgeEventBuilder {
+ pub const fn new() -> Self {
+ Self
+ }
+
+ pub fn wiki_article(
+ &self,
+ article: &RadrootsWikiArticle,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_wiki_article_event(article)
+ }
+
+ pub fn wiki_redirect(
+ &self,
+ redirect: &RadrootsWikiRedirect,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_wiki_redirect_event(redirect)
+ }
+
+ pub fn wiki_merge_request(
+ &self,
+ request: &RadrootsWikiMergeRequest,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_wiki_merge_request_event(request)
+ }
+
+ pub fn knowledge_source(
+ &self,
+ source: &RadrootsKnowledgeSource,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_knowledge_source_event(source)
+ }
+
+ pub fn knowledge_claim(
+ &self,
+ claim: &RadrootsKnowledgeClaim,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_knowledge_claim_event(claim)
+ }
+
+ pub fn knowledge_relation(
+ &self,
+ relation: &RadrootsKnowledgeRelation,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_knowledge_relation_event(relation)
+ }
+
+ pub fn knowledge_review(
+ &self,
+ review: &RadrootsKnowledgeReview,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_knowledge_review_event(review)
+ }
+
+ pub fn knowledge_field_report(
+ &self,
+ report: &RadrootsKnowledgeFieldReport,
+ ) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ build_knowledge_field_report_event(report)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct KnowledgeDraftBuilder {
+ expected_pubkey: String,
+ created_at: u32,
+}
+
+impl KnowledgeDraftBuilder {
+ pub fn new(expected_pubkey: impl AsRef<str>, created_at: u32) -> Self {
+ Self {
+ expected_pubkey: expected_pubkey.as_ref().to_string(),
+ created_at,
+ }
+ }
+
+ pub fn expected_pubkey(&self) -> &str {
+ self.expected_pubkey.as_str()
+ }
+
+ pub const fn created_at(&self) -> u32 {
+ self.created_at
+ }
+
+ pub fn wiki_article(
+ &self,
+ article: &RadrootsWikiArticle,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_wiki_article_draft(article, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn wiki_redirect(
+ &self,
+ redirect: &RadrootsWikiRedirect,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_wiki_redirect_draft(redirect, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn wiki_merge_request(
+ &self,
+ request: &RadrootsWikiMergeRequest,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_wiki_merge_request_draft(request, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn knowledge_source(
+ &self,
+ source: &RadrootsKnowledgeSource,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_knowledge_source_draft(source, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn knowledge_claim(
+ &self,
+ claim: &RadrootsKnowledgeClaim,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_knowledge_claim_draft(claim, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn knowledge_relation(
+ &self,
+ relation: &RadrootsKnowledgeRelation,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_knowledge_relation_draft(relation, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn knowledge_review(
+ &self,
+ review: &RadrootsKnowledgeReview,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_knowledge_review_draft(review, self.expected_pubkey(), self.created_at)
+ }
+
+ pub fn knowledge_field_report(
+ &self,
+ report: &RadrootsKnowledgeFieldReport,
+ ) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_knowledge_field_report_draft(report, self.expected_pubkey(), self.created_at)
+ }
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+pub struct KnowledgeCodec;
+
+impl KnowledgeCodec {
+ pub const fn new() -> Self {
+ Self
+ }
+
+ pub fn verify_and_decode_radroots_event(
+ &self,
+ event: RadrootsNostrEvent,
+ ) -> Result<RadrootsDecodedEvent, RadrootsSdkKnowledgeError> {
+ verify_and_decode_radroots_event(event)
+ }
+
+ pub fn contract_manifest(&self) -> RadrootsKnowledgeContractManifest {
+ contract_manifest()
+ }
+
+ pub fn contract_manifest_json(&self) -> Result<String, RadrootsSdkKnowledgeError> {
+ contract_manifest_json()
+ }
+
+ pub fn contract_manifest_sha256(&self) -> Result<String, RadrootsSdkKnowledgeError> {
+ contract_manifest_sha256()
+ }
+}
+
+pub fn build_wiki_article_event(
+ article: &RadrootsWikiArticle,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(wiki_article_to_wire_parts(article)?)
+}
+
+pub fn build_wiki_redirect_event(
+ redirect: &RadrootsWikiRedirect,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(wiki_redirect_to_wire_parts(redirect)?)
+}
+
+pub fn build_wiki_merge_request_event(
+ request: &RadrootsWikiMergeRequest,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(wiki_merge_request_to_wire_parts(request)?)
+}
+
+pub fn build_knowledge_source_event(
+ source: &RadrootsKnowledgeSource,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(knowledge_source_to_wire_parts(source)?)
+}
+
+pub fn build_knowledge_claim_event(
+ claim: &RadrootsKnowledgeClaim,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(knowledge_claim_to_wire_parts(claim)?)
+}
+
+pub fn build_knowledge_relation_event(
+ relation: &RadrootsKnowledgeRelation,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(knowledge_relation_to_wire_parts(relation)?)
+}
+
+pub fn build_knowledge_review_event(
+ review: &RadrootsKnowledgeReview,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(knowledge_review_to_wire_parts(review)?)
+}
+
+pub fn build_knowledge_field_report_event(
+ report: &RadrootsKnowledgeFieldReport,
+) -> Result<WireEventParts, RadrootsSdkKnowledgeError> {
+ Ok(knowledge_field_report_to_wire_parts(report)?)
+}
+
+pub fn prepare_wiki_article_draft(
+ article: &RadrootsWikiArticle,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_wiki_article_event(article)?,
+ WIKI_ARTICLE_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_wiki_redirect_draft(
+ redirect: &RadrootsWikiRedirect,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_wiki_redirect_event(redirect)?,
+ WIKI_REDIRECT_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_wiki_merge_request_draft(
+ request: &RadrootsWikiMergeRequest,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_wiki_merge_request_event(request)?,
+ WIKI_MERGE_REQUEST_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_knowledge_source_draft(
+ source: &RadrootsKnowledgeSource,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_knowledge_source_event(source)?,
+ KNOWLEDGE_SOURCE_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_knowledge_claim_draft(
+ claim: &RadrootsKnowledgeClaim,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_knowledge_claim_event(claim)?,
+ KNOWLEDGE_CLAIM_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_knowledge_relation_draft(
+ relation: &RadrootsKnowledgeRelation,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_knowledge_relation_event(relation)?,
+ KNOWLEDGE_RELATION_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_knowledge_review_draft(
+ review: &RadrootsKnowledgeReview,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_knowledge_review_event(review)?,
+ KNOWLEDGE_REVIEW_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn prepare_knowledge_field_report_draft(
+ report: &RadrootsKnowledgeFieldReport,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ prepare_draft(
+ build_knowledge_field_report_event(report)?,
+ KNOWLEDGE_FIELD_REPORT_CONTRACT_ID,
+ expected_pubkey,
+ created_at,
+ )
+}
+
+pub fn verify_and_decode_radroots_event(
+ event: RadrootsNostrEvent,
+) -> Result<RadrootsDecodedEvent, RadrootsSdkKnowledgeError> {
+ Ok(codec_verify_and_decode(event)?)
+}
+
+pub fn contract_manifest() -> RadrootsKnowledgeContractManifest {
+ knowledge_contract_manifest()
+}
+
+pub fn contract_manifest_json() -> Result<String, RadrootsSdkKnowledgeError> {
+ Ok(codec_contract_manifest_json()?)
+}
+
+pub fn contract_manifest_sha256() -> Result<String, RadrootsSdkKnowledgeError> {
+ Ok(codec_contract_manifest_sha256()?)
+}
+
+fn prepare_draft(
+ parts: WireEventParts,
+ contract_id: &'static str,
+ expected_pubkey: impl AsRef<str>,
+ created_at: u32,
+) -> Result<RadrootsFrozenEventDraft, RadrootsSdkKnowledgeError> {
+ Ok(to_frozen_draft(
+ parts,
+ contract_id,
+ expected_pubkey,
+ created_at,
+ )?)
+}
+
+fn draft_error_code(error: &RadrootsDraftError) -> &'static str {
+ match error {
+ RadrootsDraftError::UnknownContract(_) => "unknown_contract",
+ RadrootsDraftError::ContractKindMismatch { .. } => "contract_kind_mismatch",
+ RadrootsDraftError::SignedEventPubkeyMismatch { .. } => "signed_event_pubkey_mismatch",
+ RadrootsDraftError::SignedEventIdMismatch { .. } => "signed_event_id_mismatch",
+ RadrootsDraftError::SignedEventCreatedAtMismatch { .. } => {
+ "signed_event_created_at_mismatch"
+ }
+ RadrootsDraftError::SignedEventKindMismatch { .. } => "signed_event_kind_mismatch",
+ RadrootsDraftError::SignedEventTagsMismatch { .. } => "signed_event_tags_mismatch",
+ RadrootsDraftError::SignedEventContentMismatch { .. } => "signed_event_content_mismatch",
+ RadrootsDraftError::SignedEventComputedIdMismatch { .. } => {
+ "signed_event_computed_id_mismatch"
+ }
+ RadrootsDraftError::IdParse(_) => "id_parse",
+ RadrootsDraftError::JsonString(_) => "json_string",
+ }
+}
+
+pub mod prelude {
+ pub use super::{
+ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT,
+ KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE,
+ KIND_WIKI_MERGE_REQUEST, KIND_WIKI_REDIRECT, KNOWLEDGE_CLAIM_CONTRACT_ID,
+ KNOWLEDGE_FIELD_REPORT_CONTRACT_ID, KNOWLEDGE_RELATION_CONTRACT_ID,
+ KNOWLEDGE_REVIEW_CONTRACT_ID, KNOWLEDGE_SOURCE_CONTRACT_ID, KnowledgeCodec,
+ KnowledgeDraftBuilder, KnowledgeEventBuilder, RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA,
+ RADROOTS_EVIDENCE_BOUNTY_SCHEMA, RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA,
+ RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION,
+ RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
+ RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, 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, 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, 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,
+ };
+}
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -27,6 +27,8 @@ mod geonames;
mod idempotency;
#[cfg(feature = "identity-models")]
pub mod identity;
+#[cfg(feature = "knowledge")]
+pub mod knowledge;
#[cfg(feature = "runtime")]
mod listings_runtime;
#[cfg(feature = "runtime")]
@@ -91,6 +93,42 @@ pub use crate::geonames::{
pub use crate::idempotency::{
SDK_IDEMPOTENCY_KEY_MAX_LEN, SdkIdempotencyKey, SdkTradeIdempotencyRecord,
};
+#[cfg(feature = "knowledge")]
+pub use crate::knowledge::{
+ KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, KIND_KNOWLEDGE_RELATION,
+ KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, KIND_WIKI_MERGE_REQUEST,
+ KIND_WIKI_REDIRECT, KNOWLEDGE_CLAIM_CONTRACT_ID, KNOWLEDGE_FIELD_REPORT_CONTRACT_ID,
+ KNOWLEDGE_RELATION_CONTRACT_ID, KNOWLEDGE_REVIEW_CONTRACT_ID, KNOWLEDGE_SOURCE_CONTRACT_ID,
+ KnowledgeCodec, KnowledgeDraftBuilder, KnowledgeEventBuilder,
+ RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA,
+ RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA,
+ RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA,
+ RADROOTS_KNOWLEDGE_RELATION_SCHEMA, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA,
+ 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,
+ 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,
+ 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,
+};
#[cfg(feature = "runtime")]
pub use crate::listings_runtime::{
LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, ListingEnqueueReceipt,
diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs
@@ -0,0 +1,345 @@
+#![cfg(feature = "knowledge")]
+
+use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
+use radroots_sdk::knowledge::prelude::*;
+
+const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
+const CREATED_AT: u32 = 1_800_000_000;
+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())
+ .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");
+
+ assert_eq!(article.kind, KIND_WIKI_ARTICLE);
+ assert_eq!(redirect.kind, KIND_WIKI_REDIRECT);
+ assert_eq!(merge_request.kind, KIND_WIKI_MERGE_REQUEST);
+ assert_eq!(source.kind, KIND_KNOWLEDGE_SOURCE);
+ assert_eq!(claim.kind, KIND_KNOWLEDGE_CLAIM);
+ assert_eq!(relation.kind, KIND_KNOWLEDGE_RELATION);
+ assert_eq!(review.kind, KIND_KNOWLEDGE_REVIEW);
+ assert_eq!(field_report.kind, KIND_KNOWLEDGE_FIELD_REPORT);
+}
+
+#[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 source = draft_builder
+ .knowledge_source(&knowledge_source())
+ .expect("source draft");
+ let claim = draft_builder
+ .knowledge_claim(&knowledge_claim())
+ .expect("claim draft");
+ let relation = draft_builder
+ .knowledge_relation(&knowledge_relation())
+ .expect("relation draft");
+ let review = draft_builder
+ .knowledge_review(&knowledge_review())
+ .expect("review draft");
+ let field_report = draft_builder
+ .knowledge_field_report(&knowledge_field_report())
+ .expect("field report draft");
+
+ assert_eq!(article.contract_id, WIKI_ARTICLE_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);
+ assert_eq!(review.contract_id, KNOWLEDGE_REVIEW_CONTRACT_ID);
+ assert_eq!(field_report.contract_id, KNOWLEDGE_FIELD_REPORT_CONTRACT_ID);
+ assert_eq!(claim.expected_pubkey, public_key_hex());
+ assert_eq!(claim.created_at, CREATED_AT);
+ assert_eq!(claim.kind, KIND_KNOWLEDGE_CLAIM);
+}
+
+#[test]
+fn knowledge_codec_exposes_manifest_and_verified_decode() {
+ let codec = KnowledgeCodec::new();
+ let manifest = codec.contract_manifest();
+
+ assert_eq!(
+ manifest.schema_version,
+ RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION
+ );
+ assert_eq!(manifest.contract_count, 11);
+ assert!(
+ manifest
+ .contracts
+ .iter()
+ .any(|contract| contract.contract_id == KNOWLEDGE_CLAIM_CONTRACT_ID)
+ );
+
+ let signed = sign_parts(
+ KnowledgeEventBuilder::new()
+ .knowledge_claim(&knowledge_claim())
+ .expect("claim parts"),
+ );
+ let decoded = codec
+ .verify_and_decode_radroots_event(signed)
+ .expect("decoded claim");
+
+ match decoded {
+ RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
+ assert_eq!(parsed.data.data.text, "Cover crops improve soil structure.");
+ }
+ _ => panic!("expected knowledge claim"),
+ }
+
+ let sha256 = codec.contract_manifest_sha256().expect("manifest sha256");
+ assert_eq!(sha256.len(), 64);
+}
+
+#[test]
+fn knowledge_errors_expose_stable_codes() {
+ let mut article = wiki_article();
+ article.d_tag = "Soil Health".to_owned();
+ let error = build_wiki_article_event(&article).expect_err("invalid d tag");
+
+ assert_eq!(error.code(), "knowledge_encode");
+ assert_eq!(error.inner_code(), "invalid_field");
+ assert!(!error.to_string().contains(article.content_djot.as_str()));
+}
+
+fn sign_parts(parts: WireEventParts) -> RadrootsNostrEvent {
+ let tags = parts
+ .tags
+ .into_iter()
+ .map(Tag::parse)
+ .collect::<Result<Vec<_>, _>>()
+ .expect("tags");
+ let keys = Keys::parse(SECRET_KEY_HEX).expect("keys");
+ let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
+ .tags(tags)
+ .custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
+ .sign_with_keys(&keys)
+ .expect("signed event");
+ RadrootsNostrEvent {
+ id: event.id.to_hex(),
+ author: event.pubkey.to_hex(),
+ created_at: event.created_at.as_secs() as u32,
+ kind: u32::from(event.kind.as_u16()),
+ tags: event
+ .tags
+ .as_slice()
+ .iter()
+ .map(|tag| tag.as_slice().to_vec())
+ .collect(),
+ content: event.content,
+ sig: event.sig.to_string(),
+ }
+}
+
+fn public_key_hex() -> String {
+ Keys::parse(SECRET_KEY_HEX)
+ .expect("keys")
+ .public_key()
+ .to_hex()
+}
+
+fn hex_64(character: char) -> String {
+ character.to_string().repeat(64)
+}
+
+fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef {
+ RadrootsNostrEventRef {
+ id: hex_64(character),
+ author: hex_64('a'),
+ kind,
+ d_tag: None,
+ relays: Some(vec![RELAY.to_owned()]),
+ }
+}
+
+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,
+ pubkey: hex_64('a'),
+ d_tag: "soil-health".to_owned(),
+ relays: vec![RELAY.to_owned()],
+ }
+}
+
+fn wiki_article() -> RadrootsWikiArticle {
+ RadrootsWikiArticle {
+ d_tag: "soil-health".to_owned(),
+ title: "Soil health".to_owned(),
+ content_djot: "# Soil health".to_owned(),
+ summary: Some("Living soil basics".to_owned()),
+ topics: vec!["soil".to_owned(), "local-food".to_owned()],
+ references: vec![event_ref('1', KIND_KNOWLEDGE_SOURCE)],
+ forked_from: Vec::new(),
+ deferred_to: None,
+ }
+}
+
+fn wiki_redirect() -> RadrootsWikiRedirect {
+ RadrootsWikiRedirect {
+ d_tag: "soil".to_owned(),
+ target: article_ref(),
+ }
+}
+
+fn wiki_merge_request() -> RadrootsWikiMergeRequest {
+ RadrootsWikiMergeRequest {
+ target_article: address_ref(),
+ destination_pubkey: hex_64('a'),
+ base_version_event_id: Some(hex_64('e')),
+ source_version_event_id: hex_64('f'),
+ explanation: Some("Merge synthetic soil article updates".to_owned()),
+ }
+}
+
+fn knowledge_source() -> RadrootsKnowledgeSource {
+ RadrootsKnowledgeSource {
+ schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_owned(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ d_tag: "soil-source".to_owned(),
+ title: "Soil Source".to_owned(),
+ source_type: "book".to_owned(),
+ authors: vec!["A. Example".to_owned()],
+ publisher: Some("Radroots Synthetic Press".to_owned()),
+ publication_year: Some(2026),
+ edition: None,
+ canonical_url: Some("https://source.example.test/soil-source".to_owned()),
+ artifact_refs: vec![event_ref('3', KIND_FILE_METADATA)],
+ author_asserted_rights: None,
+ topics: vec!["soil".to_owned()],
+ summary: Some("Synthetic source for SDK coverage".to_owned()),
+ }
+}
+
+fn knowledge_claim() -> RadrootsKnowledgeClaim {
+ 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(),
+ }
+}
+
+fn knowledge_node_ref(label: &str) -> RadrootsKnowledgeNodeRef {
+ RadrootsKnowledgeNodeRef {
+ node_type: "event".to_owned(),
+ event_ref: Some(event_ref('6', KIND_KNOWLEDGE_CLAIM)),
+ address_ref: None,
+ external_id: None,
+ label: Some(label.to_owned()),
+ }
+}
+
+fn knowledge_relation() -> RadrootsKnowledgeRelation {
+ RadrootsKnowledgeRelation {
+ schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_owned(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ subject: knowledge_node_ref("cover crops"),
+ predicate: "supports".to_owned(),
+ object: knowledge_node_ref("soil structure"),
+ support_refs: vec![event_ref('7', KIND_KNOWLEDGE_CLAIM)],
+ author_asserted_confidence: Some("medium".to_owned()),
+ supersedes: Vec::new(),
+ }
+}
+
+fn knowledge_review() -> RadrootsKnowledgeReview {
+ RadrootsKnowledgeReview {
+ schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_owned(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ 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".to_owned(),
+ verdict: "needs_more_evidence".to_owned(),
+ scores: vec![RadrootsKnowledgeReviewScore {
+ dimension: "evidence".to_owned(),
+ value: "partial".to_owned(),
+ note: None,
+ }],
+ notes: Some("Synthetic review".to_owned()),
+ evidence_refs: vec![event_ref('9', KIND_KNOWLEDGE_SOURCE)],
+ }
+}
+
+fn knowledge_field_report() -> RadrootsKnowledgeFieldReport {
+ RadrootsKnowledgeFieldReport {
+ schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_owned(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ report_type: "observation".to_owned(),
+ title: "Field observation".to_owned(),
+ summary: Some("Observed cover crop residue.".to_owned()),
+ 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()],
+ },
+ observations: vec![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_refs: vec![event_ref('c', KIND_FILE_METADATA)],
+ related_refs: vec![event_ref('d', KIND_KNOWLEDGE_CLAIM)],
+ limitations: vec!["single observer".to_owned()],
+ }
+}
diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs
@@ -10,6 +10,7 @@ mod manifest;
mod output;
mod package_matrix;
mod package_metadata;
+mod smoke;
mod ts;
mod wasm;
mod wasm_declarations;
@@ -20,6 +21,7 @@ enum CommandAction<'a> {
GeneratePackageMetadata,
Coverage(&'a [String]),
Check,
+ Smoke(&'a [String]),
}
fn main() {
@@ -37,6 +39,7 @@ fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> {
CommandAction::GeneratePackageMetadata => generate::generate_package_metadata(),
CommandAction::Coverage(rest) => coverage::run(rest),
CommandAction::Check => check::check(),
+ CommandAction::Smoke(rest) => smoke::run(rest),
}
}
@@ -53,13 +56,14 @@ fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> {
}
[command, rest @ ..] if command == "coverage" => Ok(CommandAction::Coverage(rest)),
[command] if command == "check" => Ok(CommandAction::Check),
+ [command, rest @ ..] if command == "smoke" => Ok(CommandAction::Smoke(rest)),
[] => Err(usage()),
_ => Err(usage()),
}
}
fn usage() -> String {
- "usage: cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask coverage run"
+ "usage: cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke knowledge-rust-local | cargo xtask coverage run"
.to_owned()
}
@@ -104,6 +108,15 @@ mod tests {
}
#[test]
+ fn accepts_smoke() {
+ let args = ["smoke".to_owned(), "knowledge-rust-local".to_owned()];
+ assert!(matches!(
+ command_action(&args).expect("action"),
+ CommandAction::Smoke(rest) if rest == ["knowledge-rust-local"]
+ ));
+ }
+
+ #[test]
fn accepts_coverage_run() {
let args = ["coverage".to_owned(), "run".to_owned()];
assert!(matches!(
diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs
@@ -0,0 +1,144 @@
+use std::{fs, path::Path, process::Command};
+
+use crate::fs::workspace_root;
+
+pub fn run(args: &[String]) -> Result<(), String> {
+ match args {
+ [target] if target == "knowledge-rust-local" => knowledge_rust_local(),
+ _ => Err("usage: cargo xtask smoke knowledge-rust-local".to_owned()),
+ }
+}
+
+fn knowledge_rust_local() -> Result<(), String> {
+ let root = workspace_root()?;
+ let sdk_path = root.join("crates/sdk");
+ let tempdir =
+ tempfile::tempdir().map_err(|error| format!("failed to create smoke tempdir: {error}"))?;
+ write_consumer(&tempdir.path().join("Cargo.toml"), &sdk_path)?;
+ let src_dir = tempdir.path().join("src");
+ fs::create_dir_all(&src_dir)
+ .map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?;
+ fs::write(src_dir.join("main.rs"), CONSUMER_MAIN)
+ .map_err(|error| format!("failed to write smoke consumer main.rs: {error}"))?;
+ let status = Command::new("cargo")
+ .arg("check")
+ .arg("--quiet")
+ .current_dir(tempdir.path())
+ .status()
+ .map_err(|error| format!("failed to run smoke cargo check: {error}"))?;
+ if status.success() {
+ Ok(())
+ } else {
+ Err(format!(
+ "knowledge Rust local smoke failed with status {status}"
+ ))
+ }
+}
+
+fn write_consumer(path: &Path, sdk_path: &Path) -> Result<(), String> {
+ let sdk_path = serde_json::to_string(&sdk_path.to_string_lossy())
+ .map_err(|error| format!("failed to render SDK path: {error}"))?;
+ let cargo_toml = format!(
+ r#"[package]
+name = "radroots-sdk-knowledge-smoke"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[dependencies]
+radroots_sdk = {{ path = {sdk_path}, features = ["std", "serde", "serde_json", "nostr", "knowledge"] }}
+nostr = "0.44.2"
+"#
+ );
+ fs::write(path, cargo_toml)
+ .map_err(|error| format!("failed to write {}: {error}", path.display()))
+}
+
+const CONSUMER_MAIN: &str = r#"use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
+use radroots_sdk::knowledge::prelude::*;
+
+const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
+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 decoded = verify_and_decode_radroots_event(sign_parts(parts)?)?;
+ let manifest = contract_manifest();
+
+ assert_eq!(draft.contract_id, KNOWLEDGE_CLAIM_CONTRACT_ID);
+ assert_eq!(manifest.contract_count, 11);
+ match decoded {
+ RadrootsDecodedEvent::KnowledgeClaim(parsed) => assert_eq!(parsed.data.data.text, claim.text),
+ _ => return Err("expected knowledge claim".into()),
+ }
+ Ok(())
+}
+
+fn sign_parts(parts: WireEventParts) -> Result<RadrootsNostrEvent, Box<dyn std::error::Error>> {
+ let tags = parts
+ .tags
+ .into_iter()
+ .map(Tag::parse)
+ .collect::<Result<Vec<_>, _>>()?;
+ let keys = Keys::parse(SECRET_KEY_HEX)?;
+ let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
+ .tags(tags)
+ .custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
+ .sign_with_keys(&keys)?;
+ Ok(RadrootsNostrEvent {
+ id: event.id.to_hex(),
+ author: event.pubkey.to_hex(),
+ created_at: event.created_at.as_secs() as u32,
+ kind: u32::from(event.kind.as_u16()),
+ tags: event
+ .tags
+ .as_slice()
+ .iter()
+ .map(|tag| tag.as_slice().to_vec())
+ .collect(),
+ content: event.content,
+ sig: event.sig.to_string(),
+ })
+}
+
+fn public_key_hex() -> String {
+ Keys::parse(SECRET_KEY_HEX)
+ .expect("keys")
+ .public_key()
+ .to_hex()
+}
+
+fn hex_64(character: char) -> String {
+ character.to_string().repeat(64)
+}
+
+fn event_ref(character: char, kind: u32) -> RadrootsNostrEventRef {
+ RadrootsNostrEventRef {
+ id: hex_64(character),
+ author: hex_64('a'),
+ kind,
+ d_tag: None,
+ relays: Some(vec!["wss://relay.radroots.example".to_owned()]),
+ }
+}
+"#;