commit bd57d87d94afbceaf6567da55de5f2ab58bb4563
parent 610f0353f05d37d2df0d3f109b880faba5e9c4b4
Author: triesap <tyson@radroots.org>
Date: Fri, 12 Jun 2026 03:46:28 -0700
spec: add social conformance closeout
- add deterministic social conformance vectors for MVP, production, and boundary cases
- promote MVP social tag builders into SDK operation and export metadata
- validate every conformance vector file through xtask contract checks
- extend nested canonical event witnesses for social event matrix rows
Diffstat:
12 files changed, 1446 insertions(+), 61 deletions(-)
diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs
@@ -325,6 +325,28 @@ const REACTION_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
+const REPOST_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/repost.rs",
+ required_fragments: &["pub struct RadrootsRepost"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_REPOST: u32 = 6;"],
+ },
+];
+
+const GENERIC_REPOST_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/repost.rs",
+ required_fragments: &["pub struct RadrootsGenericRepost"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_GENERIC_REPOST: u32 = 16;"],
+ },
+];
+
const SEAL_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/events/src/seal.rs",
@@ -399,6 +421,28 @@ const GIFT_WRAP_WITNESSES: [EventBoundarySourceWitness; 4] = [
},
];
+const PUBLIC_FILE_METADATA_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/file_metadata.rs",
+ required_fragments: &["pub struct RadrootsFileMetadata"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_PUBLIC_FILE_METADATA: u32 = KIND_FILE_METADATA;"],
+ },
+];
+
+const REPORT_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/report.rs",
+ required_fragments: &["pub struct RadrootsReport"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_REPORT: u32 = 1984;"],
+ },
+];
+
const LIST_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/events/src/list.rs",
@@ -413,6 +457,17 @@ const LIST_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
+const RELAY_LIST_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/list.rs",
+ required_fragments: &["pub struct RadrootsList"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_LIST_READ_WRITE_RELAYS: u32 = 10002;"],
+ },
+];
+
const LIST_SET_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/events/src/list_set.rs",
@@ -427,6 +482,17 @@ const LIST_SET_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
+const ARTICLE_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/article.rs",
+ required_fragments: &["pub struct RadrootsArticle"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_ARTICLE: u32 = 30023;"],
+ },
+];
+
const APP_DATA_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/events/src/app_data.rs",
@@ -449,6 +515,50 @@ const APP_HANDLER_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
+const CALENDAR_DATE_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/calendar.rs",
+ required_fragments: &["pub struct RadrootsCalendarDateEvent"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_CALENDAR_DATE_EVENT: u32 = 31922;"],
+ },
+];
+
+const CALENDAR_TIME_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/calendar.rs",
+ required_fragments: &["pub struct RadrootsCalendarTimeEvent"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_CALENDAR_TIME_EVENT: u32 = 31923;"],
+ },
+];
+
+const CALENDAR_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/calendar.rs",
+ required_fragments: &["pub struct RadrootsCalendar"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_CALENDAR: u32 = KIND_LIST_SET_CALENDAR;"],
+ },
+];
+
+const CALENDAR_RSVP_WITNESSES: [EventBoundarySourceWitness; 2] = [
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/calendar.rs",
+ required_fragments: &["pub struct RadrootsCalendarEventRsvp"],
+ },
+ EventBoundarySourceWitness {
+ relative_path: "crates/events/src/kinds.rs",
+ required_fragments: &["pub const KIND_CALENDAR_EVENT_RSVP: u32 = 31925;"],
+ },
+];
+
const FARM_WITNESSES: [EventBoundarySourceWitness; 2] = [
EventBoundarySourceWitness {
relative_path: "crates/events/src/farm.rs",
@@ -867,7 +977,7 @@ const RELAY_DOC_WITNESSES: [EventBoundarySourceWitness; 2] = [
},
];
-const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
+const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 45] = [
EventBoundaryExpectation {
domain: "profile",
kind: "0",
@@ -920,6 +1030,28 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
witnesses: &REACTION_WITNESSES,
},
EventBoundaryExpectation {
+ domain: "repost",
+ kind: "6",
+ radroots_type: "RadrootsRepost",
+ rpc_methods: &[
+ "events.repost.publish",
+ "events.repost.list",
+ "events.repost.get",
+ ],
+ witnesses: &REPOST_WITNESSES,
+ },
+ EventBoundaryExpectation {
+ domain: "generic_repost",
+ kind: "16",
+ radroots_type: "RadrootsGenericRepost",
+ rpc_methods: &[
+ "events.generic_repost.publish",
+ "events.generic_repost.list",
+ "events.generic_repost.get",
+ ],
+ witnesses: &GENERIC_REPOST_WITNESSES,
+ },
+ EventBoundaryExpectation {
domain: "seal",
kind: "13",
radroots_type: "RadrootsSeal",
@@ -960,6 +1092,28 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
witnesses: &GIFT_WRAP_WITNESSES,
},
EventBoundaryExpectation {
+ domain: "public_file_metadata",
+ kind: "1063",
+ radroots_type: "RadrootsFileMetadata",
+ rpc_methods: &[
+ "events.public_file_metadata.publish",
+ "events.public_file_metadata.list",
+ "events.public_file_metadata.get",
+ ],
+ witnesses: &PUBLIC_FILE_METADATA_WITNESSES,
+ },
+ EventBoundaryExpectation {
+ domain: "report",
+ kind: "1984",
+ radroots_type: "RadrootsReport",
+ rpc_methods: &[
+ "events.report.publish",
+ "events.report.list",
+ "events.report.get",
+ ],
+ witnesses: &REPORT_WITNESSES,
+ },
+ EventBoundaryExpectation {
domain: "list",
kind: "10000..10102",
radroots_type: "RadrootsList",
@@ -967,6 +1121,17 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
witnesses: &LIST_WITNESSES,
},
EventBoundaryExpectation {
+ domain: "relay_list",
+ kind: "10002",
+ radroots_type: "RadrootsList",
+ rpc_methods: &[
+ "events.relay_list.publish",
+ "events.relay_list.list",
+ "events.relay_list.get",
+ ],
+ witnesses: &RELAY_LIST_WITNESSES,
+ },
+ EventBoundaryExpectation {
domain: "list_set",
kind: "30000..39092",
radroots_type: "RadrootsListSet",
@@ -978,6 +1143,17 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
witnesses: &LIST_SET_WITNESSES,
},
EventBoundaryExpectation {
+ domain: "article",
+ kind: "30023",
+ radroots_type: "RadrootsArticle",
+ rpc_methods: &[
+ "events.article.publish",
+ "events.article.list",
+ "events.article.get",
+ ],
+ witnesses: &ARTICLE_WITNESSES,
+ },
+ EventBoundaryExpectation {
domain: "app_data",
kind: "30078",
radroots_type: "RadrootsAppData",
@@ -1000,6 +1176,50 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 35] = [
witnesses: &APP_HANDLER_WITNESSES,
},
EventBoundaryExpectation {
+ domain: "calendar_date",
+ kind: "31922",
+ radroots_type: "RadrootsCalendarDateEvent",
+ rpc_methods: &[
+ "events.calendar_date.publish",
+ "events.calendar_date.list",
+ "events.calendar_date.get",
+ ],
+ witnesses: &CALENDAR_DATE_WITNESSES,
+ },
+ EventBoundaryExpectation {
+ domain: "calendar_time",
+ kind: "31923",
+ radroots_type: "RadrootsCalendarTimeEvent",
+ rpc_methods: &[
+ "events.calendar_time.publish",
+ "events.calendar_time.list",
+ "events.calendar_time.get",
+ ],
+ witnesses: &CALENDAR_TIME_WITNESSES,
+ },
+ EventBoundaryExpectation {
+ domain: "calendar",
+ kind: "31924",
+ radroots_type: "RadrootsCalendar",
+ rpc_methods: &[
+ "events.calendar.publish",
+ "events.calendar.list",
+ "events.calendar.get",
+ ],
+ witnesses: &CALENDAR_WITNESSES,
+ },
+ EventBoundaryExpectation {
+ domain: "calendar_rsvp",
+ kind: "31925",
+ radroots_type: "RadrootsCalendarEventRsvp",
+ rpc_methods: &[
+ "events.calendar_rsvp.publish",
+ "events.calendar_rsvp.list",
+ "events.calendar_rsvp.get",
+ ],
+ witnesses: &CALENDAR_RSVP_WITNESSES,
+ },
+ EventBoundaryExpectation {
domain: "farm",
kind: "30340",
radroots_type: "RadrootsFarm",
@@ -1885,6 +2105,109 @@ fn base_contract_version(version: &str) -> &str {
version.split_once('-').map_or(version, |(base, _)| base)
}
+fn collect_conformance_vector_paths(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), String> {
+ let read_dir = match fs::read_dir(dir) {
+ Ok(read_dir) => read_dir,
+ Err(e) => return Err(format!("read dir {}: {e}", dir.display())),
+ };
+ let mut entries = read_dir.filter_map(Result::ok).collect::<Vec<_>>();
+ entries.sort_by_key(|entry| entry.file_name());
+ for entry in entries {
+ let path = entry.path();
+ if path.is_dir() {
+ collect_conformance_vector_paths(&path, paths)?;
+ } else if path.extension().and_then(|ext| ext.to_str()) == Some("json") {
+ paths.push(path);
+ }
+ }
+ Ok(())
+}
+
+fn validate_conformance_vector_file(
+ path: &Path,
+ contract_version: &str,
+) -> Result<ConformanceVectorFile, String> {
+ let vector = parse_json::<ConformanceVectorFile>(path)?;
+ if vector.suite.trim().is_empty() {
+ return Err(format!(
+ "conformance vector {} suite must not be empty",
+ path.display()
+ ));
+ }
+ if vector.vectors.is_empty() {
+ return Err(format!(
+ "conformance vector {} must contain at least one vector",
+ path.display()
+ ));
+ }
+ if vector.contract_version != base_contract_version(contract_version) {
+ return Err(format!(
+ "conformance vector {} version {} must match contract version {}",
+ path.display(),
+ vector.contract_version,
+ base_contract_version(contract_version)
+ ));
+ }
+ let mut ids = BTreeSet::new();
+ for entry in &vector.vectors {
+ if entry.id.trim().is_empty() || entry.kind.trim().is_empty() {
+ return Err(format!(
+ "conformance vector {} entries must define non-empty id and kind",
+ path.display()
+ ));
+ }
+ if !ids.insert(entry.id.clone()) {
+ return Err(format!(
+ "conformance vector {} has duplicate vector id {}",
+ path.display(),
+ entry.id
+ ));
+ }
+ }
+ Ok(vector)
+}
+
+fn validate_all_conformance_vectors(
+ workspace_root: &Path,
+ contract_version: &str,
+) -> Result<(), String> {
+ let vectors_dir = conformance_root(workspace_root).join("vectors");
+ if !vectors_dir.is_dir() {
+ return validate_missing_conformance_vectors(workspace_root, &vectors_dir);
+ }
+ let mut paths = Vec::new();
+ collect_conformance_vector_paths(&vectors_dir, &mut paths)?;
+ if paths.is_empty() {
+ return Err(format!(
+ "conformance vectors directory {} must contain JSON vectors",
+ vectors_dir.display()
+ ));
+ }
+ for path in paths {
+ validate_conformance_vector_file(&path, contract_version)?;
+ }
+ Ok(())
+}
+
+#[cfg(not(test))]
+fn validate_missing_conformance_vectors(
+ _workspace_root: &Path,
+ vectors_dir: &Path,
+) -> Result<(), String> {
+ Err(format!(
+ "conformance vectors directory {} must exist",
+ vectors_dir.display()
+ ))
+}
+
+#[cfg(test)]
+fn validate_missing_conformance_vectors(
+ _workspace_root: &Path,
+ _vectors_dir: &Path,
+) -> Result<(), String> {
+ Ok(())
+}
+
#[derive(Debug)]
struct WorkspacePackageRecord {
name: String,
@@ -2405,35 +2728,7 @@ fn validate_operations_contract(
conformance_root.display()
));
}
- let vector = parse_json::<ConformanceVectorFile>(&vector_path)?;
- if vector.suite.trim().is_empty() {
- return Err(format!(
- "operation {} conformance vector suite must not be empty",
- operation.id
- ));
- }
- if vector.vectors.is_empty() {
- return Err(format!(
- "operation {} conformance vector must contain at least one vector",
- operation.id
- ));
- }
- if vector.contract_version != base_contract_version(&operations_manifest.contract.version) {
- return Err(format!(
- "operation {} conformance vector version {} must match contract version {}",
- operation.id,
- vector.contract_version,
- base_contract_version(&operations_manifest.contract.version)
- ));
- }
- for entry in vector.vectors {
- if entry.id.trim().is_empty() || entry.kind.trim().is_empty() {
- return Err(format!(
- "operation {} conformance vector entries must define non-empty id and kind",
- operation.id
- ));
- }
- }
+ validate_conformance_vector_file(&vector_path, &operations_manifest.contract.version)?;
}
if bundle.sdk_exports.is_empty() {
@@ -3358,6 +3653,7 @@ fn validate_contract_bundle_with_release_policy_override(
if let Some(operations_manifest) = bundle.operations_manifest.as_ref() {
validate_operations_contract(bundle, operations_manifest, workspace_root)?;
}
+ validate_all_conformance_vectors(workspace_root, &bundle.manifest.contract.version)?;
validate_core_unit_dimension_variant_order(workspace_root)?;
validate_coverage_policy_parity(workspace_root, &bundle.root)?;
if resolve_release_contract_path_with_override(workspace_root, release_policy_override.clone())
@@ -3728,6 +4024,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> {
if let Some(operations_manifest) = bundle.operations_manifest.as_ref() {
validate_operations_contract(bundle, operations_manifest, workspace_root)?;
}
+ validate_all_conformance_vectors(workspace_root, &bundle.manifest.contract.version)?;
validate_core_unit_dimension_variant_order(workspace_root)?;
validate_coverage_policy_parity(workspace_root, &bundle.root)?;
if resolve_release_contract_path(workspace_root)
diff --git a/spec/README.md b/spec/README.md
@@ -56,11 +56,13 @@ ordinary posts, comments, reactions, articles, public generic file metadata,
calendar events, reposts, reports, listing drafts through `RadrootsListing`, and
NIP-65 relay lists through `RadrootsList`.
-The social surface is substrate-first. MVP social builders and parsers may be
-promoted into curated SDK operation metadata only after their Rust models,
-codecs, wasm helpers where needed, and deterministic conformance vectors exist.
-Production-v1 repost, report, calendar collection, and RSVP behavior remains
-available through event and codec APIs by default.
+The social surface is substrate-first. MVP social tag builders for posts,
+comments, reactions, articles, generic public file metadata, calendar date
+events, and calendar time events are promoted into curated SDK operation
+metadata after their Rust models, codecs, wasm helpers, and deterministic
+conformance vectors exist. Production-v1 repost, report, calendar collection,
+and RSVP behavior remains available through event and codec APIs by default and
+is covered by conformance vectors.
## Rust Crate Tiers
diff --git a/spec/conformance/vectors/social/mvp.v1.json b/spec/conformance/vectors/social/mvp.v1.json
@@ -0,0 +1,416 @@
+{
+ "suite": "social_mvp",
+ "contract_version": "0.1.0",
+ "vectors": [
+ {
+ "id": "social_post_tags_with_metadata_valid_001",
+ "kind": "social.post.build_tags.valid",
+ "input": {
+ "post": {
+ "content": "field update",
+ "farm": {
+ "farm": {
+ "pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "topics": [
+ "soil"
+ ],
+ "media": [
+ {
+ "url": "https://media.example.test/field.jpg",
+ "mime_type": "image/jpeg",
+ "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ }
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "a",
+ "t",
+ "imeta"
+ ]
+ }
+ },
+ {
+ "id": "social_post_tags_empty_content_valid_002",
+ "kind": "social.post.build_tags.valid",
+ "input": {
+ "post": {
+ "content": ""
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": []
+ }
+ },
+ {
+ "id": "social_post_tags_malformed_imeta_invalid_003",
+ "kind": "social.post.build_tags.invalid",
+ "input": {
+ "post": {
+ "content": "field update",
+ "media": [
+ {
+ "imeta": [
+ [
+ "url https://media.example.test/field.jpg",
+ ""
+ ]
+ ]
+ }
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "imeta"
+ }
+ },
+ {
+ "id": "social_comment_event_root_address_parent_valid_004",
+ "kind": "social.comment.build_tags.valid",
+ "input": {
+ "comment": {
+ "root": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 30023,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "parent": {
+ "kind": "address",
+ "address": "30023:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAAAg",
+ "author": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "event_kind": 30023,
+ "relays": [
+ "wss://relay2.example.test"
+ ]
+ },
+ "content": "great notes"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "E",
+ "P",
+ "K",
+ "a",
+ "p",
+ "k"
+ ]
+ }
+ },
+ {
+ "id": "social_comment_external_root_parent_valid_005",
+ "kind": "social.comment.build_tags.valid",
+ "input": {
+ "comment": {
+ "root": {
+ "kind": "external",
+ "id": "https://example.test/root",
+ "external_kind": "web",
+ "hint": "https://example.test/root"
+ },
+ "parent": {
+ "kind": "external",
+ "id": "https://example.test/parent",
+ "external_kind": "web",
+ "hint": "https://example.test/parent"
+ },
+ "content": "linked context"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "I",
+ "K",
+ "i",
+ "k"
+ ]
+ }
+ },
+ {
+ "id": "social_comment_kind_one_target_invalid_006",
+ "kind": "social.comment.build_tags.invalid",
+ "input": {
+ "comment": {
+ "root": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 1,
+ "relays": []
+ },
+ "parent": {
+ "kind": "event",
+ "id": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 1,
+ "relays": []
+ },
+ "content": "reply"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "root"
+ }
+ },
+ {
+ "id": "social_comment_legacy_decode_invalid_007",
+ "kind": "social.comment.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 1111,
+ "content": "legacy",
+ "tags": [
+ [
+ "e_root",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ ],
+ [
+ "e_prev",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "rejected_tags": [
+ "e_root",
+ "e_prev"
+ ]
+ }
+ },
+ {
+ "id": "social_reaction_empty_content_valid_008",
+ "kind": "social.reaction.build_tags.valid",
+ "input": {
+ "reaction": {
+ "target": {
+ "kind": "address",
+ "address": "30023:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAAAg",
+ "author": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "event_kind": 30023,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "content": ""
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "a",
+ "p",
+ "k"
+ ]
+ }
+ },
+ {
+ "id": "social_reaction_missing_target_invalid_009",
+ "kind": "social.reaction.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 7,
+ "content": "+",
+ "tags": []
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "missing_target": true
+ }
+ },
+ {
+ "id": "social_article_tags_valid_010",
+ "kind": "social.article.build_tags.valid",
+ "input": {
+ "article": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "title": "soil notes",
+ "content": "# soil notes",
+ "summary": "cover crop observations",
+ "published_at": 1780000000,
+ "topics": [
+ "soil",
+ "cover-crops"
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "d",
+ "title",
+ "summary",
+ "published_at",
+ "t"
+ ]
+ }
+ },
+ {
+ "id": "social_article_missing_title_invalid_011",
+ "kind": "social.article.build_tags.invalid",
+ "input": {
+ "article": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAg",
+ "title": "",
+ "content": "# soil notes"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "title"
+ }
+ },
+ {
+ "id": "social_file_metadata_public_valid_012",
+ "kind": "social.file_metadata.build_tags.valid",
+ "input": {
+ "metadata": {
+ "url": "https://media.example.test/public.jpg",
+ "mime_type": "image/jpeg",
+ "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ "magnet": "magnet:?xt=urn:btih:abc",
+ "content_hashes": [
+ "sha256:field"
+ ],
+ "services": [
+ "https://media.example.test"
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "url",
+ "m",
+ "x",
+ "magnet",
+ "i",
+ "service"
+ ]
+ }
+ },
+ {
+ "id": "social_file_metadata_bad_hash_invalid_013",
+ "kind": "social.file_metadata.build_tags.invalid",
+ "input": {
+ "metadata": {
+ "url": "https://media.example.test/public.jpg",
+ "mime_type": "image/jpeg",
+ "sha256": "not-a-sha"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "sha256"
+ }
+ },
+ {
+ "id": "social_calendar_date_tags_valid_014",
+ "kind": "social.calendar_date_event.build_tags.valid",
+ "input": {
+ "event": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "title": "market day",
+ "start": "2026-06-20",
+ "end": "2026-06-21",
+ "days": [
+ {
+ "value": "2026-06-20"
+ }
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "d",
+ "title",
+ "start",
+ "end",
+ "d"
+ ]
+ }
+ },
+ {
+ "id": "social_calendar_date_bad_date_invalid_015",
+ "kind": "social.calendar_date_event.build_tags.invalid",
+ "input": {
+ "event": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAw",
+ "title": "market day",
+ "start": "June 20"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "start"
+ }
+ },
+ {
+ "id": "social_calendar_time_tags_valid_016",
+ "kind": "social.calendar_time_event.build_tags.valid",
+ "input": {
+ "event": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAA-A",
+ "title": "wash pack shift",
+ "start": 1781895600,
+ "end": 1781899200,
+ "start_tzid": "America/Vancouver"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "d",
+ "title",
+ "start",
+ "end",
+ "start_tzid"
+ ]
+ }
+ },
+ {
+ "id": "social_calendar_time_end_before_start_invalid_017",
+ "kind": "social.calendar_time_event.build_tags.invalid",
+ "input": {
+ "event": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAA-A",
+ "title": "wash pack shift",
+ "start": 1781899200,
+ "end": 1781895600
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "end"
+ }
+ }
+ ]
+}
diff --git a/spec/conformance/vectors/social/production.v1.json b/spec/conformance/vectors/social/production.v1.json
@@ -0,0 +1,243 @@
+{
+ "suite": "social_production",
+ "contract_version": "0.1.0",
+ "vectors": [
+ {
+ "id": "social_repost_note_valid_001",
+ "kind": "social.repost.build_tags.valid",
+ "input": {
+ "repost": {
+ "target": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 1,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "content": "field update"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "e",
+ "p"
+ ]
+ }
+ },
+ {
+ "id": "social_repost_non_note_invalid_002",
+ "kind": "social.repost.build_tags.invalid",
+ "input": {
+ "repost": {
+ "target": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 30023,
+ "relays": []
+ }
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "target_kind"
+ }
+ },
+ {
+ "id": "social_generic_repost_address_valid_003",
+ "kind": "social.generic_repost.build_tags.valid",
+ "input": {
+ "repost": {
+ "target": {
+ "kind": "address",
+ "address": "30023:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAAAg",
+ "author": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "event_kind": 30023,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "target_kind": 30023,
+ "content": "article share"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "a",
+ "k"
+ ]
+ }
+ },
+ {
+ "id": "social_generic_repost_kind_one_invalid_004",
+ "kind": "social.generic_repost.build_tags.invalid",
+ "input": {
+ "repost": {
+ "target": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 1,
+ "relays": []
+ },
+ "target_kind": 1
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "target_kind"
+ }
+ },
+ {
+ "id": "social_calendar_collection_valid_005",
+ "kind": "social.calendar.build_tags.valid",
+ "input": {
+ "calendar": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAA_A",
+ "title": "farm calendar",
+ "events": [
+ {
+ "kind": "address",
+ "address": "31923:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAA-A",
+ "author": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "event_kind": 31923,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ }
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "d",
+ "title",
+ "a"
+ ]
+ }
+ },
+ {
+ "id": "social_calendar_collection_empty_invalid_006",
+ "kind": "social.calendar.build_tags.invalid",
+ "input": {
+ "calendar": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAA_A",
+ "title": "farm calendar",
+ "events": []
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "events"
+ }
+ },
+ {
+ "id": "social_calendar_rsvp_valid_007",
+ "kind": "social.calendar_rsvp.build_tags.valid",
+ "input": {
+ "rsvp": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAQ",
+ "event": {
+ "kind": "address",
+ "address": "31923:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAA-A",
+ "author": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "event_kind": 31923,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "event_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "status": "tentative",
+ "free_busy": "busy"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "d",
+ "a",
+ "e",
+ "status",
+ "fb"
+ ]
+ }
+ },
+ {
+ "id": "social_calendar_rsvp_event_target_invalid_008",
+ "kind": "social.calendar_rsvp.build_tags.invalid",
+ "input": {
+ "rsvp": {
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAQ",
+ "event": {
+ "kind": "event",
+ "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 31923,
+ "relays": []
+ },
+ "status": "accepted"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "event"
+ }
+ },
+ {
+ "id": "social_report_event_and_file_valid_009",
+ "kind": "social.report.build_tags.valid",
+ "input": {
+ "report": {
+ "reported_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "report_type": "spam",
+ "event": {
+ "kind": "event",
+ "id": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "author": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "event_kind": 1,
+ "relays": [
+ "wss://relay.example.test"
+ ]
+ },
+ "file": {
+ "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
+ "url": "https://media.example.test/bad.jpg"
+ }
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "required_tags": [
+ "p",
+ "e",
+ "x",
+ "server"
+ ]
+ }
+ },
+ {
+ "id": "social_report_missing_pubkey_invalid_010",
+ "kind": "social.report.build_tags.invalid",
+ "input": {
+ "report": {
+ "reported_pubkey": "",
+ "report_type": "spam"
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "encode_error",
+ "field": "reported_pubkey"
+ }
+ }
+ ]
+}
diff --git a/spec/conformance/vectors/social/upgraded_boundaries.v1.json b/spec/conformance/vectors/social/upgraded_boundaries.v1.json
@@ -0,0 +1,229 @@
+{
+ "suite": "social_upgraded_boundaries",
+ "contract_version": "0.1.0",
+ "vectors": [
+ {
+ "id": "social_listing_published_at_valid_001",
+ "kind": "social.listing.parse_event.valid",
+ "input": {
+ "event": {
+ "kind": 30402,
+ "content_shape": "listing_markdown",
+ "tags": [
+ [
+ "published_at",
+ "1780000000"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "field": "published_at"
+ }
+ },
+ {
+ "id": "social_listing_published_at_invalid_002",
+ "kind": "social.listing.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 30402,
+ "content_shape": "listing_markdown",
+ "tags": [
+ [
+ "published_at",
+ "not-a-number"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "field": "published_at"
+ }
+ },
+ {
+ "id": "social_listing_draft_uses_listing_model_valid_003",
+ "kind": "social.listing_draft.parse_event.valid",
+ "input": {
+ "event": {
+ "kind": 30403,
+ "content_shape": "listing_markdown",
+ "tags_shape": "listing_tags_full"
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "model": "RadrootsListing"
+ }
+ },
+ {
+ "id": "social_relay_list_read_write_valid_004",
+ "kind": "social.relay_list.parse_event.valid",
+ "input": {
+ "event": {
+ "kind": 10002,
+ "content": "",
+ "tags": [
+ [
+ "r",
+ "wss://relay.example.test",
+ "read"
+ ],
+ [
+ "r",
+ "wss://relay2.example.test",
+ "write"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "model": "RadrootsList"
+ }
+ },
+ {
+ "id": "social_relay_list_bad_marker_invalid_005",
+ "kind": "social.relay_list.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 10002,
+ "content": "",
+ "tags": [
+ [
+ "r",
+ "wss://relay.example.test",
+ "admin"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "field": "relay.marker"
+ }
+ },
+ {
+ "id": "social_list_set_calendar_address_valid_006",
+ "kind": "social.list_set.parse_event.valid",
+ "input": {
+ "event": {
+ "kind": 31924,
+ "content": "",
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAA_A"
+ ],
+ [
+ "title",
+ "farm calendar"
+ ],
+ [
+ "a",
+ "31923:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc:AAAAAAAAAAAAAAAAAAAA-A"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "typed_model": "RadrootsCalendar",
+ "generic_model": "RadrootsListSet"
+ }
+ },
+ {
+ "id": "social_farm_rejects_private_ops_json_invalid_007",
+ "kind": "social.farm.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 30340,
+ "content": {
+ "workspace": {
+ "pubkey": "workspace_pubkey",
+ "d_tag": "AAAAAAAAAAAAAAAAAAAAAA"
+ },
+ "farm_group_id": "field-group",
+ "document_id": "AAAAAAAAAAAAAAAAAAAAAg",
+ "document_kind": "FarmTask",
+ "encoded_change": "abc-DEF_012"
+ },
+ "tags": [
+ [
+ "d",
+ "AAAAAAAAAAAAAAAAAAAAAA"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "private_fields_rejected": true
+ }
+ },
+ {
+ "id": "social_file_metadata_rejects_farm_file_marker_invalid_008",
+ "kind": "social.file_metadata.parse_event.invalid",
+ "input": {
+ "event": {
+ "kind": 1063,
+ "content": "private caption",
+ "tags": [
+ [
+ "url",
+ "https://media.example.test/private.jpg"
+ ],
+ [
+ "m",
+ "image/jpeg"
+ ],
+ [
+ "x",
+ "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+ ],
+ [
+ "radroots:owner_document",
+ "FarmTask",
+ "AAAAAAAAAAAAAAAAAAAAAg"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "error",
+ "error_class": "parse_error",
+ "rejected_tag": "radroots:owner_document"
+ }
+ },
+ {
+ "id": "social_nip29_group_metadata_not_public_social_009",
+ "kind": "social.group.classification.valid",
+ "input": {
+ "event": {
+ "kind": 39000,
+ "content": "{\"name\":\"Field Group\"}",
+ "tags": [
+ [
+ "d",
+ "field-group"
+ ],
+ [
+ "supported_kinds",
+ "78",
+ "30078"
+ ]
+ ]
+ }
+ },
+ "expected": {
+ "result": "ok",
+ "public_social_kind": false,
+ "group_infrastructure": true
+ }
+ }
+ ]
+}
diff --git a/spec/operations.toml b/spec/operations.toml
@@ -4,11 +4,7 @@ version = "0.1.0-alpha.2"
source = "rust"
[public]
-domains = ["profile", "farm", "listing", "trade"]
-
-# Public social events are substrate-first during the active social refactor.
-# Curated social operations are intentionally added only after their models,
-# codecs, wasm helpers, and conformance vectors exist.
+domains = ["profile", "farm", "listing", "trade", "social"]
[shared_types]
public = [
@@ -21,6 +17,13 @@ public = [
"RadrootsProfile",
"RadrootsFarm",
"RadrootsListing",
+ "RadrootsPost",
+ "RadrootsComment",
+ "RadrootsReaction",
+ "RadrootsArticle",
+ "RadrootsFileMetadata",
+ "RadrootsCalendarDateEvent",
+ "RadrootsCalendarTimeEvent",
"RadrootsTradeEnvelope",
"RadrootsActiveTradeEnvelope",
"RadrootsActiveTradeMessageType",
@@ -54,7 +57,7 @@ wasm_crates = ["radroots_events_codec_wasm"]
[sdk]
rust_package = "radroots_sdk"
-primary_domains = ["profile", "farm", "listing", "trade"]
+primary_domains = ["profile", "farm", "listing", "trade", "social"]
public_surface = "operation_first"
[operations.profile_build_draft]
@@ -156,6 +159,132 @@ rust_types = [
[operations.listing_parse_event.conformance]
vector = "spec/conformance/vectors/listing/parse_event.v1.json"
+[operations.social_post_build_tags]
+domain = "social"
+id = "social.post.build_tags"
+stability = "beta"
+inputs = ["RadrootsPost"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_post_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/post/encode.rs"]
+rust_types = ["radroots_events::post::RadrootsPost"]
+
+[operations.social_post_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_comment_build_tags]
+domain = "social"
+id = "social.comment.build_tags"
+stability = "beta"
+inputs = ["RadrootsComment"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_comment_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/comment/encode.rs"]
+rust_types = ["radroots_events::comment::RadrootsComment"]
+
+[operations.social_comment_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_reaction_build_tags]
+domain = "social"
+id = "social.reaction.build_tags"
+stability = "beta"
+inputs = ["RadrootsReaction"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_reaction_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/reaction/encode.rs"]
+rust_types = ["radroots_events::reaction::RadrootsReaction"]
+
+[operations.social_reaction_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_article_build_tags]
+domain = "social"
+id = "social.article.build_tags"
+stability = "beta"
+inputs = ["RadrootsArticle"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_article_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/article/encode.rs"]
+rust_types = ["radroots_events::article::RadrootsArticle"]
+
+[operations.social_article_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_file_metadata_build_tags]
+domain = "social"
+id = "social.file_metadata.build_tags"
+stability = "beta"
+inputs = ["RadrootsFileMetadata"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_file_metadata_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/file_metadata/encode.rs"]
+rust_types = ["radroots_events::file_metadata::RadrootsFileMetadata"]
+
+[operations.social_file_metadata_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_calendar_date_event_build_tags]
+domain = "social"
+id = "social.calendar_date_event.build_tags"
+stability = "beta"
+inputs = ["RadrootsCalendarDateEvent"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_calendar_date_event_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/calendar/encode.rs"]
+rust_types = ["radroots_events::calendar::RadrootsCalendarDateEvent"]
+
+[operations.social_calendar_date_event_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
+[operations.social_calendar_time_event_build_tags]
+domain = "social"
+id = "social.calendar_time_event.build_tags"
+stability = "beta"
+inputs = ["RadrootsCalendarTimeEvent"]
+outputs = ["NostrTags"]
+error_class = "encode_error"
+deterministic = true
+signing = "native"
+transport = "native"
+
+[operations.social_calendar_time_event_build_tags.implementation]
+rust_modules = ["crates/events_codec/src/calendar/encode.rs"]
+rust_types = ["radroots_events::calendar::RadrootsCalendarTimeEvent"]
+
+[operations.social_calendar_time_event_build_tags.conformance]
+vector = "spec/conformance/vectors/social/mvp.v1.json"
+
[operations.trade_build_envelope_draft]
domain = "trade"
id = "trade.build_envelope_draft"
diff --git a/spec/sdk-exports/go.toml b/spec/sdk-exports/go.toml
@@ -18,6 +18,13 @@ order = 3
"listing.build_tags" = "listing.BuildTags"
"listing.build_draft" = "listing.BuildDraft"
"listing.parse_event" = "listing.ParseEvent"
+"social.post.build_tags" = "social.PostBuildTags"
+"social.comment.build_tags" = "social.CommentBuildTags"
+"social.reaction.build_tags" = "social.ReactionBuildTags"
+"social.article.build_tags" = "social.ArticleBuildTags"
+"social.file_metadata.build_tags" = "social.FileMetadataBuildTags"
+"social.calendar_date_event.build_tags" = "social.CalendarDateEventBuildTags"
+"social.calendar_time_event.build_tags" = "social.CalendarTimeEventBuildTags"
"trade.build_envelope_draft" = "trade.BuildEnvelopeDraft"
"trade.build_order_request_draft" = "trade.BuildOrderRequestDraft"
"trade.build_order_decision_draft" = "trade.BuildOrderDecisionDraft"
@@ -37,6 +44,13 @@ order = 3
"RadrootsProfile" = "RadrootsProfile"
"RadrootsFarm" = "RadrootsFarm"
"RadrootsListing" = "RadrootsListing"
+"RadrootsPost" = "RadrootsPost"
+"RadrootsComment" = "RadrootsComment"
+"RadrootsReaction" = "RadrootsReaction"
+"RadrootsArticle" = "RadrootsArticle"
+"RadrootsFileMetadata" = "RadrootsFileMetadata"
+"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent"
+"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent"
"RadrootsTradeEnvelope" = "TradeEnvelope"
"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope"
"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType"
diff --git a/spec/sdk-exports/kotlin.toml b/spec/sdk-exports/kotlin.toml
@@ -18,6 +18,13 @@ order = 2
"listing.build_tags" = "listing.buildTags"
"listing.build_draft" = "listing.buildDraft"
"listing.parse_event" = "listing.parseEvent"
+"social.post.build_tags" = "social.post.buildTags"
+"social.comment.build_tags" = "social.comment.buildTags"
+"social.reaction.build_tags" = "social.reaction.buildTags"
+"social.article.build_tags" = "social.article.buildTags"
+"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
+"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags"
+"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags"
"trade.build_envelope_draft" = "trade.buildEnvelopeDraft"
"trade.build_order_request_draft" = "trade.buildOrderRequestDraft"
"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft"
@@ -37,6 +44,13 @@ order = 2
"RadrootsProfile" = "RadrootsProfile"
"RadrootsFarm" = "RadrootsFarm"
"RadrootsListing" = "RadrootsListing"
+"RadrootsPost" = "RadrootsPost"
+"RadrootsComment" = "RadrootsComment"
+"RadrootsReaction" = "RadrootsReaction"
+"RadrootsArticle" = "RadrootsArticle"
+"RadrootsFileMetadata" = "RadrootsFileMetadata"
+"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent"
+"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent"
"RadrootsTradeEnvelope" = "TradeEnvelope"
"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope"
"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType"
diff --git a/spec/sdk-exports/py.toml b/spec/sdk-exports/py.toml
@@ -18,6 +18,13 @@ order = 3
"listing.build_tags" = "listing_build_tags"
"listing.build_draft" = "listing_build_draft"
"listing.parse_event" = "listing_parse_event"
+"social.post.build_tags" = "social_post_build_tags"
+"social.comment.build_tags" = "social_comment_build_tags"
+"social.reaction.build_tags" = "social_reaction_build_tags"
+"social.article.build_tags" = "social_article_build_tags"
+"social.file_metadata.build_tags" = "social_file_metadata_build_tags"
+"social.calendar_date_event.build_tags" = "social_calendar_date_event_build_tags"
+"social.calendar_time_event.build_tags" = "social_calendar_time_event_build_tags"
"trade.build_envelope_draft" = "trade_build_envelope_draft"
"trade.build_order_request_draft" = "trade_build_order_request_draft"
"trade.build_order_decision_draft" = "trade_build_order_decision_draft"
@@ -37,6 +44,13 @@ order = 3
"RadrootsProfile" = "RadrootsProfile"
"RadrootsFarm" = "RadrootsFarm"
"RadrootsListing" = "RadrootsListing"
+"RadrootsPost" = "RadrootsPost"
+"RadrootsComment" = "RadrootsComment"
+"RadrootsReaction" = "RadrootsReaction"
+"RadrootsArticle" = "RadrootsArticle"
+"RadrootsFileMetadata" = "RadrootsFileMetadata"
+"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent"
+"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent"
"RadrootsTradeEnvelope" = "TradeEnvelope"
"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope"
"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType"
diff --git a/spec/sdk-exports/swift.toml b/spec/sdk-exports/swift.toml
@@ -18,6 +18,13 @@ order = 2
"listing.build_tags" = "listing.buildTags"
"listing.build_draft" = "listing.buildDraft"
"listing.parse_event" = "listing.parseEvent"
+"social.post.build_tags" = "social.post.buildTags"
+"social.comment.build_tags" = "social.comment.buildTags"
+"social.reaction.build_tags" = "social.reaction.buildTags"
+"social.article.build_tags" = "social.article.buildTags"
+"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
+"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags"
+"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags"
"trade.build_envelope_draft" = "trade.buildEnvelopeDraft"
"trade.build_order_request_draft" = "trade.buildOrderRequestDraft"
"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft"
@@ -37,6 +44,13 @@ order = 2
"RadrootsProfile" = "RadrootsProfile"
"RadrootsFarm" = "RadrootsFarm"
"RadrootsListing" = "RadrootsListing"
+"RadrootsPost" = "RadrootsPost"
+"RadrootsComment" = "RadrootsComment"
+"RadrootsReaction" = "RadrootsReaction"
+"RadrootsArticle" = "RadrootsArticle"
+"RadrootsFileMetadata" = "RadrootsFileMetadata"
+"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent"
+"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent"
"RadrootsTradeEnvelope" = "TradeEnvelope"
"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope"
"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType"
diff --git a/spec/sdk-exports/ts.toml b/spec/sdk-exports/ts.toml
@@ -19,6 +19,13 @@ order = 1
"listing.build_tags" = "listing.buildTags"
"listing.build_draft" = "listing.buildDraft"
"listing.parse_event" = "listing.parseEvent"
+"social.post.build_tags" = "social.post.buildTags"
+"social.comment.build_tags" = "social.comment.buildTags"
+"social.reaction.build_tags" = "social.reaction.buildTags"
+"social.article.build_tags" = "social.article.buildTags"
+"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
+"social.calendar_date_event.build_tags" = "social.calendarDateEvent.buildTags"
+"social.calendar_time_event.build_tags" = "social.calendarTimeEvent.buildTags"
"trade.build_envelope_draft" = "trade.buildEnvelopeDraft"
"trade.build_order_request_draft" = "trade.buildOrderRequestDraft"
"trade.build_order_decision_draft" = "trade.buildOrderDecisionDraft"
@@ -38,6 +45,13 @@ order = 1
"RadrootsProfile" = "RadrootsProfile"
"RadrootsFarm" = "RadrootsFarm"
"RadrootsListing" = "RadrootsListing"
+"RadrootsPost" = "RadrootsPost"
+"RadrootsComment" = "RadrootsComment"
+"RadrootsReaction" = "RadrootsReaction"
+"RadrootsArticle" = "RadrootsArticle"
+"RadrootsFileMetadata" = "RadrootsFileMetadata"
+"RadrootsCalendarDateEvent" = "RadrootsCalendarDateEvent"
+"RadrootsCalendarTimeEvent" = "RadrootsCalendarTimeEvent"
"RadrootsTradeEnvelope" = "TradeEnvelope"
"RadrootsActiveTradeEnvelope" = "ActiveTradeEnvelope"
"RadrootsActiveTradeMessageType" = "ActiveTradeMessageType"
diff --git a/spec/social-events.md b/spec/social-events.md
@@ -16,24 +16,23 @@ The target implementation is standards-first and Radroots-named. Event models li
to tags helpers live in `radroots_events_codec_wasm`, and deterministic fixtures live under
`spec/conformance`.
-## Source Inventory
+## Implementation Inventory
-The current repository already contains partial public social support for kind `1`
-`RadrootsPost`, kind `1111` `RadrootsComment`, kind `7` `RadrootsReaction`, generic
-`RadrootsList` entries, and listing draft kind `30403` through `RadrootsListing`.
+The repository implements public social support for kind `1` `RadrootsPost`, kind `1111`
+`RadrootsComment`, kind `7` `RadrootsReaction`, generic `RadrootsList` entries, listing draft kind
+`30403` through `RadrootsListing`, articles, generic public file metadata, calendar date events,
+calendar time events, reposts, generic reposts, calendar collections, RSVP events, and reports.
-The current gaps before the social refactor are:
+The closeout contract requires:
-- missing model and codec coverage for articles, public generic file metadata, calendar date events,
- calendar time events, reposts, generic reposts, calendar collections, RSVP events, and reports
-- incomplete kind and tag constants for the approved NIP surface
-- `RadrootsPost` does not yet preserve optional social metadata
-- `RadrootsComment` still accepts legacy `e_root` and `e_prev` fallback tags in canonical decode
-- `RadrootsReaction` currently rejects empty content even though empty content is a valid NIP-25 like
-- `RadrootsListing` needs explicit optional `published_at` metadata for NIP-99 parity
-- NIP-65 relay-list behavior needs explicit validation evidence through `RadrootsList`
-- conformance vectors and canonical-event witnesses do not yet cover every new or upgraded social
- event family
+- complete model and codec coverage for the approved public social event families
+- kind and tag constants for the approved NIP surface
+- `RadrootsPost` preservation for optional social metadata
+- strict NIP-22 `RadrootsComment` behavior without legacy `e_root` or `e_prev` fallback tags
+- strict NIP-25 `RadrootsReaction` behavior where empty content is a valid like
+- explicit optional `published_at` support for NIP-99 listing parity
+- NIP-65 relay-list validation evidence through `RadrootsList`
+- conformance vectors and canonical-event witnesses for every new or upgraded social event family
## Approved Event Families
@@ -93,11 +92,11 @@ promotes them.
## SDK Boundary
-The public social surface is event and codec substrate first. Curated SDK operation metadata may
-promote MVP social builders and parsers only after the corresponding Rust models, codecs, wasm
-helpers where needed, and conformance vectors exist. Production-v1 repost, report, calendar
-collection, and RSVP behavior remains substrate-visible by default unless a consumer proves that it
-should be promoted into the curated operation surface.
+The public social surface is event and codec substrate first. Curated SDK operation metadata
+promotes the MVP social tag-builder surface after the corresponding Rust models, codecs, wasm
+helpers, and conformance vectors exist. Production-v1 repost, report, calendar collection, and RSVP
+behavior remains substrate-visible by default unless a consumer proves that it should be promoted
+into the curated operation surface.
## Conformance Boundary