lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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:
Mcrates/xtask/src/contract.rs | 357++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mspec/README.md | 12+++++++-----
Aspec/conformance/vectors/social/mvp.v1.json | 416+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspec/conformance/vectors/social/production.v1.json | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspec/conformance/vectors/social/upgraded_boundaries.v1.json | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mspec/operations.toml | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mspec/sdk-exports/go.toml | 14++++++++++++++
Mspec/sdk-exports/kotlin.toml | 14++++++++++++++
Mspec/sdk-exports/py.toml | 14++++++++++++++
Mspec/sdk-exports/swift.toml | 14++++++++++++++
Mspec/sdk-exports/ts.toml | 14++++++++++++++
Mspec/social-events.md | 39+++++++++++++++++++--------------------
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