commit 6519051e4f6cd9f52c16c5ee2427efdcd6a7dde6
parent 582a37b43b56f20e51f0366652b93a67fbfc451c
Author: triesap <tyson@radroots.org>
Date: Mon, 6 Jul 2026 01:43:32 +0000
events: make wiki article titles optional
Diffstat:
5 files changed, 48 insertions(+), 10 deletions(-)
diff --git a/crates/events/src/knowledge.rs b/crates/events/src/knowledge.rs
@@ -295,7 +295,9 @@ pub fn validate_wiki_article(
validate_wiki_d_tag(article.d_tag.as_str())
.map(|_| ())
.map_err(|_| RadrootsKnowledgeValidationError::InvalidField("d_tag"))?;
- require_non_empty(article.title.as_str(), "title")?;
+ if let Some(title) = article.title.as_deref() {
+ require_non_empty(title, "title")?;
+ }
require_non_empty(article.content_djot.as_str(), "content_djot")?;
validate_event_refs(&article.references, "references")?;
for version_ref in &article.forked_from {
@@ -550,7 +552,7 @@ pub struct RadrootsRightsAssertion {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsWikiArticle {
pub d_tag: String,
- pub title: String,
+ pub title: Option<String>,
pub content_djot: String,
pub summary: Option<String>,
pub topics: Vec<String>,
@@ -1138,7 +1140,7 @@ mod tests {
fn models_wiki_article_as_djot_payload() {
let article = RadrootsWikiArticle {
d_tag: "soil-health".to_string(),
- title: "Soil health".to_string(),
+ title: Some("Soil health".to_string()),
content_djot: "# Soil health".to_string(),
summary: Some("Living soil basics".to_string()),
topics: vec!["soil".to_string()],
@@ -1217,7 +1219,7 @@ mod tests {
fn knowledge_validators_accept_valid_models() {
let article = RadrootsWikiArticle {
d_tag: "soil-health".to_string(),
- title: "Soil health".to_string(),
+ title: Some("Soil health".to_string()),
content_djot: "# Soil health".to_string(),
summary: None,
topics: Vec::new(),
@@ -1260,6 +1262,27 @@ mod tests {
}
#[test]
+ fn wiki_article_title_is_optional_but_not_blank() {
+ let mut article = RadrootsWikiArticle {
+ d_tag: "soil-health".to_string(),
+ title: None,
+ content_djot: "# Soil health".to_string(),
+ summary: None,
+ topics: Vec::new(),
+ references: Vec::new(),
+ forked_from: Vec::new(),
+ deferred_to: None,
+ };
+ assert_eq!(validate_wiki_article(&article), Ok(()));
+
+ article.title = Some(" ".to_string());
+ assert_validation_error(
+ validate_wiki_article(&article),
+ RadrootsKnowledgeValidationError::EmptyField("title"),
+ );
+ }
+
+ #[test]
fn knowledge_claims_require_citations_except_exact_uncited_types() {
let mut claim = knowledge_claim();
claim.citation_spans.clear();
@@ -1293,7 +1316,7 @@ mod tests {
fn knowledge_validators_reject_representative_invalid_models() {
let mut article = RadrootsWikiArticle {
d_tag: "soil-health".to_string(),
- title: "Soil health".to_string(),
+ title: Some("Soil health".to_string()),
content_djot: " ".to_string(),
summary: None,
topics: Vec::new(),
diff --git a/crates/events_codec/src/knowledge/decode.rs b/crates/events_codec/src/knowledge/decode.rs
@@ -317,7 +317,7 @@ pub fn wiki_article_from_event(
ensure_kind(event.kind, KIND_WIKI_ARTICLE, "wiki article")?;
let d_tag = required_one_value(&event.tags, TAG_D)?;
validate_wiki_d_tag(&d_tag).map_err(|_| EventParseError::InvalidTag(TAG_D))?;
- let title = required_one_value(&event.tags, TAG_TITLE)?;
+ let title = optional_one_value(&event.tags, TAG_TITLE)?;
let summary = optional_one_value(&event.tags, TAG_SUMMARY)?;
let topics = values(&event.tags, TAG_T);
let references = event_refs(&event.tags, TAG_SOURCE)?;
diff --git a/crates/events_codec/src/knowledge/encode.rs b/crates/events_codec/src/knowledge/encode.rs
@@ -160,7 +160,7 @@ pub fn wiki_article_build_tags(
validate_wiki_article(article).map_err(encode_validation_error)?;
let mut tags = Vec::new();
tags.push(vec![TAG_D.to_string(), article.d_tag.clone()]);
- push_value(&mut tags, TAG_TITLE, &article.title);
+ push_optional_value(&mut tags, TAG_TITLE, article.title.as_deref());
push_optional_value(&mut tags, TAG_SUMMARY, article.summary.as_deref());
push_topics(&mut tags, &article.topics);
push_event_refs(&mut tags, TAG_SOURCE, &article.references);
diff --git a/crates/events_codec/tests/knowledge.rs b/crates/events_codec/tests/knowledge.rs
@@ -147,7 +147,7 @@ fn sign_parts(parts: WireEventParts) -> RadrootsNostrEvent {
fn wiki_article() -> RadrootsWikiArticle {
RadrootsWikiArticle {
d_tag: "soil-health".to_string(),
- title: "Soil health".to_string(),
+ title: Some("Soil health".to_string()),
content_djot: "# Soil health".to_string(),
summary: Some("Living soil basics".to_string()),
topics: vec!["soil".to_string(), "health".to_string()],
@@ -318,7 +318,7 @@ fn knowledge_codecs_roundtrip_all_contracts() {
.data
.data
.title,
- "Soil health"
+ Some("Soil health".to_string())
);
let redirect = RadrootsWikiRedirect {
@@ -658,6 +658,21 @@ fn malformed_nip54_wiki_shapes_are_rejected() {
}
#[test]
+fn wiki_article_codec_accepts_missing_title_tag() {
+ let mut article = wiki_article();
+ article.title = None;
+ let article_event = event_from_parts(wiki_article_to_wire_parts(&article).unwrap());
+ assert!(
+ !article_event
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(String::as_str) == Some("title"))
+ );
+ let decoded = wiki_article_from_event(article_event).unwrap();
+ assert_eq!(decoded.data.data.title, None);
+}
+
+#[test]
fn semantic_validation_rejects_invalid_encode_models() {
let mut article = wiki_article();
article.content_djot = String::new();
diff --git a/crates/test_fixtures/src/knowledge.rs b/crates/test_fixtures/src/knowledge.rs
@@ -185,7 +185,7 @@ pub fn wiki_article_deferred_version_ref() -> RadrootsWikiArticleVersionRef {
pub fn wiki_article() -> RadrootsWikiArticle {
RadrootsWikiArticle {
d_tag: "soil-health".to_string(),
- title: "Soil health".to_string(),
+ title: Some("Soil health".to_string()),
content_djot: "# Soil health\n\nLiving soil supports resilient local food systems."
.to_string(),
summary: Some("Living soil basics".to_string()),