commit 9e1462ae7e9acd785ca09acf0cc81049a68d262d
parent 4638baeffcb0be18c0ee32b799d88422f6edb7d8
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 06:55:18 +0000
wasm: add knowledge event exports
Diffstat:
8 files changed, 639 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1819,6 +1819,7 @@ dependencies = [
name = "radroots_events_codec_wasm"
version = "0.1.0-alpha.2"
dependencies = [
+ "nostr",
"radroots_core",
"radroots_events",
"radroots_events_codec",
diff --git a/crates/events_bindings/src/model.rs b/crates/events_bindings/src/model.rs
@@ -45,6 +45,9 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
("KIND_GIFT_WRAP", kinds::KIND_GIFT_WRAP),
("KIND_COMMENT", kinds::KIND_COMMENT),
("KIND_GEOCHAT", kinds::KIND_GEOCHAT),
+ ("KIND_WIKI_MERGE_REQUEST", kinds::KIND_WIKI_MERGE_REQUEST),
+ ("KIND_WIKI_ARTICLE", kinds::KIND_WIKI_ARTICLE),
+ ("KIND_WIKI_REDIRECT", kinds::KIND_WIKI_REDIRECT),
("KIND_LIST_MUTE", kinds::KIND_LIST_MUTE),
("KIND_LIST_PINNED_NOTES", kinds::KIND_LIST_PINNED_NOTES),
(
@@ -164,6 +167,23 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
"KIND_TRADE_VALIDATION_RECEIPT",
kinds::KIND_TRADE_VALIDATION_RECEIPT,
),
+ ("KIND_KNOWLEDGE_CLAIM", kinds::KIND_KNOWLEDGE_CLAIM),
+ ("KIND_KNOWLEDGE_RELATION", kinds::KIND_KNOWLEDGE_RELATION),
+ ("KIND_KNOWLEDGE_REVIEW", kinds::KIND_KNOWLEDGE_REVIEW),
+ (
+ "KIND_KNOWLEDGE_FIELD_REPORT",
+ kinds::KIND_KNOWLEDGE_FIELD_REPORT,
+ ),
+ (
+ "KIND_KNOWLEDGE_CHANGE_PROPOSAL",
+ kinds::KIND_KNOWLEDGE_CHANGE_PROPOSAL,
+ ),
+ (
+ "KIND_CONTRIBUTION_ATTESTATION",
+ kinds::KIND_CONTRIBUTION_ATTESTATION,
+ ),
+ ("KIND_KNOWLEDGE_SOURCE", kinds::KIND_KNOWLEDGE_SOURCE),
+ ("KIND_EVIDENCE_BOUNTY", kinds::KIND_EVIDENCE_BOUNTY),
("KIND_JOB_REQUEST_MIN", kinds::KIND_JOB_REQUEST_MIN),
("KIND_JOB_REQUEST_MAX", kinds::KIND_JOB_REQUEST_MAX),
("KIND_JOB_RESULT_MIN", kinds::KIND_JOB_RESULT_MIN),
diff --git a/crates/events_codec_wasm/Cargo.toml b/crates/events_codec_wasm/Cargo.toml
@@ -18,16 +18,21 @@ crate-type = ["cdylib", "rlib"]
radroots_events = { workspace = true, default-features = false, features = [
"std",
"serde",
+ "knowledge-nip54",
] }
radroots_events_codec = { workspace = true, default-features = false, features = [
"std",
"serde_json",
+ "nostr",
+ "knowledge-nip54",
+ "contract-manifest",
] }
serde = { workspace = true }
serde_json = { workspace = true }
wasm-bindgen = { workspace = true }
[dev-dependencies]
+nostr = { workspace = true }
radroots_core = { workspace = true, default-features = false, features = [
"std",
] }
diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs
@@ -25,6 +25,11 @@ use radroots_events::http_auth::RadrootsHttpAuth;
use radroots_events::job_feedback::RadrootsJobFeedback;
use radroots_events::job_request::RadrootsJobRequest;
use radroots_events::job_result::RadrootsJobResult;
+use radroots_events::knowledge::{
+ RadrootsKnowledgeClaim, RadrootsKnowledgeFieldReport, RadrootsKnowledgeRelation,
+ RadrootsKnowledgeReview, RadrootsKnowledgeSource, RadrootsWikiArticle,
+ RadrootsWikiMergeRequest, RadrootsWikiRedirect,
+};
use radroots_events::list::RadrootsList;
use radroots_events::list_set::RadrootsListSet;
use radroots_events::listing::RadrootsListing;
@@ -63,6 +68,11 @@ use radroots_events_codec::http_auth::encode::http_auth_build_tags;
use radroots_events_codec::job::feedback::encode::job_feedback_build_tags;
use radroots_events_codec::job::request::encode::job_request_build_tags;
use radroots_events_codec::job::result::encode::job_result_build_tags;
+use radroots_events_codec::knowledge::{
+ knowledge_claim_build_tags, knowledge_field_report_build_tags, knowledge_relation_build_tags,
+ knowledge_review_build_tags, knowledge_source_build_tags, wiki_article_build_tags,
+ wiki_merge_request_build_tags, wiki_redirect_build_tags,
+};
use radroots_events_codec::list::encode::list_build_tags;
use radroots_events_codec::list_set::encode::list_set_build_tags;
use radroots_events_codec::listing::tags::{
@@ -77,6 +87,8 @@ use radroots_events_codec::relay_auth::encode::relay_auth_build_tags;
use radroots_events_codec::report::encode::report_build_tags;
use radroots_events_codec::repost::encode::{generic_repost_build_tags, repost_build_tags};
use radroots_events_codec::seal::encode::seal_build_tags;
+use radroots_events_codec::verification::{RadrootsDecodeError, RadrootsDecodedEvent};
+use serde::Serialize;
use serde::de::DeserializeOwned;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsValue;
@@ -100,6 +112,14 @@ fn err_js<E: ToString>(err: E) -> RadrootsJsValue {
}
}
+fn error_json(code: &str, inner_code: Option<&str>) -> RadrootsJsValue {
+ let value = serde_json::json!({
+ "code": code,
+ "inner_code": inner_code,
+ });
+ err_js(value)
+}
+
fn normalized_payload(input: &str) -> &str {
if input.is_empty() { "{}" } else { input }
}
@@ -112,6 +132,10 @@ fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
serde_json::to_string(&tags).map_err(err_js)
}
+fn parse_event_json(input: &str) -> Result<radroots_events::RadrootsNostrEvent, RadrootsJsValue> {
+ serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("event_json")))
+}
+
fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
where
T: DeserializeOwned,
@@ -133,6 +157,94 @@ where
tags_to_json(tags)
}
+#[derive(Serialize)]
+struct DecodedEventJson<'a, T>
+where
+ T: Serialize,
+{
+ event_type: &'static str,
+ contract_id: &'static str,
+ event: &'a radroots_events::RadrootsNostrEvent,
+ payload: &'a T,
+}
+
+fn decoded_event_to_json(decoded: RadrootsDecodedEvent) -> Result<String, RadrootsJsValue> {
+ match decoded {
+ RadrootsDecodedEvent::WikiArticle(parsed) => {
+ decoded_payload_to_json("wiki_article", "radroots.wiki.article.v1", &parsed)
+ }
+ RadrootsDecodedEvent::WikiRedirect(parsed) => {
+ decoded_payload_to_json("wiki_redirect", "radroots.wiki.redirect.v1", &parsed)
+ }
+ RadrootsDecodedEvent::WikiMergeRequest(parsed) => decoded_payload_to_json(
+ "wiki_merge_request",
+ "radroots.wiki.merge_request.v1",
+ &parsed,
+ ),
+ RadrootsDecodedEvent::KnowledgeSource(parsed) => {
+ decoded_payload_to_json("knowledge_source", "radroots.knowledge.source.v1", &parsed)
+ }
+ RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
+ decoded_payload_to_json("knowledge_claim", "radroots.knowledge.claim.v1", &parsed)
+ }
+ RadrootsDecodedEvent::KnowledgeRelation(parsed) => decoded_payload_to_json(
+ "knowledge_relation",
+ "radroots.knowledge.relation.v1",
+ &parsed,
+ ),
+ RadrootsDecodedEvent::KnowledgeReview(parsed) => {
+ decoded_payload_to_json("knowledge_review", "radroots.knowledge.review.v1", &parsed)
+ }
+ RadrootsDecodedEvent::KnowledgeFieldReport(parsed) => decoded_payload_to_json(
+ "knowledge_field_report",
+ "radroots.knowledge.field_report.v1",
+ &parsed,
+ ),
+ RadrootsDecodedEvent::EvidenceBounty(parsed) => decoded_payload_to_json(
+ "evidence_bounty",
+ "radroots.knowledge.evidence_bounty.v1",
+ &parsed,
+ ),
+ RadrootsDecodedEvent::KnowledgeChangeProposal(parsed) => decoded_payload_to_json(
+ "knowledge_change_proposal",
+ "radroots.knowledge.change_proposal.v1",
+ &parsed,
+ ),
+ RadrootsDecodedEvent::ContributionAttestation(parsed) => decoded_payload_to_json(
+ "contribution_attestation",
+ "radroots.knowledge.contribution_attestation.v1",
+ &parsed,
+ ),
+ }
+}
+
+fn decoded_payload_to_json<T>(
+ event_type: &'static str,
+ contract_id: &'static str,
+ parsed: &radroots_events_codec::parsed::RadrootsParsedEvent<T>,
+) -> Result<String, RadrootsJsValue>
+where
+ T: Serialize,
+{
+ serde_json::to_string(&DecodedEventJson {
+ event_type,
+ contract_id,
+ event: &parsed.event,
+ payload: &parsed.data.data,
+ })
+ .map_err(err_js)
+}
+
+fn decode_error_json(error: RadrootsDecodeError) -> RadrootsJsValue {
+ let inner = match &error {
+ RadrootsDecodeError::Nip01Verification(error) => Some(error.code()),
+ RadrootsDecodeError::ContractValidation(error) => Some(error.code()),
+ RadrootsDecodeError::EventParse(error) => Some(error.code()),
+ RadrootsDecodeError::UnsupportedContract { .. } => None,
+ };
+ error_json(error.code(), inner)
+}
+
#[derive(serde::Deserialize)]
struct FarmCrdtTagsInput {
change: RadrootsFarmCrdtChange,
@@ -164,6 +276,62 @@ pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
build_tags_json::<RadrootsArticle, _, _>(article_json, article_build_tags)
}
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_article_tags))]
+pub fn wiki_article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsWikiArticle, _, _>(article_json, wiki_article_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_redirect_tags))]
+pub fn wiki_redirect_tags(redirect_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsWikiRedirect, _, _>(redirect_json, wiki_redirect_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_merge_request_tags))]
+pub fn wiki_merge_request_tags(request_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsWikiMergeRequest, _, _>(request_json, wiki_merge_request_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_source_tags))]
+pub fn knowledge_source_tags(source_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsKnowledgeSource, _, _>(source_json, knowledge_source_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_claim_tags))]
+pub fn knowledge_claim_tags(claim_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsKnowledgeClaim, _, _>(claim_json, knowledge_claim_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_relation_tags))]
+pub fn knowledge_relation_tags(relation_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsKnowledgeRelation, _, _>(relation_json, knowledge_relation_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_review_tags))]
+pub fn knowledge_review_tags(review_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsKnowledgeReview, _, _>(review_json, knowledge_review_build_tags)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_field_report_tags))]
+pub fn knowledge_field_report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsKnowledgeFieldReport, _, _>(
+ report_json,
+ knowledge_field_report_build_tags,
+ )
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = verify_and_decode_event_json))]
+pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, RadrootsJsValue> {
+ let event = parse_event_json(event_json)?;
+ let decoded = radroots_events_codec::verify_and_decode_radroots_event(event)
+ .map_err(decode_error_json)?;
+ decoded_event_to_json(decoded)
+}
+
+#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))]
+pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> {
+ radroots_events_codec::contract_manifest_json().map_err(err_js)
+}
+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))]
pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> {
build_tags_json::<RadrootsFileMetadata, _, _>(metadata_json, file_metadata_build_tags)
@@ -417,7 +585,19 @@ mod tests {
use radroots_events::http_auth::RadrootsHttpAuth;
use radroots_events::job::JobInputType;
use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam};
- use radroots_events::kinds::KIND_FARM_FILE_METADATA;
+ use radroots_events::kinds::{
+ KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE,
+ KIND_WIKI_ARTICLE,
+ };
+ use radroots_events::knowledge::{
+ 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,
+ RadrootsAddressableRef, RadrootsKnowledgeCitationSpan, RadrootsKnowledgeFieldContext,
+ RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef,
+ RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue,
+ RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget,
+ };
use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct};
use radroots_events::relay_auth::RadrootsRelayAuth;
use radroots_events::social::{
@@ -512,6 +692,221 @@ mod tests {
}
}
+ fn knowledge_event_ref(seed: char, kind: u32) -> radroots_events::RadrootsNostrEventRef {
+ radroots_events::RadrootsNostrEventRef {
+ id: synthetic_event_id(seed),
+ author: synthetic_pubkey('a'),
+ kind,
+ d_tag: None,
+ relays: Some(vec!["wss://relay.example.test".to_string()]),
+ }
+ }
+
+ fn knowledge_address_ref() -> RadrootsAddressableRef {
+ RadrootsAddressableRef {
+ kind: KIND_WIKI_ARTICLE,
+ pubkey: synthetic_pubkey('a'),
+ d_tag: "soil-health".to_string(),
+ relays: vec!["wss://relay.example.test".to_string()],
+ }
+ }
+
+ fn sample_wiki_article() -> RadrootsWikiArticle {
+ RadrootsWikiArticle {
+ d_tag: "soil-health".to_string(),
+ title: "Soil health".to_string(),
+ content_djot: "# Soil health".to_string(),
+ summary: Some("Living soil basics".to_string()),
+ topics: vec!["soil".to_string()],
+ references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)],
+ forked_from: Vec::new(),
+ deferred_to: None,
+ }
+ }
+
+ fn sample_wiki_redirect() -> RadrootsWikiRedirect {
+ RadrootsWikiRedirect {
+ d_tag: "soil".to_string(),
+ target: radroots_events::RadrootsNostrEventRef {
+ id: synthetic_event_id('b'),
+ author: synthetic_pubkey('a'),
+ kind: KIND_WIKI_ARTICLE,
+ d_tag: Some("soil-health".to_string()),
+ relays: Some(vec!["wss://relay.example.test".to_string()]),
+ },
+ }
+ }
+
+ fn sample_wiki_merge_request() -> RadrootsWikiMergeRequest {
+ RadrootsWikiMergeRequest {
+ target_article: knowledge_address_ref(),
+ destination_pubkey: synthetic_pubkey('a'),
+ base_version_event_id: Some(synthetic_event_id('e')),
+ source_version_event_id: synthetic_event_id('f'),
+ explanation: Some("Merge synthetic soil article updates".to_string()),
+ }
+ }
+
+ fn sample_knowledge_source() -> RadrootsKnowledgeSource {
+ RadrootsKnowledgeSource {
+ schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ d_tag: "soil-source".to_string(),
+ title: "Soil Source".to_string(),
+ source_type: "book".to_string(),
+ authors: vec!["A. Example".to_string()],
+ publisher: Some("Radroots Synthetic Press".to_string()),
+ publication_year: Some(2026),
+ edition: None,
+ canonical_url: Some("https://source.example.test/soil-source".to_string()),
+ artifact_refs: vec![knowledge_event_ref('3', KIND_FILE_METADATA)],
+ author_asserted_rights: None,
+ topics: vec!["soil".to_string()],
+ summary: Some("Synthetic source for wasm coverage".to_string()),
+ }
+ }
+
+ fn sample_knowledge_claim() -> RadrootsKnowledgeClaim {
+ RadrootsKnowledgeClaim {
+ schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ claim_type: "practice_effect".to_string(),
+ text: "Cover crops improve soil structure.".to_string(),
+ citation_spans: vec![RadrootsKnowledgeCitationSpan {
+ source_ref: knowledge_event_ref('4', KIND_KNOWLEDGE_SOURCE),
+ artifact_ref: None,
+ page_start: Some(12),
+ page_end: Some(13),
+ section_path: vec!["chapter-1".to_string()],
+ quote_hash: Some(synthetic_event_id('5')),
+ chunk_id: Some("chunk-1".to_string()),
+ }],
+ topics: vec!["cover-crops".to_string()],
+ applies_to: vec!["local-food".to_string()],
+ author_asserted_confidence: Some("medium".to_string()),
+ supersedes: Vec::new(),
+ }
+ }
+
+ fn sample_knowledge_node_ref(label: &str) -> RadrootsKnowledgeNodeRef {
+ RadrootsKnowledgeNodeRef {
+ node_type: "event".to_string(),
+ event_ref: Some(knowledge_event_ref('6', KIND_KNOWLEDGE_CLAIM)),
+ address_ref: None,
+ external_id: None,
+ label: Some(label.to_string()),
+ }
+ }
+
+ fn sample_knowledge_relation() -> RadrootsKnowledgeRelation {
+ RadrootsKnowledgeRelation {
+ schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ subject: sample_knowledge_node_ref("cover crops"),
+ predicate: "supports".to_string(),
+ object: sample_knowledge_node_ref("soil structure"),
+ support_refs: vec![knowledge_event_ref('7', KIND_KNOWLEDGE_CLAIM)],
+ author_asserted_confidence: Some("medium".to_string()),
+ supersedes: Vec::new(),
+ }
+ }
+
+ fn sample_knowledge_review() -> RadrootsKnowledgeReview {
+ RadrootsKnowledgeReview {
+ schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ target: RadrootsKnowledgeReviewTarget {
+ event_id: synthetic_event_id('8'),
+ author_pubkey: synthetic_pubkey('a'),
+ kind: KIND_KNOWLEDGE_CLAIM,
+ address: None,
+ relays: vec!["wss://relay.example.test".to_string()],
+ review_scope: RadrootsKnowledgeReviewScope::SpecificVersion,
+ },
+ reviewer_role: "peer".to_string(),
+ verdict: "needs_more_evidence".to_string(),
+ scores: vec![RadrootsKnowledgeReviewScore {
+ dimension: "evidence".to_string(),
+ value: "partial".to_string(),
+ note: None,
+ }],
+ notes: Some("Synthetic review".to_string()),
+ evidence_refs: vec![knowledge_event_ref('9', KIND_KNOWLEDGE_SOURCE)],
+ }
+ }
+
+ fn sample_knowledge_field_report() -> RadrootsKnowledgeFieldReport {
+ RadrootsKnowledgeFieldReport {
+ schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(),
+ schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
+ report_type: "observation".to_string(),
+ title: "Field observation".to_string(),
+ summary: Some("Observed cover crop residue.".to_string()),
+ context: RadrootsKnowledgeFieldContext {
+ location_precision: RadrootsKnowledgeLocationPrecision::CoarseGeohash,
+ public_location: Some(RadrootsKnowledgeLocation {
+ label: Some("watershed".to_string()),
+ region: Some("synthetic-region".to_string()),
+ locality: None,
+ geohash: Some("c23".to_string()),
+ }),
+ private_location_ref: None,
+ topics: vec!["field".to_string()],
+ context_tags: vec!["observation".to_string()],
+ },
+ observations: vec![RadrootsKnowledgeObservation {
+ observation_type: "residue".to_string(),
+ text: "Residue was visible across beds.".to_string(),
+ observed_at: Some("2026-07-05".to_string()),
+ values: vec![RadrootsKnowledgeObservationValue {
+ key: "coverage".to_string(),
+ value: "medium".to_string(),
+ unit: None,
+ }],
+ }],
+ artifact_refs: vec![knowledge_event_ref('c', KIND_FILE_METADATA)],
+ related_refs: vec![knowledge_event_ref('d', KIND_KNOWLEDGE_CLAIM)],
+ limitations: vec!["single observer".to_string()],
+ }
+ }
+
+ fn signed_claim_event_json() -> String {
+ let claim_json = serde_json::to_string(&sample_knowledge_claim()).expect("claim json");
+ let tags = serde_json::from_str::<Vec<Vec<String>>>(
+ &knowledge_claim_tags(&claim_json).expect("claim tags"),
+ )
+ .expect("tags");
+ let tags = tags
+ .into_iter()
+ .map(nostr::Tag::parse)
+ .collect::<Result<Vec<_>, _>>()
+ .expect("parsed tags");
+ let keys =
+ nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101")
+ .expect("keys");
+ let event =
+ nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json)
+ .tags(tags)
+ .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000))
+ .sign_with_keys(&keys)
+ .expect("signed event");
+ serde_json::to_string(&radroots_events::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(),
+ })
+ .expect("event json")
+ }
+
fn social_location() -> RadrootsSocialLocation {
RadrootsSocialLocation {
name: Some("field edge".to_string()),
@@ -989,6 +1384,98 @@ mod tests {
}
#[test]
+ fn knowledge_bindings_encode_to_json_when_input_is_valid() {
+ let article_tags = tags_json(wiki_article_tags(
+ &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"),
+ ));
+ assert!(has_tag(&article_tags, "title", "Soil health"));
+
+ let redirect_tags = tags_json(wiki_redirect_tags(
+ &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"),
+ ));
+ assert!(
+ redirect_tags
+ .iter()
+ .any(|tag| tag.first() == Some(&"a".to_string()))
+ );
+
+ assert_tags_json(wiki_merge_request_tags(
+ &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"),
+ ));
+
+ let source_tags = tags_json(knowledge_source_tags(
+ &serde_json::to_string(&sample_knowledge_source()).expect("source json"),
+ ));
+ assert!(has_tag(
+ &source_tags,
+ "contract",
+ RADROOTS_KNOWLEDGE_SOURCE_SCHEMA
+ ));
+
+ let claim_tags = tags_json(knowledge_claim_tags(
+ &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"),
+ ));
+ assert!(has_tag(
+ &claim_tags,
+ "contract",
+ RADROOTS_KNOWLEDGE_CLAIM_SCHEMA
+ ));
+
+ assert_tags_json(knowledge_relation_tags(
+ &serde_json::to_string(&sample_knowledge_relation()).expect("relation json"),
+ ));
+ assert_tags_json(knowledge_review_tags(
+ &serde_json::to_string(&sample_knowledge_review()).expect("review json"),
+ ));
+ assert_tags_json(knowledge_field_report_tags(
+ &serde_json::to_string(&sample_knowledge_field_report()).expect("field report json"),
+ ));
+ }
+
+ #[test]
+ fn knowledge_bindings_verify_decode_and_manifest_json() {
+ let decoded = verify_and_decode_event_json(&signed_claim_event_json()).expect("decoded");
+ let decoded: serde_json::Value = serde_json::from_str(&decoded).expect("decoded json");
+ assert_eq!(decoded["event_type"], "knowledge_claim");
+ assert_eq!(decoded["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA);
+ assert_eq!(
+ decoded["payload"]["text"],
+ "Cover crops improve soil structure."
+ );
+
+ let manifest = contract_manifest_json().expect("manifest");
+ let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json");
+ assert_eq!(manifest["schema_version"], 1);
+ assert_eq!(manifest["contract_count"], 11);
+ assert!(
+ manifest["contracts"]
+ .as_array()
+ .expect("contracts")
+ .iter()
+ .any(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA)
+ );
+ }
+
+ #[test]
+ fn knowledge_bindings_verify_decode_errors_are_coded_json() {
+ let invalid_json = verify_and_decode_event_json("not json").expect_err("invalid json");
+ let invalid_json: serde_json::Value =
+ serde_json::from_str(&invalid_json).expect("error json");
+ assert_eq!(invalid_json["code"], "invalid_json");
+ assert_eq!(invalid_json["inner_code"], "event_json");
+
+ let mut event: serde_json::Value =
+ serde_json::from_str(&signed_claim_event_json()).expect("event json");
+ event["sig"] = serde_json::Value::String("0".repeat(128));
+ let signature_error =
+ verify_and_decode_event_json(&event.to_string()).expect_err("signature error");
+ let signature_error: serde_json::Value =
+ serde_json::from_str(&signature_error).expect("signature error json");
+ assert_eq!(signature_error["code"], "nip01_verification");
+ assert_eq!(signature_error["inner_code"], "signature_invalid");
+ }
+
+ #[test]
fn field_bindings_encode_to_json_when_input_is_valid() {
let workspace_json =
serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
diff --git a/packages/events-bindings/src/generated/kinds.ts b/packages/events-bindings/src/generated/kinds.ts
@@ -20,6 +20,12 @@ export const KIND_COMMENT = 1111;
export const KIND_GEOCHAT = 20000;
+export const KIND_WIKI_MERGE_REQUEST = 818;
+
+export const KIND_WIKI_ARTICLE = 30818;
+
+export const KIND_WIKI_REDIRECT = 30819;
+
export const KIND_LIST_MUTE = 10000;
export const KIND_LIST_PINNED_NOTES = 10001;
@@ -138,6 +144,22 @@ export const KIND_TRADE_RECEIPT = 3434;
export const KIND_TRADE_VALIDATION_RECEIPT = 3440;
+export const KIND_KNOWLEDGE_CLAIM = 3460;
+
+export const KIND_KNOWLEDGE_RELATION = 3461;
+
+export const KIND_KNOWLEDGE_REVIEW = 3462;
+
+export const KIND_KNOWLEDGE_FIELD_REPORT = 3463;
+
+export const KIND_KNOWLEDGE_CHANGE_PROPOSAL = 3464;
+
+export const KIND_CONTRIBUTION_ATTESTATION = 3465;
+
+export const KIND_KNOWLEDGE_SOURCE = 30450;
+
+export const KIND_EVIDENCE_BOUNTY = 30451;
+
export const KIND_JOB_REQUEST_MIN = 5000;
export const KIND_JOB_REQUEST_MAX = 5999;
diff --git a/packages/events-bindings/src/generated/types.ts b/packages/events-bindings/src/generated/types.ts
@@ -10,12 +10,16 @@ export type JobPaymentRequest = { amount_sat: number, bolt11?: string | null, };
export type RadrootsAccountClaim = { username: string, pubkey: string, nip05?: string | null, };
+export type RadrootsAddressableRef = { kind: number, pubkey: string, d_tag: string, relays: Array<string>, };
+
export type RadrootsAppData = { d_tag: string, content: string, };
export type RadrootsComment = { root: RadrootsSocialTarget, parent: RadrootsSocialTarget, content: string, };
export type RadrootsCommercialDomain = "trade:listing";
+export type RadrootsContributionAttestation = { schema: string, schema_version: number, contributor_pubkey: string, contribution_type: string, subject_refs: Array<RadrootsNostrEventRef>, summary: string, evidence_refs: Array<RadrootsNostrEventRef>, };
+
export type RadrootsCoop = { d_tag: string, name: string, about?: string | null, website?: string | null, picture?: string | null, banner?: string | null, location?: RadrootsCoopLocation | null, tags?: Array<string> | null, };
export type RadrootsCoopLocation = { primary?: string | null, city?: string | null, region?: string | null, country?: string | null, gcs: RadrootsGcsLocation, };
@@ -26,6 +30,8 @@ export type RadrootsDocument = { d_tag: string, doc_type: string, title: string,
export type RadrootsDocumentSubject = { pubkey: string, address?: string | null, };
+export type RadrootsEvidenceBounty = { schema: string, schema_version: number, d_tag: string, title: string, summary?: string | null, topics: Array<string>, target_refs: Array<RadrootsNostrEventRef>, reward_note?: string | null, closes_at?: string | null, };
+
export type RadrootsFarm = { d_tag: string, name: string, about?: string | null, website?: string | null, picture?: string | null, banner?: string | null, location?: RadrootsFarmPublicLocation | null, tags?: Array<string> | null, };
export type RadrootsFarmPublicLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, geohash: string, };
@@ -58,6 +64,38 @@ export type RadrootsJobRequest = { kind: number, inputs: Array<RadrootsJobInput>
export type RadrootsJobResult = { kind: number, request_event: RadrootsNostrEventPtr, request_json?: string | null, inputs: Array<RadrootsJobInput>, customer_pubkey?: string | null, payment?: JobPaymentRequest | null, content?: string | null, encrypted: boolean, };
+export type RadrootsKnowledgeChangeProposal = { schema: string, schema_version: number, target: RadrootsNostrEventRef, proposal_type: string, summary: string, rationale?: string | null, evidence_refs: Array<RadrootsNostrEventRef>, supersedes: Array<RadrootsNostrEventRef>, };
+
+export type RadrootsKnowledgeCitationSpan = { source_ref: RadrootsNostrEventRef, artifact_ref?: RadrootsNostrEventRef | null, page_start?: number | null, page_end?: number | null, section_path: Array<string>, quote_hash?: string | null, chunk_id?: string | null, };
+
+export type RadrootsKnowledgeClaim = { schema: string, schema_version: number, claim_type: string, text: string, citation_spans: Array<RadrootsKnowledgeCitationSpan>, topics: Array<string>, applies_to: Array<string>, author_asserted_confidence?: string | null, supersedes: Array<RadrootsNostrEventRef>, };
+
+export type RadrootsKnowledgeFieldContext = { location_precision: RadrootsKnowledgeLocationPrecision, public_location?: RadrootsKnowledgeLocation | null, private_location_ref?: RadrootsNostrEventRef | null, topics: Array<string>, context_tags: Array<string>, };
+
+export type RadrootsKnowledgeFieldReport = { schema: string, schema_version: number, report_type: string, title: string, summary?: string | null, context: RadrootsKnowledgeFieldContext, observations: Array<RadrootsKnowledgeObservation>, artifact_refs: Array<RadrootsNostrEventRef>, related_refs: Array<RadrootsNostrEventRef>, limitations: Array<string>, };
+
+export type RadrootsKnowledgeLocation = { label?: string | null, region?: string | null, locality?: string | null, geohash?: string | null, };
+
+export type RadrootsKnowledgeLocationPrecision = "none" | "region" | "locality" | "coarse_geohash" | "exact_public" | "exact_private_reference";
+
+export type RadrootsKnowledgeNodeRef = { node_type: string, event_ref?: RadrootsNostrEventRef | null, address_ref?: RadrootsAddressableRef | null, external_id?: string | null, label?: string | null, };
+
+export type RadrootsKnowledgeObservation = { observation_type: string, text: string, observed_at?: string | null, values: Array<RadrootsKnowledgeObservationValue>, };
+
+export type RadrootsKnowledgeObservationValue = { key: string, value: string, unit?: string | null, };
+
+export type RadrootsKnowledgeRelation = { schema: string, schema_version: number, subject: RadrootsKnowledgeNodeRef, predicate: string, object: RadrootsKnowledgeNodeRef, support_refs: Array<RadrootsNostrEventRef>, author_asserted_confidence?: string | null, supersedes: Array<RadrootsNostrEventRef>, };
+
+export type RadrootsKnowledgeReview = { schema: string, schema_version: number, target: RadrootsKnowledgeReviewTarget, reviewer_role: string, verdict: string, scores: Array<RadrootsKnowledgeReviewScore>, notes?: string | null, evidence_refs: Array<RadrootsNostrEventRef>, };
+
+export type RadrootsKnowledgeReviewScope = "specific_version" | "addressable_coordinate_at_published_at" | "policy_latest";
+
+export type RadrootsKnowledgeReviewScore = { dimension: string, value: string, note?: string | null, };
+
+export type RadrootsKnowledgeReviewTarget = { event_id: string, author_pubkey: string, kind: number, address?: string | null, relays: Array<string>, review_scope: RadrootsKnowledgeReviewScope, };
+
+export type RadrootsKnowledgeSource = { schema: string, schema_version: number, d_tag: string, title: string, source_type: string, authors: Array<string>, publisher?: string | null, publication_year?: number | null, edition?: string | null, canonical_url?: string | null, artifact_refs: Array<RadrootsNostrEventRef>, author_asserted_rights?: RadrootsRightsAssertion | null, topics: Array<string>, summary?: string | null, };
+
export type RadrootsList = { content: string, entries: Array<RadrootsListEntry>, };
export type RadrootsListEntry = { tag: string, values: Array<string>, };
@@ -162,6 +200,8 @@ export type RadrootsResourceHarvestCap = { d_tag: string, resource_area: Radroot
export type RadrootsResourceHarvestProduct = { key: string, category?: string | null, };
+export type RadrootsRightsAssertion = { assertion: string, holder?: string | null, license?: string | null, url?: string | null, };
+
export type RadrootsSeal = { content: string, };
export type RadrootsSocialFarmAnchor = { farm: RadrootsFarmRef, relays?: Array<string> | null, };
@@ -181,3 +221,9 @@ export type RadrootsTradeListingValidateRequest = { listing_event?: RadrootsNost
export type RadrootsTradeListingValidateResult = { valid: boolean, errors: Array<RadrootsTradeValidationListingError>, };
export type RadrootsTradeValidationListingError = { kind: "invalid_kind", amount: { kind: number, }, } | { kind: "missing_listing_id", } | { kind: "listing_event_not_found", amount: { listing_addr: string, }, } | { kind: "listing_event_fetch_failed", amount: { listing_addr: string, }, } | { kind: "parse_error", amount: { error: RadrootsListingParseError, }, } | { kind: "invalid_seller", } | { kind: "missing_farm_profile", } | { kind: "missing_farm_record", } | { kind: "missing_title", } | { kind: "missing_description", } | { kind: "missing_product_type", } | { kind: "missing_bins", } | { kind: "missing_primary_bin", } | { kind: "invalid_bin", } | { kind: "missing_price", } | { kind: "invalid_price", } | { kind: "missing_inventory", } | { kind: "invalid_inventory", } | { kind: "missing_availability", } | { kind: "missing_location", } | { kind: "missing_location_locality", } | { kind: "missing_location_geohash", } | { kind: "invalid_location_geohash", } | { kind: "missing_delivery_method", };
+
+export type RadrootsWikiArticle = { d_tag: string, title: string, content_djot: string, summary?: string | null, topics: Array<string>, references: Array<RadrootsNostrEventRef>, forked_from: Array<RadrootsNostrEventRef>, deferred_to?: RadrootsNostrEventRef | null, };
+
+export type RadrootsWikiMergeRequest = { target_article: RadrootsAddressableRef, destination_pubkey: string, base_version_event_id?: string | null, source_version_event_id: string, explanation?: string | null, };
+
+export type RadrootsWikiRedirect = { d_tag: string, target: RadrootsNostrEventRef, };
diff --git a/tools/xtask/src/dto_roots.rs b/tools/xtask/src/dto_roots.rs
@@ -590,14 +590,17 @@ mod tests {
"JobInputType",
"JobPaymentRequest",
"RadrootsAccountClaim",
+ "RadrootsAddressableRef",
"RadrootsAppData",
"RadrootsComment",
"RadrootsCommercialDomain",
+ "RadrootsContributionAttestation",
"RadrootsCoop",
"RadrootsCoopLocation",
"RadrootsCoopRef",
"RadrootsDocument",
"RadrootsDocumentSubject",
+ "RadrootsEvidenceBounty",
"RadrootsFarm",
"RadrootsFarmPublicLocation",
"RadrootsFarmRef",
@@ -614,6 +617,22 @@ mod tests {
"RadrootsJobParam",
"RadrootsJobRequest",
"RadrootsJobResult",
+ "RadrootsKnowledgeChangeProposal",
+ "RadrootsKnowledgeCitationSpan",
+ "RadrootsKnowledgeClaim",
+ "RadrootsKnowledgeFieldContext",
+ "RadrootsKnowledgeFieldReport",
+ "RadrootsKnowledgeLocation",
+ "RadrootsKnowledgeLocationPrecision",
+ "RadrootsKnowledgeNodeRef",
+ "RadrootsKnowledgeObservation",
+ "RadrootsKnowledgeObservationValue",
+ "RadrootsKnowledgeRelation",
+ "RadrootsKnowledgeReview",
+ "RadrootsKnowledgeReviewScope",
+ "RadrootsKnowledgeReviewScore",
+ "RadrootsKnowledgeReviewTarget",
+ "RadrootsKnowledgeSource",
"RadrootsList",
"RadrootsListEntry",
"RadrootsListSet",
@@ -666,6 +685,7 @@ mod tests {
"RadrootsResourceAreaRef",
"RadrootsResourceHarvestCap",
"RadrootsResourceHarvestProduct",
+ "RadrootsRightsAssertion",
"RadrootsSeal",
"RadrootsSocialFarmAnchor",
"RadrootsSocialLocation",
@@ -676,6 +696,9 @@ mod tests {
"RadrootsTradeListingValidateRequest",
"RadrootsTradeListingValidateResult",
"RadrootsTradeValidationListingError",
+ "RadrootsWikiArticle",
+ "RadrootsWikiMergeRequest",
+ "RadrootsWikiRedirect",
];
const EVENTS_INDEXED_TYPE_INVENTORY: &[&str] = &[
"RadrootsEventsIndexedShardId",
diff --git a/tools/xtask/src/wasm_declarations.rs b/tools/xtask/src/wasm_declarations.rs
@@ -393,6 +393,23 @@ const EVENTS_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
public_function!("job_feedback_tags", Some("job_json"), String, 2, 4),
public_function!("job_request_tags", Some("job_json"), String, 2, 4),
public_function!("job_result_tags", Some("job_json"), String, 2, 4),
+ public_function!("knowledge_claim_tags", Some("claim_json"), String, 2, 4),
+ public_function!(
+ "knowledge_field_report_tags",
+ Some("report_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "knowledge_relation_tags",
+ Some("relation_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!("knowledge_review_tags", Some("review_json"), String, 2, 4),
+ public_function!("knowledge_source_tags", Some("source_json"), String, 2, 4),
public_function!("list_set_tags", Some("list_json"), String, 2, 4),
public_function!("list_tags", Some("list_json"), String, 2, 4),
public_function!("listing_tags", Some("listing_json"), String, 2, 4),
@@ -406,6 +423,23 @@ const EVENTS_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
public_function!("report_tags", Some("report_json"), String, 2, 4),
public_function!("repost_tags", Some("repost_json"), String, 2, 4),
public_function!("seal_tags", Some("seal_json"), String, 2, 4),
+ public_function!("wiki_article_tags", Some("article_json"), String, 2, 4),
+ public_function!(
+ "wiki_merge_request_tags",
+ Some("request_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!("wiki_redirect_tags", Some("redirect_json"), String, 2, 4),
+ public_function!(
+ "verify_and_decode_event_json",
+ Some("event_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!("contract_manifest_json", None, String, 0, 4),
];
const EVENTS_CODEC_SUPPORT_EXPORTS: &[WasmSupportExport] = &[