knowledge.rs (19487B)
1 use radroots_event::envelope::kind::{ 2 KIND_CONTRIBUTION_ATTESTATION, KIND_EVIDENCE_BOUNTY, KIND_FILE_METADATA, 3 KIND_KNOWLEDGE_CHANGE_PROPOSAL, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_FIELD_REPORT, 4 KIND_KNOWLEDGE_RELATION, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, 5 KIND_WIKI_MERGE_REQUEST, KIND_WIKI_REDIRECT, 6 }; 7 use radroots_event::knowledge::{ 8 AddressableRef, ContributionAttestation, EvidenceBounty, KnowledgeChangeProposal, 9 KnowledgeCitationSpan, KnowledgeClaim, KnowledgeFieldContext, KnowledgeFieldReport, 10 KnowledgeLocation, KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation, 11 KnowledgeObservationValue, KnowledgeRelation, KnowledgeReview, KnowledgeReviewScope, 12 KnowledgeReviewScore, KnowledgeReviewTarget, KnowledgeSource, 13 RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, RADROOTS_EVIDENCE_BOUNTY_SCHEMA, 14 RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 15 RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA, 16 RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 17 RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, WikiArticle, WikiArticleVersionRef, WikiMergeRequest, 18 WikiRedirect, 19 }; 20 use radroots_event::tag::EventRef; 21 use radroots_identity::PublicKey; 22 23 use super::{FIXTURE_ALICE_PUBLIC_KEY_HEX, RELAY_PRIMARY_WSS}; 24 25 pub const RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE: &str = "radroots-knowledge-fixture-v1"; 26 27 pub const RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS: [&str; 11] = [ 28 "radroots.wiki.article.v1", 29 "radroots.wiki.redirect.v1", 30 "radroots.wiki.merge_request.v1", 31 RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, 32 RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 33 RADROOTS_KNOWLEDGE_RELATION_SCHEMA, 34 RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, 35 RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, 36 RADROOTS_EVIDENCE_BOUNTY_SCHEMA, 37 RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, 38 RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, 39 ]; 40 41 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 42 pub struct RadrootsKnowledgeAdversarialFixture { 43 pub id: &'static str, 44 pub pipeline_stage: &'static str, 45 pub expected_error_code: &'static str, 46 } 47 48 pub const RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES: [RadrootsKnowledgeAdversarialFixture; 13] = [ 49 RadrootsKnowledgeAdversarialFixture { 50 id: "malformed_tags", 51 pipeline_stage: "contract_validation", 52 expected_error_code: "tag_cardinality_mismatch", 53 }, 54 RadrootsKnowledgeAdversarialFixture { 55 id: "wrong_schema", 56 pipeline_stage: "contract_validation", 57 expected_error_code: "content_field_mismatch", 58 }, 59 RadrootsKnowledgeAdversarialFixture { 60 id: "missing_contract_id", 61 pipeline_stage: "contract_validation", 62 expected_error_code: "contract_match", 63 }, 64 RadrootsKnowledgeAdversarialFixture { 65 id: "private_coordinate_leakage", 66 pipeline_stage: "event_parse", 67 expected_error_code: "invalid_json", 68 }, 69 RadrootsKnowledgeAdversarialFixture { 70 id: "unsupported_contract_shape", 71 pipeline_stage: "contract_validation", 72 expected_error_code: "contract_match", 73 }, 74 RadrootsKnowledgeAdversarialFixture { 75 id: "invalid_nip54_d_tag", 76 pipeline_stage: "encode", 77 expected_error_code: "invalid_field", 78 }, 79 RadrootsKnowledgeAdversarialFixture { 80 id: "invalid_redirect_target_kind", 81 pipeline_stage: "encode", 82 expected_error_code: "invalid_field", 83 }, 84 RadrootsKnowledgeAdversarialFixture { 85 id: "merge_request_missing_source_marker", 86 pipeline_stage: "event_parse", 87 expected_error_code: "invalid_tag", 88 }, 89 RadrootsKnowledgeAdversarialFixture { 90 id: "merge_request_json_content_guard", 91 pipeline_stage: "wire_shape", 92 expected_error_code: "plain_text_content", 93 }, 94 RadrootsKnowledgeAdversarialFixture { 95 id: "orphan_fork_marker", 96 pipeline_stage: "event_parse", 97 expected_error_code: "invalid_tag", 98 }, 99 RadrootsKnowledgeAdversarialFixture { 100 id: "orphan_defer_marker", 101 pipeline_stage: "event_parse", 102 expected_error_code: "invalid_tag", 103 }, 104 RadrootsKnowledgeAdversarialFixture { 105 id: "id_mismatch", 106 pipeline_stage: "nip01_verification", 107 expected_error_code: "id_mismatch", 108 }, 109 RadrootsKnowledgeAdversarialFixture { 110 id: "signature_invalidity", 111 pipeline_stage: "nip01_verification", 112 expected_error_code: "signature_invalid", 113 }, 114 ]; 115 116 #[derive(Clone, Debug, PartialEq, Eq)] 117 pub enum RadrootsKnowledgeFixture { 118 WikiArticle(WikiArticle), 119 WikiRedirect(WikiRedirect), 120 WikiMergeRequest(WikiMergeRequest), 121 KnowledgeSource(KnowledgeSource), 122 KnowledgeClaim(KnowledgeClaim), 123 KnowledgeRelation(KnowledgeRelation), 124 KnowledgeReview(KnowledgeReview), 125 KnowledgeFieldReport(KnowledgeFieldReport), 126 EvidenceBounty(EvidenceBounty), 127 KnowledgeChangeProposal(KnowledgeChangeProposal), 128 ContributionAttestation(ContributionAttestation), 129 } 130 131 #[derive(Clone, Debug, PartialEq, Eq)] 132 pub struct RadrootsKnowledgeFixtureCase { 133 pub id: &'static str, 134 pub contract_id: &'static str, 135 pub kind: u32, 136 pub data: RadrootsKnowledgeFixture, 137 } 138 139 pub fn hex_64(character: char) -> String { 140 core::iter::repeat_n(character, 64).collect() 141 } 142 143 pub fn event_ref(character: char, kind: u32) -> EventRef { 144 EventRef { 145 id: hex_64(character), 146 author: PublicKey::from_hex(FIXTURE_ALICE_PUBLIC_KEY_HEX) 147 .expect("fixture public key is valid"), 148 kind, 149 d_tag: None, 150 relays: Some(vec![RELAY_PRIMARY_WSS.to_string()]), 151 } 152 } 153 154 pub fn address_ref() -> AddressableRef { 155 AddressableRef { 156 kind: KIND_WIKI_ARTICLE, 157 pubkey: hex_64('a'), 158 d_tag: "soil-health".to_string(), 159 relays: vec![RELAY_PRIMARY_WSS.to_string()], 160 } 161 } 162 163 pub fn deferred_address_ref() -> AddressableRef { 164 AddressableRef { 165 kind: KIND_WIKI_ARTICLE, 166 pubkey: hex_64('a'), 167 d_tag: "soil-health-v2".to_string(), 168 relays: vec![RELAY_PRIMARY_WSS.to_string()], 169 } 170 } 171 172 pub fn wiki_article_version_ref() -> WikiArticleVersionRef { 173 WikiArticleVersionRef { 174 event_id: hex_64('b'), 175 address_ref: address_ref(), 176 } 177 } 178 179 pub fn wiki_article_deferred_version_ref() -> WikiArticleVersionRef { 180 WikiArticleVersionRef { 181 event_id: hex_64('c'), 182 address_ref: deferred_address_ref(), 183 } 184 } 185 186 pub fn wiki_article() -> WikiArticle { 187 WikiArticle { 188 d_tag: "soil-health".to_string(), 189 title: Some("Soil health".to_string()), 190 content_djot: "# Soil health\n\nLiving soil supports resilient local food systems." 191 .to_string(), 192 summary: Some("Living soil basics".to_string()), 193 topics: vec!["soil".to_string(), "local-food".to_string()], 194 references: vec![event_ref('2', KIND_KNOWLEDGE_SOURCE)], 195 forked_from: vec![wiki_article_version_ref()], 196 deferred_to: Some(wiki_article_deferred_version_ref()), 197 } 198 } 199 200 pub fn wiki_redirect() -> WikiRedirect { 201 WikiRedirect { 202 d_tag: "soil".to_string(), 203 target: address_ref(), 204 } 205 } 206 207 pub fn wiki_merge_request() -> WikiMergeRequest { 208 WikiMergeRequest { 209 target_article: address_ref(), 210 destination_pubkey: hex_64('a'), 211 base_version_event_id: Some(hex_64('e')), 212 source_version_event_id: hex_64('f'), 213 explanation: Some("Merge synthetic soil article updates".to_string()), 214 } 215 } 216 217 pub fn wiki_merge_request_without_base_version() -> WikiMergeRequest { 218 WikiMergeRequest { 219 base_version_event_id: None, 220 ..wiki_merge_request() 221 } 222 } 223 224 pub fn knowledge_source() -> KnowledgeSource { 225 KnowledgeSource { 226 schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), 227 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 228 d_tag: "soil-source".to_string(), 229 title: "Soil Source".to_string(), 230 source_type: "book".to_string(), 231 authors: vec!["A. Example".to_string()], 232 publisher: Some("Radroots Synthetic Press".to_string()), 233 publication_year: Some(2026), 234 edition: None, 235 canonical_url: Some("https://source.example.test/soil-source".to_string()), 236 artifact_refs: vec![event_ref('3', KIND_FILE_METADATA)], 237 author_asserted_rights: None, 238 topics: vec!["soil".to_string()], 239 summary: Some("Synthetic source for knowledge fixture coverage".to_string()), 240 } 241 } 242 243 pub fn knowledge_claim() -> KnowledgeClaim { 244 KnowledgeClaim { 245 schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), 246 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 247 claim_type: "practice_effect".to_string(), 248 text: "Cover crops improve soil structure.".to_string(), 249 citation_spans: vec![KnowledgeCitationSpan { 250 source_ref: event_ref('4', KIND_KNOWLEDGE_SOURCE), 251 artifact_ref: None, 252 page_start: Some(12), 253 page_end: Some(13), 254 section_path: vec!["chapter-1".to_string()], 255 quote_hash: Some(hex_64('5')), 256 chunk_id: Some("chunk-1".to_string()), 257 }], 258 topics: vec!["cover-crops".to_string()], 259 applies_to: vec!["local-food".to_string()], 260 author_asserted_confidence: Some("medium".to_string()), 261 supersedes: Vec::new(), 262 } 263 } 264 265 pub fn knowledge_node_ref(label: &str) -> KnowledgeNodeRef { 266 KnowledgeNodeRef { 267 node_type: "event".to_string(), 268 event_ref: Some(event_ref('6', KIND_KNOWLEDGE_CLAIM)), 269 address_ref: None, 270 external_id: None, 271 label: Some(label.to_string()), 272 } 273 } 274 275 pub fn knowledge_relation() -> KnowledgeRelation { 276 KnowledgeRelation { 277 schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), 278 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 279 subject: knowledge_node_ref("cover crops"), 280 predicate: "supports".to_string(), 281 object: knowledge_node_ref("soil structure"), 282 support_refs: vec![event_ref('7', KIND_KNOWLEDGE_CLAIM)], 283 author_asserted_confidence: Some("medium".to_string()), 284 supersedes: Vec::new(), 285 } 286 } 287 288 pub fn knowledge_review() -> KnowledgeReview { 289 KnowledgeReview { 290 schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), 291 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 292 target: KnowledgeReviewTarget { 293 event_id: hex_64('8'), 294 author_pubkey: hex_64('a'), 295 kind: KIND_KNOWLEDGE_CLAIM, 296 address: None, 297 relays: vec![RELAY_PRIMARY_WSS.to_string()], 298 review_scope: KnowledgeReviewScope::SpecificVersion, 299 }, 300 reviewer_role: "peer".to_string(), 301 verdict: "needs_more_evidence".to_string(), 302 scores: vec![KnowledgeReviewScore { 303 dimension: "evidence".to_string(), 304 value: "partial".to_string(), 305 note: None, 306 }], 307 notes: Some("Synthetic review".to_string()), 308 evidence_refs: vec![event_ref('9', KIND_KNOWLEDGE_SOURCE)], 309 } 310 } 311 312 pub fn knowledge_field_report() -> KnowledgeFieldReport { 313 KnowledgeFieldReport { 314 schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), 315 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 316 report_type: "observation".to_string(), 317 title: "Field observation".to_string(), 318 summary: Some("Observed cover crop residue.".to_string()), 319 context: KnowledgeFieldContext { 320 location_precision: KnowledgeLocationPrecision::CoarseGeohash, 321 public_location: Some(KnowledgeLocation { 322 label: Some("watershed".to_string()), 323 region: Some("synthetic-region".to_string()), 324 locality: None, 325 geohash: Some("c23".to_string()), 326 }), 327 private_location_ref: None, 328 topics: vec!["field".to_string()], 329 context_tags: vec!["observation".to_string()], 330 }, 331 observations: vec![KnowledgeObservation { 332 observation_type: "residue".to_string(), 333 text: "Residue was visible across beds.".to_string(), 334 observed_at: Some("2026-07-05".to_string()), 335 values: vec![KnowledgeObservationValue { 336 key: "coverage".to_string(), 337 value: "medium".to_string(), 338 unit: None, 339 }], 340 }], 341 artifact_refs: vec![event_ref('c', KIND_FILE_METADATA)], 342 related_refs: vec![event_ref('d', KIND_KNOWLEDGE_CLAIM)], 343 limitations: vec!["single observer".to_string()], 344 } 345 } 346 347 pub fn evidence_bounty() -> EvidenceBounty { 348 EvidenceBounty { 349 schema: RADROOTS_EVIDENCE_BOUNTY_SCHEMA.to_string(), 350 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 351 d_tag: "soil-bounty".to_string(), 352 title: "Soil bounty".to_string(), 353 summary: None, 354 topics: vec!["soil".to_string()], 355 target_refs: vec![event_ref('a', KIND_KNOWLEDGE_CLAIM)], 356 reward_note: None, 357 closes_at: None, 358 } 359 } 360 361 pub fn knowledge_change_proposal() -> KnowledgeChangeProposal { 362 KnowledgeChangeProposal { 363 schema: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA.to_string(), 364 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 365 target: event_ref('b', KIND_KNOWLEDGE_CLAIM), 366 proposal_type: "amend".to_string(), 367 summary: "Clarify scope".to_string(), 368 rationale: None, 369 evidence_refs: vec![event_ref('c', KIND_KNOWLEDGE_SOURCE)], 370 supersedes: Vec::new(), 371 } 372 } 373 374 pub fn contribution_attestation() -> ContributionAttestation { 375 ContributionAttestation { 376 schema: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA.to_string(), 377 schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, 378 contributor_pubkey: hex_64('a'), 379 contribution_type: "review".to_string(), 380 subject_refs: vec![event_ref('d', KIND_KNOWLEDGE_REVIEW)], 381 summary: "Reviewed synthetic claim".to_string(), 382 evidence_refs: vec![event_ref('e', KIND_KNOWLEDGE_REVIEW)], 383 } 384 } 385 386 pub fn knowledge_valid_fixtures() -> Vec<RadrootsKnowledgeFixtureCase> { 387 vec![ 388 RadrootsKnowledgeFixtureCase { 389 id: "wiki_article_valid", 390 contract_id: "radroots.wiki.article.v1", 391 kind: KIND_WIKI_ARTICLE, 392 data: RadrootsKnowledgeFixture::WikiArticle(wiki_article()), 393 }, 394 RadrootsKnowledgeFixtureCase { 395 id: "wiki_redirect_valid", 396 contract_id: "radroots.wiki.redirect.v1", 397 kind: KIND_WIKI_REDIRECT, 398 data: RadrootsKnowledgeFixture::WikiRedirect(wiki_redirect()), 399 }, 400 RadrootsKnowledgeFixtureCase { 401 id: "wiki_merge_request_valid", 402 contract_id: "radroots.wiki.merge_request.v1", 403 kind: KIND_WIKI_MERGE_REQUEST, 404 data: RadrootsKnowledgeFixture::WikiMergeRequest(wiki_merge_request()), 405 }, 406 RadrootsKnowledgeFixtureCase { 407 id: "wiki_merge_request_without_base_valid", 408 contract_id: "radroots.wiki.merge_request.v1", 409 kind: KIND_WIKI_MERGE_REQUEST, 410 data: RadrootsKnowledgeFixture::WikiMergeRequest( 411 wiki_merge_request_without_base_version(), 412 ), 413 }, 414 RadrootsKnowledgeFixtureCase { 415 id: "knowledge_source_valid", 416 contract_id: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, 417 kind: KIND_KNOWLEDGE_SOURCE, 418 data: RadrootsKnowledgeFixture::KnowledgeSource(knowledge_source()), 419 }, 420 RadrootsKnowledgeFixtureCase { 421 id: "knowledge_claim_valid", 422 contract_id: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, 423 kind: KIND_KNOWLEDGE_CLAIM, 424 data: RadrootsKnowledgeFixture::KnowledgeClaim(knowledge_claim()), 425 }, 426 RadrootsKnowledgeFixtureCase { 427 id: "knowledge_relation_valid", 428 contract_id: RADROOTS_KNOWLEDGE_RELATION_SCHEMA, 429 kind: KIND_KNOWLEDGE_RELATION, 430 data: RadrootsKnowledgeFixture::KnowledgeRelation(knowledge_relation()), 431 }, 432 RadrootsKnowledgeFixtureCase { 433 id: "knowledge_review_valid", 434 contract_id: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, 435 kind: KIND_KNOWLEDGE_REVIEW, 436 data: RadrootsKnowledgeFixture::KnowledgeReview(knowledge_review()), 437 }, 438 RadrootsKnowledgeFixtureCase { 439 id: "knowledge_field_report_valid", 440 contract_id: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, 441 kind: KIND_KNOWLEDGE_FIELD_REPORT, 442 data: RadrootsKnowledgeFixture::KnowledgeFieldReport(knowledge_field_report()), 443 }, 444 RadrootsKnowledgeFixtureCase { 445 id: "evidence_bounty_valid", 446 contract_id: RADROOTS_EVIDENCE_BOUNTY_SCHEMA, 447 kind: KIND_EVIDENCE_BOUNTY, 448 data: RadrootsKnowledgeFixture::EvidenceBounty(evidence_bounty()), 449 }, 450 RadrootsKnowledgeFixtureCase { 451 id: "knowledge_change_proposal_valid", 452 contract_id: RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, 453 kind: KIND_KNOWLEDGE_CHANGE_PROPOSAL, 454 data: RadrootsKnowledgeFixture::KnowledgeChangeProposal(knowledge_change_proposal()), 455 }, 456 RadrootsKnowledgeFixtureCase { 457 id: "contribution_attestation_valid", 458 contract_id: RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, 459 kind: KIND_CONTRIBUTION_ATTESTATION, 460 data: RadrootsKnowledgeFixture::ContributionAttestation(contribution_attestation()), 461 }, 462 ] 463 } 464 465 #[cfg(test)] 466 #[cfg_attr(coverage_nightly, coverage(off))] 467 mod tests { 468 use super::{ 469 RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES, RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE, 470 RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS, knowledge_valid_fixtures, 471 }; 472 473 #[test] 474 fn valid_fixture_catalog_covers_all_contract_ids() { 475 let fixtures = knowledge_valid_fixtures(); 476 assert!(fixtures.len() >= RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS.len()); 477 for contract_id in RADROOTS_KNOWLEDGE_VALID_CONTRACT_IDS { 478 assert!( 479 fixtures 480 .iter() 481 .any(|fixture| fixture.contract_id == contract_id), 482 "{contract_id}" 483 ); 484 } 485 assert_eq!( 486 RADROOTS_KNOWLEDGE_FIXTURE_NAMESPACE, 487 "radroots-knowledge-fixture-v1" 488 ); 489 } 490 491 #[test] 492 fn adversarial_fixture_catalog_covers_required_cases() { 493 let expected = [ 494 "malformed_tags", 495 "wrong_schema", 496 "missing_contract_id", 497 "private_coordinate_leakage", 498 "unsupported_contract_shape", 499 "invalid_nip54_d_tag", 500 "invalid_redirect_target_kind", 501 "merge_request_missing_source_marker", 502 "merge_request_json_content_guard", 503 "orphan_fork_marker", 504 "orphan_defer_marker", 505 "id_mismatch", 506 "signature_invalidity", 507 ]; 508 assert_eq!( 509 RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES.len(), 510 expected.len() 511 ); 512 for id in expected { 513 assert!( 514 RADROOTS_KNOWLEDGE_ADVERSARIAL_FIXTURES 515 .iter() 516 .any(|fixture| fixture.id == id), 517 "{id}" 518 ); 519 } 520 } 521 }