lib

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

commit f133f4d07f00b9d1209d79daca052a8c57e7ecd8
parent 9656fb4f6ccf20c79fe27e4bf61ce5d1b5b797ba
Author: triesap <tyson@radroots.org>
Date:   Sat, 18 Jul 2026 11:02:47 +0000

contract: close breaking event compatibility

- move every governed package and contract to mounted-safe 1.0.0-alpha.1 with exact release validation
- remove legacy Profile authoring, publishing, and implicit SDK bypasses in favor of shared Blossom proofs
- make identity and replica inputs fail closed, advance transfer v2, and exclude lossy Profile emission
- preserve replica backup schema compatibility and enforce executable conformance and mirror governance

Diffstat:
ACHANGELOG.md | 50++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.lock | 104++++++++++++++++++++++++++++++++++++++++----------------------------------------
MCargo.toml | 103++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcontracts/conformance/vectors/blossom/bud11_claims.v1.json | 2+-
Mcontracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json | 2+-
Mcontracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json | 2+-
Mcontracts/conformance/vectors/calendar/nip52_baseline.v1.json | 2+-
Mcontracts/conformance/vectors/calendar/radroots_profile.v1.json | 2+-
Mcontracts/conformance/vectors/event/nip01_wire.v1.json | 2+-
Mcontracts/conformance/vectors/events/listing_tags_full.v1.json | 2+-
Mcontracts/conformance/vectors/farm/build_draft.v1.json | 2+-
Mcontracts/conformance/vectors/knowledge/manifest_and_decode.v1.json | 2+-
Mcontracts/conformance/vectors/knowledge/public_surface.v1.json | 2+-
Mcontracts/conformance/vectors/listing/build_draft.v1.json | 2+-
Mcontracts/conformance/vectors/listing/build_tags.v1.json | 2+-
Mcontracts/conformance/vectors/listing/parse_event.v1.json | 2+-
Mcontracts/conformance/vectors/mesh/frame_cbor.v1.json | 2+-
Mcontracts/conformance/vectors/nip46/current_session.v1.json | 2+-
Dcontracts/conformance/vectors/profile/build_draft.v1.json | 23-----------------------
Mcontracts/conformance/vectors/profile/metadata.v1.json | 2+-
Mcontracts/conformance/vectors/replica_schema/json_models.v1.json | 2+-
Mcontracts/conformance/vectors/social/mvp.v1.json | 2+-
Mcontracts/conformance/vectors/social/production.v1.json | 2+-
Mcontracts/conformance/vectors/social/upgraded_boundaries.v1.json | 2+-
Mcontracts/conformance/vectors/trade/build_mutation_event.v1.json | 2+-
Mcontracts/conformance/vectors/trade/canonicalize_mutation.v1.json | 2+-
Mcontracts/conformance/vectors/trade/parse_listing_address.v1.json | 2+-
Mcontracts/conformance/vectors/trade/parse_mutation_event.v1.json | 2+-
Mcontracts/conformance/vectors/trade/reduce_records.v1.json | 2+-
Mcontracts/conformance/vectors/trade_validation/validate_listing_event.v1.json | 2+-
Mcontracts/conformance/vectors/transport/target_uri.v1.json | 2+-
Mcontracts/event_boundary_matrix.md | 2+-
Mcontracts/events/profile-metadata.md | 35++++++++++++++++-------------------
Mcontracts/knowledge/knowledge_event_contract_manifest.v2.json | 4++--
Mcontracts/knowledge/knowledge_event_contract_manifest.v2.sha256 | 2+-
Mcontracts/manifest.toml | 2+-
Mcontracts/operations.toml | 27+++------------------------
Acontracts/releases/1.0.0-alpha.1.toml | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/replica.toml | 13+++++++++++--
Mcontracts/version.toml | 8+++++++-
Mcrates/authority/Cargo.toml | 2+-
Mcrates/blossom/Cargo.toml | 2+-
Mcrates/blossom/tests/bud11_conformance.rs | 2+-
Mcrates/blossom/tests/conformance.rs | 2+-
Mcrates/blossom/tests/fixtures/bud11_claims.v1.json | 2+-
Mcrates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json | 2+-
Mcrates/core/Cargo.toml | 2+-
Mcrates/event/Cargo.toml | 2+-
Mcrates/event/README | 9+++++----
Mcrates/event/src/contract.rs | 2+-
Mcrates/event/src/generated/dto_roots.rs | 1-
Mcrates/event/src/profile.rs | 101+++++++++++++++++--------------------------------------------------------------
Mcrates/event_codec/Cargo.toml | 2+-
Mcrates/event_codec/src/manifest.rs | 26+++++++++++++++++++++++++-
Dcrates/event_codec/src/profile/encode.rs | 113-------------------------------------------------------------------------------
Dcrates/event_codec/src/profile/error.rs | 22----------------------
Mcrates/event_codec/src/profile/mod.rs | 7+------
Mcrates/event_codec/src/tag_builders.rs | 12+-----------
Mcrates/event_codec/tests/calendar_conformance.rs | 4++--
Mcrates/event_codec/tests/codec_error_job.rs | 13-------------
Mcrates/event_codec/tests/fixtures/calendar_nip52_baseline.v1.json | 2+-
Mcrates/event_codec/tests/fixtures/calendar_radroots_profile.v1.json | 2+-
Mcrates/event_codec/tests/fixtures/profile_metadata.v1.json | 2+-
Mcrates/event_codec/tests/profile_conformance.rs | 22++++++++++++----------
Dcrates/event_codec/tests/profile_encode.rs | 76----------------------------------------------------------------------------
Mcrates/event_codec/tests/tag_builders.rs | 15---------------
Mcrates/event_index/Cargo.toml | 2+-
Mcrates/event_store/Cargo.toml | 2+-
Mcrates/geocoder/Cargo.toml | 2+-
Mcrates/identity/Cargo.toml | 8++------
Mcrates/identity/src/identity.rs | 66++++++++++--------------------------------------------------------
Mcrates/identity/tests/identity.rs | 71+++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/log/Cargo.toml | 2+-
Mcrates/mesh/Cargo.toml | 2+-
Mcrates/mesh_agent_client/Cargo.toml | 2+-
Mcrates/mesh_agent_proto/Cargo.toml | 2+-
Mcrates/net/Cargo.toml | 2+-
Mcrates/net/src/nostr_client/events/profile.rs | 40+---------------------------------------
Mcrates/net/src/nostr_client/status.rs | 2+-
Mcrates/nostr/Cargo.toml | 2+-
Mcrates/nostr/src/client.rs | 68++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/nostr/src/error.rs | 4----
Mcrates/nostr/src/events/metadata.rs | 32++------------------------------
Dcrates/nostr/src/identity_profile.rs | 108-------------------------------------------------------------------------------
Mcrates/nostr/src/lib.rs | 22++++------------------
Mcrates/nostr/src/relays.rs | 2+-
Mcrates/nostr/src/types.rs | 3+++
Mcrates/nostr/tests/blossom_conformance.rs | 2+-
Mcrates/nostr/tests/coverage.rs | 14++------------
Mcrates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json | 2+-
Mcrates/nostr_accounts/Cargo.toml | 3+--
Mcrates/nostr_connect/Cargo.toml | 2+-
Mcrates/nostr_connect/tests/protocol.rs | 2+-
Mcrates/nostr_runtime/Cargo.toml | 2+-
Mcrates/nostr_signer/Cargo.toml | 3+--
Mcrates/nostrdb/Cargo.toml | 2+-
Mcrates/nostrdb/src/nostrdb.rs | 4++--
Mcrates/outbox/Cargo.toml | 2+-
Mcrates/protected_store/Cargo.toml | 2+-
Mcrates/protocol_contract_v1/Cargo.toml | 2+-
Mcrates/replica_schema/Cargo.toml | 2+-
Mcrates/replica_store/Cargo.toml | 2+-
Mcrates/replica_store/src/backup.rs | 47+++++++++++++++++++++++++++++------------------
Mcrates/replica_store/src/export.rs | 6++++--
Mcrates/replica_store/tests/backup_export_paths.rs | 8++++----
Mcrates/replica_sync/Cargo.toml | 2+-
Mcrates/replica_sync/src/emit.rs | 392+++----------------------------------------------------------------------------
Mcrates/replica_sync/src/ingest.rs | 28+++++++++++++---------------
Mcrates/replica_sync/src/lib.rs | 3+--
Mcrates/replica_sync/src/sync_state.rs | 4++--
Mcrates/replica_sync/src/tests.rs | 54+++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/replica_sync/src/types.rs | 6++++--
Mcrates/replica_sync/tests/ingest_roundtrip.rs | 40+++++++++++++++-------------------------
Mcrates/runtime/Cargo.toml | 2+-
Mcrates/runtime_distribution/Cargo.toml | 2+-
Mcrates/runtime_distribution/src/lib.rs | 32++++++++++++++++----------------
Mcrates/runtime_manager/Cargo.toml | 2+-
Mcrates/runtime_manager/src/lib.rs | 2+-
Mcrates/runtime_manager/src/managed.rs | 4++--
Mcrates/runtime_manager/src/registry.rs | 2+-
Mcrates/runtime_paths/Cargo.toml | 2+-
Mcrates/runtime_store/Cargo.toml | 2+-
Mcrates/secret_vault/Cargo.toml | 2+-
Mcrates/simplex_agent_proto/Cargo.toml | 2+-
Mcrates/simplex_agent_runtime/Cargo.toml | 2+-
Mcrates/simplex_agent_store/Cargo.toml | 2+-
Mcrates/simplex_app_store/Cargo.toml | 2+-
Mcrates/simplex_chat_proto/Cargo.toml | 2+-
Mcrates/simplex_interop_tests/Cargo.toml | 2+-
Mcrates/simplex_smp_crypto/Cargo.toml | 2+-
Mcrates/simplex_smp_proto/Cargo.toml | 2+-
Mcrates/simplex_smp_transport/Cargo.toml | 2+-
Mcrates/sql_core/Cargo.toml | 2+-
Mcrates/test_fixtures/Cargo.toml | 2+-
Mcrates/trade/Cargo.toml | 2+-
Mcrates/trade_sp1_guest/Cargo.toml | 2+-
Mcrates/trade_sp1_host/Cargo.toml | 2+-
Mcrates/transport/Cargo.toml | 2+-
Mcrates/transport_nostr/Cargo.toml | 2+-
Mcrates/transport_publish_protocol/Cargo.toml | 2+-
Mcrates/transport_reticulum/Cargo.toml | 2+-
Mtools/xtask/Cargo.toml | 3++-
Mtools/xtask/src/contract.rs | 1463++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mtools/xtask/src/coverage.rs | 2+-
144 files changed, 2085 insertions(+), 1504 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -0,0 +1,50 @@ +# Changelog + +All notable changes to the Radroots core libraries are documented in this file. + +## [1.0.0-alpha.1] + +This alpha is not published until the repository release preflight and external +publish policy both pass for the same source revision. + +### Changed + +- Calendar authoring and admission now use explicit NIP-52 authored, parsed, and + admitted states for date events, time events, calendars, and RSVPs. Kind + `31922` no longer emits uppercase `D`; kind `31923` derives integer UTC-day + `D` values from its validated time range. +- Authored profile and calendar media now share the byte-verified Blossom image + proof type; unverified URLs remain inbound data and cannot enter authoring APIs. +- Workspace packages declare one governed version explicitly so mounted path + consumers preserve it, and every internal root dependency requires that exact + pre-release version. +- Conformance suites now identify the `1.0.0` event-contract generation. +- Release metadata records exact governed impacts for removed public types, + fields, functions, modules, constants, Cargo features, and trait + implementations, plus changed field types, constant values, and algorithms. + +### Removed + +- Legacy calendar event models and permissive calendar tag-builder authoring + paths were removed. +- Direct `RadrootsProfile` draft encoding and the identity profile publisher were + removed in favor of the validated authored-profile boundary. +- The `radroots_identity/profile` Cargo feature was removed with its embedded + legacy Profile projection. +- Replica sync no longer synthesizes Profile events from lossy stored + projections. Its transfer protocol is now version `2`, and request JSON + containing the removed `include_profiles` option is rejected. +- `RadrootsNostrClient` no longer implicitly dereferences to the upstream SDK + client. Narrow client operations and the explicit ownership bridge remain. + +### Compatibility + +- Identity JSON containing the removed embedded `profile` projection is now + rejected, including the nested public-profile form, instead of being loaded + and later rewritten without that field. + Migrate the value to a signed kind-`0` metadata event before rewriting the + identity file. +- Replica-store backups and export manifests now record a stable schema + compatibility version instead of package SemVer. Existing backups stamped by + `0.1.0-alpha.2` remain restorable because this release does not change their + stored schema. diff --git a/Cargo.lock b/Cargo.lock @@ -4279,7 +4279,7 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "radroots_authority" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_event", "radroots_nostr", @@ -4288,7 +4288,7 @@ dependencies = [ [[package]] name = "radroots_blossom" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "mediatype", @@ -4300,7 +4300,7 @@ dependencies = [ [[package]] name = "radroots_core" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "dto_bindgen", "rust_decimal", @@ -4311,7 +4311,7 @@ dependencies = [ [[package]] name = "radroots_event" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "dto_bindgen", "hex", @@ -4327,7 +4327,7 @@ dependencies = [ [[package]] name = "radroots_event_codec" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "nostr", @@ -4342,7 +4342,7 @@ dependencies = [ [[package]] name = "radroots_event_index" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "dto_bindgen", "dto_bindgen_core", @@ -4352,7 +4352,7 @@ dependencies = [ [[package]] name = "radroots_event_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_event", @@ -4369,7 +4369,7 @@ dependencies = [ [[package]] name = "radroots_geocoder" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures-executor", "hex", @@ -4385,10 +4385,9 @@ dependencies = [ [[package]] name = "radroots_identity" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", - "radroots_event", "radroots_protected_store", "radroots_runtime", "radroots_runtime_paths", @@ -4404,7 +4403,7 @@ dependencies = [ [[package]] name = "radroots_log" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chrono", "serde_json", @@ -4416,7 +4415,7 @@ dependencies = [ [[package]] name = "radroots_mesh" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_transport", "serde", @@ -4425,7 +4424,7 @@ dependencies = [ [[package]] name = "radroots_mesh_agent_client" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_mesh", "radroots_mesh_agent_proto", @@ -4436,14 +4435,14 @@ dependencies = [ [[package]] name = "radroots_mesh_agent_proto" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "sha2", ] [[package]] name = "radroots_net" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "directories", "futures", @@ -4467,7 +4466,7 @@ dependencies = [ [[package]] name = "radroots_nostr" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "nostr", @@ -4485,7 +4484,7 @@ dependencies = [ [[package]] name = "radroots_nostr_accounts" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_identity", "radroots_nostr_signer", @@ -4502,7 +4501,7 @@ dependencies = [ [[package]] name = "radroots_nostr_connect" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "nostr", "serde", @@ -4514,7 +4513,7 @@ dependencies = [ [[package]] name = "radroots_nostr_runtime" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "radroots_nostr", @@ -4524,7 +4523,7 @@ dependencies = [ [[package]] name = "radroots_nostr_signer" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "nostr", @@ -4544,7 +4543,7 @@ dependencies = [ [[package]] name = "radroots_nostrdb" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "hex", @@ -4559,7 +4558,7 @@ dependencies = [ [[package]] name = "radroots_outbox" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_event", @@ -4577,7 +4576,7 @@ dependencies = [ [[package]] name = "radroots_protected_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chacha20poly1305", "getrandom 0.2.17", @@ -4590,7 +4589,7 @@ dependencies = [ [[package]] name = "radroots_protocol_contract_v1" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "dto_bindgen", "serde", @@ -4598,7 +4597,7 @@ dependencies = [ [[package]] name = "radroots_replica_schema" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "dto_bindgen_core", "serde", @@ -4607,7 +4606,7 @@ dependencies = [ [[package]] name = "radroots_replica_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_replica_schema", @@ -4619,7 +4618,7 @@ dependencies = [ [[package]] name = "radroots_replica_sync" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "hex", @@ -4637,7 +4636,7 @@ dependencies = [ [[package]] name = "radroots_runtime" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "anyhow", "chacha20poly1305", @@ -4663,7 +4662,7 @@ dependencies = [ [[package]] name = "radroots_runtime_distribution" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "thiserror 1.0.69", @@ -4672,7 +4671,7 @@ dependencies = [ [[package]] name = "radroots_runtime_manager" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "flate2", "radroots_runtime_paths", @@ -4685,7 +4684,7 @@ dependencies = [ [[package]] name = "radroots_runtime_paths" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "thiserror 1.0.69", @@ -4693,7 +4692,7 @@ dependencies = [ [[package]] name = "radroots_runtime_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_sql_core", "serde", @@ -4703,14 +4702,14 @@ dependencies = [ [[package]] name = "radroots_secret_vault" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "keyring", ] [[package]] name = "radroots_simplex_agent_proto" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "radroots_simplex_smp_crypto", @@ -4719,7 +4718,7 @@ dependencies = [ [[package]] name = "radroots_simplex_agent_runtime" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "radroots_simplex_agent_proto", @@ -4734,7 +4733,7 @@ dependencies = [ [[package]] name = "radroots_simplex_agent_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chacha20poly1305", "getrandom 0.2.17", @@ -4752,7 +4751,7 @@ dependencies = [ [[package]] name = "radroots_simplex_app_store" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "serde", @@ -4761,7 +4760,7 @@ dependencies = [ [[package]] name = "radroots_simplex_chat_proto" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "serde", @@ -4771,7 +4770,7 @@ dependencies = [ [[package]] name = "radroots_simplex_interop_tests" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_simplex_agent_proto", "radroots_simplex_agent_runtime", @@ -4784,7 +4783,7 @@ dependencies = [ [[package]] name = "radroots_simplex_smp_crypto" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "aes-gcm", "base64 0.22.1", @@ -4804,14 +4803,14 @@ dependencies = [ [[package]] name = "radroots_simplex_smp_proto" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", ] [[package]] name = "radroots_simplex_smp_transport" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "radroots_simplex_smp_crypto", @@ -4824,7 +4823,7 @@ dependencies = [ [[package]] name = "radroots_sql_core" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "chrono", "futures-executor", @@ -4836,14 +4835,14 @@ dependencies = [ [[package]] name = "radroots_test_fixtures" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_event", ] [[package]] name = "radroots_trade" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "dto_bindgen", @@ -4866,7 +4865,7 @@ dependencies = [ [[package]] name = "radroots_trade_sp1_guest" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "serde", "serde_json", @@ -4877,7 +4876,7 @@ dependencies = [ [[package]] name = "radroots_trade_sp1_host" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "base64 0.22.1", "bincode", @@ -4896,7 +4895,7 @@ dependencies = [ [[package]] name = "radroots_transport" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "serde", @@ -4906,7 +4905,7 @@ dependencies = [ [[package]] name = "radroots_transport_nostr" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "nostr", @@ -4924,7 +4923,7 @@ dependencies = [ [[package]] name = "radroots_transport_publish_protocol" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "radroots_transport", "serde", @@ -4933,7 +4932,7 @@ dependencies = [ [[package]] name = "radroots_transport_reticulum" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "futures", "radroots_transport", @@ -8665,11 +8664,12 @@ dependencies = [ [[package]] name = "xtask" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" dependencies = [ "hex", "radroots_event_codec", "radroots_protocol_contract_v1", + "semver", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml @@ -56,7 +56,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition = "2024" rust-version = "1.97.0" license = "MIT OR Apache-2.0" @@ -67,56 +67,56 @@ readme = "README" [workspace.dependencies] dto_bindgen = { version = "0.1.0" } dto_bindgen_core = { version = "0.1.0" } -radroots_core = { path = "crates/core", version = "0.1.0-alpha.2", default-features = false } -radroots_event = { path = "crates/event", version = "0.1.0-alpha.2", default-features = false } -radroots_event_store = { path = "crates/event_store", version = "0.1.0-alpha.2", default-features = false } -radroots_event_codec = { path = "crates/event_codec", version = "0.1.0-alpha.2", default-features = false } -radroots_event_index = { path = "crates/event_index", version = "0.1.0-alpha.2", default-features = false } -radroots_authority = { path = "crates/authority", version = "0.1.0-alpha.2", default-features = false } -radroots_blossom = { path = "crates/blossom", version = "0.1.0-alpha.2", default-features = false } -radroots_geocoder = { path = "crates/geocoder", version = "0.1.0-alpha.2" } -radroots_identity = { path = "crates/identity", version = "0.1.0-alpha.2", default-features = false } -radroots_runtime_store = { path = "crates/runtime_store", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr = { path = "crates/nostr", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr_accounts = { path = "crates/nostr_accounts", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr_connect = { path = "crates/nostr_connect", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr_signer = { path = "crates/nostr_signer", version = "0.1.0-alpha.2", default-features = false } -radroots_nostrdb = { path = "crates/nostrdb", version = "0.1.0-alpha.2", default-features = false } -radroots_runtime = { path = "crates/runtime", version = "0.1.0-alpha.2", default-features = false } -radroots_runtime_paths = { path = "crates/runtime_paths", version = "0.1.0-alpha.2", default-features = false } -radroots_runtime_distribution = { path = "crates/runtime_distribution", version = "0.1.0-alpha.2", default-features = false } -radroots_runtime_manager = { path = "crates/runtime_manager", version = "0.1.0-alpha.2", default-features = false } -radroots_log = { path = "crates/log", version = "0.1.0-alpha.2", default-features = false } -radroots_net = { path = "crates/net", version = "0.1.0-alpha.2", default-features = false } -radroots_nostr_runtime = { path = "crates/nostr_runtime", version = "0.1.0-alpha.2", default-features = false } -radroots_outbox = { path = "crates/outbox", version = "0.1.0-alpha.2", default-features = false } -radroots_protocol_contract_v1 = { path = "crates/protocol_contract_v1", version = "0.1.0-alpha.2", default-features = false } -radroots_transport_publish_protocol = { path = "crates/transport_publish_protocol", version = "0.1.0-alpha.2", default-features = false } -radroots_transport_nostr = { path = "crates/transport_nostr", version = "0.1.0-alpha.2", default-features = false } -radroots_transport_reticulum = { path = "crates/transport_reticulum", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_agent_proto = { path = "crates/simplex_agent_proto", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_agent_runtime = { path = "crates/simplex_agent_runtime", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_agent_store = { path = "crates/simplex_agent_store", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_chat_proto = { path = "crates/simplex_chat_proto", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_interop_tests = { path = "crates/simplex_interop_tests", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_smp_crypto = { path = "crates/simplex_smp_crypto", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_smp_proto = { path = "crates/simplex_smp_proto", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_smp_transport = { path = "crates/simplex_smp_transport", version = "0.1.0-alpha.2", default-features = false } -radroots_sql_core = { path = "crates/sql_core", version = "0.1.0-alpha.2", default-features = false } -radroots_test_fixtures = { path = "crates/test_fixtures", version = "0.1.0-alpha.2" } -radroots_replica_schema = { path = "crates/replica_schema", version = "0.1.0-alpha.2", default-features = false } -radroots_replica_sync = { path = "crates/replica_sync", version = "0.1.0-alpha.2", default-features = false } -radroots_replica_store = { path = "crates/replica_store", version = "0.1.0-alpha.2", default-features = false } -radroots_trade = { path = "crates/trade", version = "0.1.0-alpha.2", default-features = false } -radroots_transport = { path = "crates/transport", version = "0.1.0-alpha.2", default-features = false } -radroots_mesh = { path = "crates/mesh", version = "0.1.0-alpha.2", default-features = false } -radroots_mesh_agent_client = { path = "crates/mesh_agent_client", version = "0.1.0-alpha.2", default-features = false } -radroots_mesh_agent_proto = { path = "crates/mesh_agent_proto", version = "0.1.0-alpha.2", default-features = false } -radroots_protected_store = { path = "crates/protected_store", version = "0.1.0-alpha.2", default-features = false } -radroots_secret_vault = { path = "crates/secret_vault", version = "0.1.0-alpha.2", default-features = false } -radroots_simplex_app_store = { path = "crates/simplex_app_store", version = "0.1.0-alpha.2", default-features = false } -radroots_trade_sp1_guest = { path = "crates/trade_sp1_guest", version = "0.1.0-alpha.2", default-features = false } -radroots_trade_sp1_host = { path = "crates/trade_sp1_host", version = "0.1.0-alpha.2", default-features = false } +radroots_core = { path = "crates/core", version = "=1.0.0-alpha.1", default-features = false } +radroots_event = { path = "crates/event", version = "=1.0.0-alpha.1", default-features = false } +radroots_event_store = { path = "crates/event_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_event_codec = { path = "crates/event_codec", version = "=1.0.0-alpha.1", default-features = false } +radroots_event_index = { path = "crates/event_index", version = "=1.0.0-alpha.1", default-features = false } +radroots_authority = { path = "crates/authority", version = "=1.0.0-alpha.1", default-features = false } +radroots_blossom = { path = "crates/blossom", version = "=1.0.0-alpha.1", default-features = false } +radroots_geocoder = { path = "crates/geocoder", version = "=1.0.0-alpha.1" } +radroots_identity = { path = "crates/identity", version = "=1.0.0-alpha.1", default-features = false } +radroots_runtime_store = { path = "crates/runtime_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostr = { path = "crates/nostr", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostr_accounts = { path = "crates/nostr_accounts", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostr_connect = { path = "crates/nostr_connect", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostr_signer = { path = "crates/nostr_signer", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostrdb = { path = "crates/nostrdb", version = "=1.0.0-alpha.1", default-features = false } +radroots_runtime = { path = "crates/runtime", version = "=1.0.0-alpha.1", default-features = false } +radroots_runtime_paths = { path = "crates/runtime_paths", version = "=1.0.0-alpha.1", default-features = false } +radroots_runtime_distribution = { path = "crates/runtime_distribution", version = "=1.0.0-alpha.1", default-features = false } +radroots_runtime_manager = { path = "crates/runtime_manager", version = "=1.0.0-alpha.1", default-features = false } +radroots_log = { path = "crates/log", version = "=1.0.0-alpha.1", default-features = false } +radroots_net = { path = "crates/net", version = "=1.0.0-alpha.1", default-features = false } +radroots_nostr_runtime = { path = "crates/nostr_runtime", version = "=1.0.0-alpha.1", default-features = false } +radroots_outbox = { path = "crates/outbox", version = "=1.0.0-alpha.1", default-features = false } +radroots_protocol_contract_v1 = { path = "crates/protocol_contract_v1", version = "=1.0.0-alpha.1", default-features = false } +radroots_transport_publish_protocol = { path = "crates/transport_publish_protocol", version = "=1.0.0-alpha.1", default-features = false } +radroots_transport_nostr = { path = "crates/transport_nostr", version = "=1.0.0-alpha.1", default-features = false } +radroots_transport_reticulum = { path = "crates/transport_reticulum", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_agent_proto = { path = "crates/simplex_agent_proto", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_agent_runtime = { path = "crates/simplex_agent_runtime", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_agent_store = { path = "crates/simplex_agent_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_chat_proto = { path = "crates/simplex_chat_proto", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_interop_tests = { path = "crates/simplex_interop_tests", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_smp_crypto = { path = "crates/simplex_smp_crypto", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_smp_proto = { path = "crates/simplex_smp_proto", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_smp_transport = { path = "crates/simplex_smp_transport", version = "=1.0.0-alpha.1", default-features = false } +radroots_sql_core = { path = "crates/sql_core", version = "=1.0.0-alpha.1", default-features = false } +radroots_test_fixtures = { path = "crates/test_fixtures", version = "=1.0.0-alpha.1" } +radroots_replica_schema = { path = "crates/replica_schema", version = "=1.0.0-alpha.1", default-features = false } +radroots_replica_sync = { path = "crates/replica_sync", version = "=1.0.0-alpha.1", default-features = false } +radroots_replica_store = { path = "crates/replica_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_trade = { path = "crates/trade", version = "=1.0.0-alpha.1", default-features = false } +radroots_transport = { path = "crates/transport", version = "=1.0.0-alpha.1", default-features = false } +radroots_mesh = { path = "crates/mesh", version = "=1.0.0-alpha.1", default-features = false } +radroots_mesh_agent_client = { path = "crates/mesh_agent_client", version = "=1.0.0-alpha.1", default-features = false } +radroots_mesh_agent_proto = { path = "crates/mesh_agent_proto", version = "=1.0.0-alpha.1", default-features = false } +radroots_protected_store = { path = "crates/protected_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_secret_vault = { path = "crates/secret_vault", version = "=1.0.0-alpha.1", default-features = false } +radroots_simplex_app_store = { path = "crates/simplex_app_store", version = "=1.0.0-alpha.1", default-features = false } +radroots_trade_sp1_guest = { path = "crates/trade_sp1_guest", version = "=1.0.0-alpha.1", default-features = false } +radroots_trade_sp1_host = { path = "crates/trade_sp1_host", version = "=1.0.0-alpha.1", default-features = false } anyhow = { version = "1" } aes-gcm = { version = "0.10.3", default-features = false, features = [ @@ -156,6 +156,7 @@ num_cpus = { version = "1.17.0" } poly1305 = { version = "0.8", default-features = false } secrecy = { version = "0.10.3" } salsa20 = { version = "0.10.2", default-features = false } +semver = { version = "1" } serde = { version = "1", default-features = false, features = [ "derive", "alloc", diff --git a/contracts/conformance/vectors/blossom/bud11_claims.v1.json b/contracts/conformance/vectors/blossom/bud11_claims.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_bud11_claims", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "action-get", diff --git a/contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json b/contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_bud11_nostr_adapter", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "decode-verified-upload", diff --git a/contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json b/contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_hash_path_and_descriptor", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "blossom_sha256_digest_empty_001", diff --git a/contracts/conformance/vectors/calendar/nip52_baseline.v1.json b/contracts/conformance/vectors/calendar/nip52_baseline.v1.json @@ -1,6 +1,6 @@ { "suite": "calendar_nip52_baseline", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "baseline_date_minimal_001", diff --git a/contracts/conformance/vectors/calendar/radroots_profile.v1.json b/contracts/conformance/vectors/calendar/radroots_profile.v1.json @@ -1,6 +1,6 @@ { "suite": "calendar_radroots_profile", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "profile_date_parse_minimum_gregorian_001", diff --git a/contracts/conformance/vectors/event/nip01_wire.v1.json b/contracts/conformance/vectors/event/nip01_wire.v1.json @@ -1,6 +1,6 @@ { "suite": "event", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "nip01_wire_profile_minimal_001", diff --git a/contracts/conformance/vectors/events/listing_tags_full.v1.json b/contracts/conformance/vectors/events/listing_tags_full.v1.json @@ -1,6 +1,6 @@ { "suite": "events", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "listing_tags_full_minimal_001", diff --git a/contracts/conformance/vectors/farm/build_draft.v1.json b/contracts/conformance/vectors/farm/build_draft.v1.json @@ -1,6 +1,6 @@ { "suite": "farm", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "farm_build_draft_minimal_001", diff --git a/contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json b/contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json @@ -1,6 +1,6 @@ { "suite": "knowledge_manifest_and_decode", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "knowledge_manifest_fields_valid_001", diff --git a/contracts/conformance/vectors/knowledge/public_surface.v1.json b/contracts/conformance/vectors/knowledge/public_surface.v1.json @@ -1,6 +1,6 @@ { "suite": "knowledge_public_surface", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "knowledge_source_public_surface_valid_001", diff --git a/contracts/conformance/vectors/listing/build_draft.v1.json b/contracts/conformance/vectors/listing/build_draft.v1.json @@ -1,6 +1,6 @@ { "suite": "listing", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "listing_build_draft_minimal_001", diff --git a/contracts/conformance/vectors/listing/build_tags.v1.json b/contracts/conformance/vectors/listing/build_tags.v1.json @@ -1,6 +1,6 @@ { "suite": "listing", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "listing_build_tags_minimal_001", diff --git a/contracts/conformance/vectors/listing/parse_event.v1.json b/contracts/conformance/vectors/listing/parse_event.v1.json @@ -1,6 +1,6 @@ { "suite": "listing", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "listing_parse_event_minimal_001", diff --git a/contracts/conformance/vectors/mesh/frame_cbor.v1.json b/contracts/conformance/vectors/mesh/frame_cbor.v1.json @@ -1,6 +1,6 @@ { "suite": "mesh", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "mesh_frame_cbor_default_001", diff --git a/contracts/conformance/vectors/nip46/current_session.v1.json b/contracts/conformance/vectors/nip46/current_session.v1.json @@ -1,6 +1,6 @@ { "suite": "nip46_current_session", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "nip46_connect_legacy_one_parameter_001", diff --git a/contracts/conformance/vectors/profile/build_draft.v1.json b/contracts/conformance/vectors/profile/build_draft.v1.json @@ -1,23 +0,0 @@ -{ - "suite": "profile", - "contract_version": "0.1.0", - "vectors": [ - { - "id": "profile_build_draft_minimal_001", - "kind": "profile.build_draft", - "input": { - "profile": { - "name": "radroots user" - }, - "profile_type": "farmer" - }, - "expected": { - "wire_parts": { - "kind": "profile", - "content_shape": "metadata_json", - "tags_shape": "profile_type_optional" - } - } - } - ] -} diff --git a/contracts/conformance/vectors/profile/metadata.v1.json b/contracts/conformance/vectors/profile/metadata.v1.json @@ -1,6 +1,6 @@ { "suite": "profile_metadata", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "profile_nip05_valid_root_identifier_001", diff --git a/contracts/conformance/vectors/replica_schema/json_models.v1.json b/contracts/conformance/vectors/replica_schema/json_models.v1.json @@ -1,6 +1,6 @@ { "suite": "replica_schema", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "replica_schema_farm_json_valid_001", diff --git a/contracts/conformance/vectors/social/mvp.v1.json b/contracts/conformance/vectors/social/mvp.v1.json @@ -1,6 +1,6 @@ { "suite": "social_mvp", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "social_post_tags_with_metadata_valid_001", diff --git a/contracts/conformance/vectors/social/production.v1.json b/contracts/conformance/vectors/social/production.v1.json @@ -1,6 +1,6 @@ { "suite": "social_production", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "social_repost_note_valid_001", diff --git a/contracts/conformance/vectors/social/upgraded_boundaries.v1.json b/contracts/conformance/vectors/social/upgraded_boundaries.v1.json @@ -1,6 +1,6 @@ { "suite": "social_upgraded_boundaries", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "social_listing_published_at_valid_001", diff --git a/contracts/conformance/vectors/trade/build_mutation_event.v1.json b/contracts/conformance/vectors/trade/build_mutation_event.v1.json @@ -1,6 +1,6 @@ { "suite": "trade", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_build_mutation_event_proposal_001", diff --git a/contracts/conformance/vectors/trade/canonicalize_mutation.v1.json b/contracts/conformance/vectors/trade/canonicalize_mutation.v1.json @@ -1,6 +1,6 @@ { "suite": "trade", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_canonicalize_proposal_minimal_001", diff --git a/contracts/conformance/vectors/trade/parse_listing_address.v1.json b/contracts/conformance/vectors/trade/parse_listing_address.v1.json @@ -1,6 +1,6 @@ { "suite": "trade", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_parse_listing_address_minimal_001", diff --git a/contracts/conformance/vectors/trade/parse_mutation_event.v1.json b/contracts/conformance/vectors/trade/parse_mutation_event.v1.json @@ -1,6 +1,6 @@ { "suite": "trade", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_parse_mutation_event_proposal_001", diff --git a/contracts/conformance/vectors/trade/reduce_records.v1.json b/contracts/conformance/vectors/trade/reduce_records.v1.json @@ -1,6 +1,6 @@ { "suite": "trade", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_reduce_agreed_projection_digest_001", diff --git a/contracts/conformance/vectors/trade_validation/validate_listing_event.v1.json b/contracts/conformance/vectors/trade_validation/validate_listing_event.v1.json @@ -1,6 +1,6 @@ { "suite": "trade_validation", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "trade_validation_validate_listing_event_minimal_001", diff --git a/contracts/conformance/vectors/transport/target_uri.v1.json b/contracts/conformance/vectors/transport/target_uri.v1.json @@ -1,6 +1,6 @@ { "suite": "transport", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "transport_target_uri_generic_canonical_001", diff --git a/contracts/event_boundary_matrix.md b/contracts/event_boundary_matrix.md @@ -35,7 +35,7 @@ wire parts and require runtime signing and transport. | Domain | Kind | Radroots Type | RPC Methods | Notes | | --- | --- | --- | --- | --- | -| profile | 0 | RadrootsProfile | events.profile.publish, events.profile.list, events.profile.get | compatibility RPCs retain legacy authoring; only `profile.build_authored_draft` is strict authored authority and it emits deterministic JSON with no marker tag | +| profile | 0 | RadrootsAuthoredProfile / RadrootsInboundProfileMetadata | events.profile.publish, events.profile.list, events.profile.get | publish must use `profile.build_authored_draft`; inbound projection must use `profile.parse_inbound_metadata`; authored output is deterministic JSON with no marker tag | | follow | 3 | RadrootsFollow | events.follow.publish, events.follow.list, events.follow.get | replaceable event | | post | 1 | RadrootsPost | events.post.publish, events.post.list, events.post.get | plaintext content | | comment | 1111 | RadrootsComment | events.comment.publish, events.comment.list, events.comment.get | requires root and parent tags | diff --git a/contracts/events/profile-metadata.md b/contracts/events/profile-metadata.md @@ -19,22 +19,19 @@ pinned NIP-01, NIP-05, and NIP-24 documents at NIPs commit boundary. A caller must supply content from a kind-`0` event only after the event identifier and signature have been verified. -The following authored paths remain compatibility-only. Their behavior is -preserved, but none can satisfy the strict authored operation: - -| Compatibility surface | Exclusion reason | -| --- | --- | -| `RadrootsProfile` and `profile::encode::{to_metadata, to_wire_parts, to_wire_parts_with_profile_type, profile_type_tags, profile_build_tags}` (`profile.build_draft`) | accept arbitrary media strings, model `bot` as a string, and support a Radroots marker tag | -| `radroots_nostr_build_metadata_event` and `radroots_nostr_post_metadata_event` | accept generic upstream metadata without the strict Profile typestates | -| `RadrootsNostrClient::set_metadata` | publishes generic upstream metadata directly | -| `radroots_nostr_publish_identity_profile*` and `radroots_nostr_bootstrap_service_presence` | publish the legacy identity Profile and can add the marker tag | -| `NostrClientManager::publish_profile_event_blocking` | publishes generic metadata directly through the legacy network client | -| `radroots_replica_sync` Profile draft emission | serializes stored legacy Profile fields without the strict authored model | - -New authored callers must use `profile.build_authored_draft`. Tightening or -removing the compatibility paths is a separate breaking-release decision. -Generic raw event builders and send APIs remain protocol escape hatches; their -ability to emit kind `0` does not make them Profile operation authority. +The direct legacy `RadrootsProfile` codec, Profile-specific Nostr/network +publish helpers, and replica Profile draft emission were removed in the +`1.0.0-alpha.1` breaking contract. A replica Profile row is a lossy inbound +projection and cannot prove author intent for a complete kind-`0` replacement. +New authored callers must use `profile.build_authored_draft`. Generic raw event +builders and send APIs remain protocol escape hatches; their ability to emit +kind `0` does not make them Profile operation authority. + +`RadrootsNostrClient` no longer dereferences implicitly to the upstream SDK +client, so upstream Profile conveniences are not exposed through ordinary +method resolution. `from_inner` and `into_inner` remain explicit low-level +interoperability boundaries; once a caller takes that boundary, upstream APIs +are outside the Radroots authored-operation contract. Legacy `profile::decode`, Nostr Profile adapters/fetchers, the network Profile fetch methods, and replica Profile ingest remain read-side compatibility paths. @@ -49,8 +46,8 @@ control-free `name`, consistent with the NIP-24 recommendation that `name` remain present when `display_name` is used. The scoped optional fields are `display_name`, `about`, `nip05`, `bot`, `picture`, and `banner`; `bot` is a Boolean. NIP-05 values enter only through `RadrootsNip05Identifier`. Picture and -banner values enter through `RadrootsAuthoredProfileImage`, which can wrap only -an `image/*` `RadrootsBlossomByteVerifiedDescriptor`. There is no raw-string +banner values enter through the shared `RadrootsAuthoredImage`, which can wrap +only an `image/*` `RadrootsBlossomByteVerifiedDescriptor`. There is no raw-string media setter or unchecked deserialization path. Generic `website`, `lud06`, and `lud16` strings remain outside this strict authored operation. @@ -126,7 +123,7 @@ making downstream matches exhaustive. Current stable codes are: | --- | --- | | NIP-05 syntax | `missing_separator`, `multiple_separators`, `invalid_local_part`, `invalid_domain` | | strict Profile construction | `invalid_name` | -| strict Profile image construction | `media_type_not_image` | +| shared authored image construction | `media_type_not_image` | | strict Profile encoding | `content_too_large` | | tolerant inbound parsing | `content_too_large`, `invalid_json`, `root_not_object`, `duplicate_field` | diff --git a/contracts/knowledge/knowledge_event_contract_manifest.v2.json b/contracts/knowledge/knowledge_event_contract_manifest.v2.json @@ -1,8 +1,8 @@ { "schema_version": 2, "registry_version": 1, - "radroots_event_version": "0.1.0-alpha.2", - "radroots_event_codec_version": "0.1.0-alpha.2", + "radroots_event_version": "1.0.0-alpha.1", + "radroots_event_codec_version": "1.0.0-alpha.1", "contract_count": 11, "contracts": [ { diff --git a/contracts/knowledge/knowledge_event_contract_manifest.v2.sha256 b/contracts/knowledge/knowledge_event_contract_manifest.v2.sha256 @@ -1 +1 @@ -24887216be76c310f4ba705d7e5efdca67739072c0658ec4cc7d6393fb83075f +7e73cf5df737f6fd3cdb8c57fc198d8d3f1758585f9437b26972d908ec432f05 diff --git a/contracts/manifest.toml b/contracts/manifest.toml @@ -1,6 +1,6 @@ [contract] name = "radroots_core_contract" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" source = "rust" [surface] diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -1,6 +1,6 @@ [contract] name = "radroots_core_contract" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" source = "rust" [public] @@ -49,13 +49,10 @@ public = [ "RadrootsListingAddress", "RadrootsAuthoredImage", "RadrootsAuthoredImageError", - "RadrootsProfile", "RadrootsNip05Identifier", "RadrootsNip05IdentifierError", "RadrootsAuthoredProfile", "RadrootsAuthoredProfileError", - "RadrootsAuthoredProfileImage", - "RadrootsAuthoredProfileImageError", "RadrootsAuthoredProfileEncodeError", "RadrootsInboundProfileMetadata", "RadrootsProfileMetadataParseError", @@ -344,24 +341,6 @@ rust_types = [ [operations.blossom_decode_verify_authorization_header.conformance] vector = "contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json" -[operations.profile_build_draft] -domain = "profile" -id = "profile.build_draft" -stability = "beta" -inputs = ["RadrootsProfile", "RadrootsProfileType?"] -outputs = ["RadrootsNip01EventWireParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.profile_build_draft.implementation] -rust_modules = ["crates/event_codec/src/profile/encode.rs"] -rust_types = ["radroots_event::profile::RadrootsProfile"] - -[operations.profile_build_draft.conformance] -vector = "contracts/conformance/vectors/profile/build_draft.v1.json" - [operations.profile_build_authored_draft] domain = "profile" id = "profile.build_authored_draft" @@ -380,10 +359,10 @@ rust_modules = [ ] rust_types = [ "radroots_blossom::RadrootsBlossomByteVerifiedDescriptor", + "radroots_event::media::RadrootsAuthoredImage", + "radroots_event::media::RadrootsAuthoredImageError", "radroots_event::profile::RadrootsAuthoredProfile", "radroots_event::profile::RadrootsAuthoredProfileError", - "radroots_event::profile::RadrootsAuthoredProfileImage", - "radroots_event::profile::RadrootsAuthoredProfileImageError", "radroots_event::profile::RadrootsNip05Identifier", "radroots_event::profile::RadrootsNip05IdentifierError", "radroots_event_codec::profile::authored::RadrootsAuthoredProfileEncodeError", diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -0,0 +1,79 @@ +schema_version = 1 + +[release] +version = "1.0.0-alpha.1" +previous_version = "0.1.0-alpha.2" +contract_base_version = "1.0.0" +status = "unreleased" + +[artifacts] +changelog = "CHANGELOG.md" +manifest = "contracts/manifest.toml" +operations = "contracts/operations.toml" +replica = "contracts/replica.toml" +conformance = "contracts/conformance" + +[[changes]] +id = "calendar-nip52-typestate" +classification = "breaking" +semver_impacts = [ + "remove_exported_type", + "remove_exported_field", + "remove_exported_function", + "change_exported_field_type", + "change_exported_algorithm_behavior", +] +summary = "Replace legacy calendar authoring models with authored, parsed, and admitted NIP-52 states." + +[[changes]] +id = "authored-blossom-media-proof" +classification = "breaking" +semver_impacts = ["remove_exported_type", "change_exported_field_type"] +summary = "Require byte-verified Blossom image proofs at strict profile and calendar authoring boundaries." + +[[changes]] +id = "legacy-profile-authoring-removal" +classification = "breaking" +semver_impacts = [ + "remove_exported_type", + "remove_exported_field", + "remove_exported_function", + "remove_exported_module", + "remove_exported_trait_impl", + "remove_public_cargo_feature", + "change_exported_enum_variant", + "change_exported_algorithm_behavior", +] +summary = "Remove direct profile draft encoding, the radroots_identity profile Cargo feature, and identity publishing paths that bypass authored validation; reject identity files containing the retired embedded profile projection." + +[[changes]] +id = "nostr-client-boundary" +classification = "breaking" +semver_impacts = ["remove_exported_trait_impl"] +summary = "Remove implicit Deref access to the upstream Nostr SDK client while retaining narrow operations and an explicit ownership bridge." + +[[changes]] +id = "replica-profile-compatibility" +classification = "breaking" +semver_impacts = [ + "remove_exported_field", + "remove_exported_function", + "change_exported_constant_value", + "change_exported_algorithm_behavior", +] +summary = "Stop synthesizing lossy Profile events from replica projections, remove the include_profiles sync option, reject obsolete request fields, and advance the transfer contract to version 2." + +[[changes]] +id = "replica-backup-schema-version" +classification = "breaking" +semver_impacts = [ + "remove_exported_constant", + "change_exported_algorithm_behavior", +] +summary = "Decouple replica backup compatibility from package SemVer and retain restore support for schema-compatible 0.1.0-alpha.2 backups." + +[[changes]] +id = "workspace-version-lockstep" +classification = "fix" +semver_impacts = ["fix_packaging_metadata"] +summary = "Move the workspace and event contract to the 1.0.0-alpha.1 version cohort with exact internal requirements." diff --git a/contracts/replica.toml b/contracts/replica.toml @@ -1,6 +1,8 @@ -[replica] +schema_version = 1 + +[contract] name = "radroots_replica_contract" -version = "0.1.0" +version = "1.0.0-alpha.1" purpose = "offline-first local replica state and deterministic sync" [crate_family] @@ -12,3 +14,10 @@ sync = "radroots_replica_sync" transport_agnostic_sync_core = true deterministic_emit_and_ingest = true forbid_legacy_alias_identifiers = true +profile_event_emission = "excluded" +unknown_sync_request_fields = "reject" + +[transfer] +version = 2 +source = "crates/replica_sync/src/types.rs" +constant = "RADROOTS_REPLICA_TRANSFER_VERSION" diff --git a/contracts/version.toml b/contracts/version.toml @@ -1,13 +1,19 @@ [contract] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" stability = "draft" [semver] major_on = [ "remove_exported_type", "remove_exported_field", + "remove_exported_function", + "remove_exported_module", + "remove_exported_constant", + "remove_exported_trait_impl", + "remove_public_cargo_feature", "change_exported_field_type", "change_exported_enum_variant", + "change_exported_constant_value", "change_exported_algorithm_behavior", "rename_internal_replica_crate_family", ] diff --git a/crates/authority/Cargo.toml b/crates/authority/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_authority" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/blossom/Cargo.toml b/crates/blossom/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_blossom" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/blossom/tests/bud11_conformance.rs b/crates/blossom/tests/bud11_conformance.rs @@ -34,7 +34,7 @@ fn checked_in_bud11_vectors_execute_against_public_api() { let vectors = conformance_vectors(); let suite: Suite = serde_json::from_str(&vectors).expect("BUD-11 vectors must parse"); assert_eq!(suite.suite, "blossom_bud11_claims"); - assert_eq!(suite.contract_version, "0.1.0"); + assert_eq!(suite.contract_version, "1.0.0"); assert!(!suite.vectors.is_empty()); for vector in &suite.vectors { diff --git a/crates/blossom/tests/conformance.rs b/crates/blossom/tests/conformance.rs @@ -31,7 +31,7 @@ fn checked_in_vectors_execute_against_public_api() { let vectors = conformance_vectors(); let suite: Suite = serde_json::from_str(&vectors).expect("Blossom vectors must parse"); assert_eq!(suite.suite, "blossom_hash_path_and_descriptor"); - assert_eq!(suite.contract_version, "0.1.0"); + assert_eq!(suite.contract_version, "1.0.0"); assert!(!suite.vectors.is_empty()); for vector in &suite.vectors { diff --git a/crates/blossom/tests/fixtures/bud11_claims.v1.json b/crates/blossom/tests/fixtures/bud11_claims.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_bud11_claims", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "action-get", diff --git a/crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json b/crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_hash_path_and_descriptor", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "blossom_sha256_digest_empty_001", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_core" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/event/Cargo.toml b/crates/event/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_event" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/event/README b/crates/event/README @@ -14,11 +14,12 @@ models, kinds, and tag conventions for the `radroots` core libraries. * portable event model semantics for both `std` and `no_std` builds; * optional integration with `serde` for serialization. -The Profile module also exposes an additive strict authored model. It requires -a non-whitespace, control-free name; its media fields accept only image-typed, +The Profile module exposes the exclusive strict authored model. It requires a +non-whitespace, control-free name; its media fields accept only image-typed, byte-verified Blossom descriptors; and its NIP-05 identifier type validates -syntax without making a network identity claim. The legacy `RadrootsProfile` -model remains available for compatibility. +syntax without making a network identity claim. The legacy read projection is +not serializable or exported as a DTO. Tolerant reads use +`RadrootsInboundProfileMetadata` from `radroots_event_codec`. The calendar module keeps three different states explicit for NIP-52 kinds `31922`, `31923`, `31924`, and `31925`: the complete structural event envelope, diff --git a/crates/event/src/contract.rs b/crates/event/src/contract.rs @@ -1822,7 +1822,7 @@ static ALL_EVENT_CONTRACTS: &[RadrootsEventContract] = &[ "radroots.profile.metadata.v1", KIND_PROFILE, "Profile Metadata", - "RadrootsProfile", + "RadrootsAuthoredProfile / RadrootsInboundProfileMetadata", RadrootsEventClass::Replaceable, RadrootsEventPrivacy::Public, RadrootsActorRole::Any, diff --git a/crates/event/src/generated/dto_roots.rs b/crates/event/src/generated/dto_roots.rs @@ -138,7 +138,6 @@ pub fn dto_bindgen_roots() -> ::std::vec::Vec<::dto_bindgen::export::RootDescrip ::dto_bindgen::export::RootDescriptor::new::<crate::plot::RadrootsPlotLocation>(), ::dto_bindgen::export::RootDescriptor::new::<crate::plot::RadrootsPlotRef>(), ::dto_bindgen::export::RootDescriptor::new::<crate::post::RadrootsPost>(), - ::dto_bindgen::export::RootDescriptor::new::<crate::profile::RadrootsProfile>(), ::dto_bindgen::export::RootDescriptor::new::<crate::profile::RadrootsProfileType>(), ::dto_bindgen::export::RootDescriptor::new::<crate::reaction::RadrootsReaction>(), ::dto_bindgen::export::RootDescriptor::new::<crate::relay_document::RadrootsRelayDocument>( diff --git a/crates/event/src/profile.rs b/crates/event/src/profile.rs @@ -2,7 +2,7 @@ use alloc::string::String; use core::{fmt, str::FromStr}; -use radroots_blossom::RadrootsBlossomByteVerifiedDescriptor; +use crate::media::RadrootsAuthoredImage; pub const RADROOTS_PROFILE_TYPE_TAG_KEY: &str = "t"; pub const RADROOTS_PROFILE_TYPE_TAG_INDIVIDUAL: &str = "radroots:type:individual"; @@ -55,16 +55,13 @@ pub fn radroots_profile_type_from_tag_value(value: &str) -> Option<RadrootsProfi } } -#[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] -#[cfg_attr(feature = "dto-bindgen", dto(export))] -#[cfg_attr( - any(feature = "serde", test), - derive(serde::Serialize, serde::Deserialize) -)] -/// Compatibility-only Profile model used by legacy codecs and runtimes. +#[cfg_attr(any(feature = "serde", test), derive(serde::Deserialize))] +/// Read-only compatibility projection used by legacy inbound codecs. /// -/// Its media fields are arbitrary strings and its `bot` field is not a JSON -/// Boolean. New strict Profile authoring must use `RadrootsAuthoredProfile`. +/// This type deliberately has no serializer or DTO export. Its media fields are +/// unverified strings and its `bot` field is not a JSON Boolean, so it must not +/// cross an authored or publication boundary. New reads use the tolerant +/// inbound metadata model; new authoring must use `RadrootsAuthoredProfile`. #[derive(Clone, Debug)] pub struct RadrootsProfile { pub name: String, @@ -205,63 +202,6 @@ fn valid_nip05_domain(domain: &str) -> bool { #[non_exhaustive] #[derive(Clone, Debug, PartialEq, Eq)] -pub enum RadrootsAuthoredProfileImageError { - MediaTypeNotImage, -} - -impl RadrootsAuthoredProfileImageError { - pub const fn code(&self) -> &'static str { - match self { - Self::MediaTypeNotImage => "media_type_not_image", - } - } -} - -impl fmt::Display for RadrootsAuthoredProfileImageError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MediaTypeNotImage => { - f.write_str("Profile media descriptor must have an image media type") - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for RadrootsAuthoredProfileImageError {} - -/// A byte-verified Blossom descriptor whose declared media type is `image/*`. -/// -/// This typestate does not inspect or sanitize the image format. Media runtimes -/// remain responsible for decode and format-safety policy. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsAuthoredProfileImage(RadrootsBlossomByteVerifiedDescriptor); - -impl RadrootsAuthoredProfileImage { - pub fn try_from_verified_descriptor( - descriptor: RadrootsBlossomByteVerifiedDescriptor, - ) -> Result<Self, RadrootsAuthoredProfileImageError> { - if !descriptor.media_type().as_str().starts_with("image/") { - return Err(RadrootsAuthoredProfileImageError::MediaTypeNotImage); - } - Ok(Self(descriptor)) - } - - pub fn descriptor(&self) -> &RadrootsBlossomByteVerifiedDescriptor { - &self.0 - } -} - -impl TryFrom<RadrootsBlossomByteVerifiedDescriptor> for RadrootsAuthoredProfileImage { - type Error = RadrootsAuthoredProfileImageError; - - fn try_from(value: RadrootsBlossomByteVerifiedDescriptor) -> Result<Self, Self::Error> { - Self::try_from_verified_descriptor(value) - } -} - -#[non_exhaustive] -#[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsAuthoredProfileError { InvalidName, } @@ -305,8 +245,8 @@ pub struct RadrootsAuthoredProfile { name: String, display_name: Option<String>, about: Option<String>, - picture: Option<RadrootsAuthoredProfileImage>, - banner: Option<RadrootsAuthoredProfileImage>, + picture: Option<RadrootsAuthoredImage>, + banner: Option<RadrootsAuthoredImage>, nip05: Option<RadrootsNip05Identifier>, bot: Option<bool>, } @@ -341,13 +281,13 @@ impl RadrootsAuthoredProfile { } #[must_use] - pub fn with_picture(mut self, value: RadrootsAuthoredProfileImage) -> Self { + pub fn with_picture(mut self, value: RadrootsAuthoredImage) -> Self { self.picture = Some(value); self } #[must_use] - pub fn with_banner(mut self, value: RadrootsAuthoredProfileImage) -> Self { + pub fn with_banner(mut self, value: RadrootsAuthoredImage) -> Self { self.banner = Some(value); self } @@ -376,11 +316,11 @@ impl RadrootsAuthoredProfile { self.about.as_deref() } - pub fn picture(&self) -> Option<&RadrootsAuthoredProfileImage> { + pub fn picture(&self) -> Option<&RadrootsAuthoredImage> { self.picture.as_ref() } - pub fn banner(&self) -> Option<&RadrootsAuthoredProfileImage> { + pub fn banner(&self) -> Option<&RadrootsAuthoredImage> { self.banner.as_ref() } @@ -397,8 +337,8 @@ impl RadrootsAuthoredProfile { mod tests { use super::*; use radroots_blossom::{ - RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomMediaType, - RadrootsBlossomSha256, + RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, + RadrootsBlossomByteVerifiedDescriptor, RadrootsBlossomMediaType, RadrootsBlossomSha256, }; #[test] @@ -543,8 +483,7 @@ mod tests { #[test] fn authored_profile_requires_name_and_image_only_verified_media() { let media = - RadrootsAuthoredProfileImage::try_from(verified_descriptor("image/webp", "webp")) - .unwrap(); + RadrootsAuthoredImage::try_from(verified_descriptor("image/webp", "webp")).unwrap(); let profile = RadrootsAuthoredProfile::new("alice") .unwrap() .with_display_name("Alice") @@ -580,9 +519,11 @@ mod tests { ); let error = - RadrootsAuthoredProfileImage::try_from(verified_descriptor("text/plain", "txt")) - .unwrap_err(); - assert_eq!(error, RadrootsAuthoredProfileImageError::MediaTypeNotImage); + RadrootsAuthoredImage::try_from(verified_descriptor("text/plain", "txt")).unwrap_err(); + assert_eq!( + error, + crate::media::RadrootsAuthoredImageError::MediaTypeNotImage + ); assert_eq!(error.code(), "media_type_not_image"); assert!(!error.to_string().is_empty()); diff --git a/crates/event_codec/Cargo.toml b/crates/event_codec/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_event_codec" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/event_codec/src/manifest.rs b/crates/event_codec/src/manifest.rs @@ -18,6 +18,23 @@ use sha2::{Digest, Sha256}; pub const RADROOTS_KNOWLEDGE_CONTRACT_MANIFEST_SCHEMA_VERSION: u32 = 2; +const HISTORICAL_KNOWLEDGE_CONTRACT_INTRODUCTIONS: [(&str, &str); 11] = [ + ("radroots.knowledge.change_proposal.v1", "0.1.0-alpha.2"), + ("radroots.knowledge.claim.v1", "0.1.0-alpha.2"), + ( + "radroots.knowledge.contribution_attestation.v1", + "0.1.0-alpha.2", + ), + ("radroots.knowledge.evidence_bounty.v1", "0.1.0-alpha.2"), + ("radroots.knowledge.field_report.v1", "0.1.0-alpha.2"), + ("radroots.knowledge.relation.v1", "0.1.0-alpha.2"), + ("radroots.knowledge.review.v1", "0.1.0-alpha.2"), + ("radroots.knowledge.source.v1", "0.1.0-alpha.2"), + ("radroots.wiki.article.v1", "0.1.0-alpha.2"), + ("radroots.wiki.merge_request.v1", "0.1.0-alpha.2"), + ("radroots.wiki.redirect.v1", "0.1.0-alpha.2"), +]; + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct RadrootsKnowledgeContractManifest { pub schema_version: u32, @@ -168,10 +185,17 @@ fn manifest_entry(contract: &RadrootsEventContract) -> RadrootsKnowledgeContract wasm_verified_decode_support: true, deprecated: false, replaced_by: None, - introduced_at: env!("CARGO_PKG_VERSION").to_string(), + introduced_at: knowledge_contract_introduced_at(contract.id).to_string(), } } +fn knowledge_contract_introduced_at(contract_id: &str) -> &'static str { + HISTORICAL_KNOWLEDGE_CONTRACT_INTRODUCTIONS + .iter() + .find_map(|(historical_id, version)| (*historical_id == contract_id).then_some(*version)) + .unwrap_or(env!("CARGO_PKG_VERSION")) +} + fn mvp_sdk_and_wasm_tag_support(contract_id: &str) -> bool { matches!( contract_id, diff --git a/crates/event_codec/src/profile/encode.rs b/crates/event_codec/src/profile/encode.rs @@ -1,113 +0,0 @@ -use crate::profile::error::ProfileEncodeError; -#[cfg(feature = "serde_json")] -use radroots_event::kinds::KIND_PROFILE; -use radroots_event::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_tag_value, -}; - -use nostr::Metadata; -use nostr::prelude::Url; - -#[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; - -#[cfg(feature = "serde_json")] -use radroots_event::wire::RadrootsNip01EventWireParts; - -fn push_tag(tags: &mut Vec<Vec<String>>, key: &str, value: &str) { - tags.push(vec![key.to_string(), value.to_string()]); -} - -/// Builds the legacy Radroots Profile marker tag. -/// -/// This compatibility helper is not part of the strict authored Profile -/// operation. -pub fn profile_type_tags(profile_type: RadrootsProfileType) -> Vec<Vec<String>> { - let mut tags = Vec::with_capacity(1); - push_tag( - &mut tags, - RADROOTS_PROFILE_TYPE_TAG_KEY, - radroots_profile_type_tag_value(profile_type), - ); - tags -} - -/// Builds optional legacy Radroots Profile marker tags. -/// -/// This compatibility helper is not part of the strict authored Profile -/// operation. -pub fn profile_build_tags(profile_type: Option<RadrootsProfileType>) -> Vec<Vec<String>> { - match profile_type { - Some(value) => profile_type_tags(value), - None => Vec::new(), - } -} - -/// Converts the legacy Profile model to generic Nostr metadata. -/// -/// This compatibility API accepts arbitrary media strings and does not satisfy -/// the strict authored Profile contract. -pub fn to_metadata(p: &RadrootsProfile) -> Result<Metadata, ProfileEncodeError> { - let mut md = Metadata::new().name(p.name.clone()); - - if let Some(s) = &p.display_name { - md = md.display_name(s.clone()); - } - if let Some(s) = &p.about { - md = md.about(s.clone()); - } - if let Some(s) = &p.website { - let u = Url::parse(s).map_err(|_| ProfileEncodeError::InvalidUrl("website", s.clone()))?; - md = md.website(u); - } - if let Some(s) = &p.picture { - let u = Url::parse(s).map_err(|_| ProfileEncodeError::InvalidUrl("picture", s.clone()))?; - md = md.picture(u); - } - if let Some(s) = &p.banner { - let u = Url::parse(s).map_err(|_| ProfileEncodeError::InvalidUrl("banner", s.clone()))?; - md = md.banner(u); - } - if let Some(s) = &p.nip05 { - md = md.nip05(s.clone()); - } - if let Some(s) = &p.lud06 { - md = md.lud06(s.clone()); - } - if let Some(s) = &p.lud16 { - md = md.lud16(s.clone()); - } - - Ok(md) -} - -#[cfg(feature = "serde_json")] -/// Encodes the legacy Profile model without a marker tag. -/// -/// This compatibility API does not satisfy the strict authored Profile media -/// contract. New authored callers must use `profile.build_authored_draft`. -pub fn to_wire_parts( - p: &RadrootsProfile, -) -> Result<RadrootsNip01EventWireParts, ProfileEncodeError> { - to_wire_parts_with_profile_type(p, None) -} - -#[cfg(feature = "serde_json")] -/// Encodes the legacy Profile model with an optional marker tag. -/// -/// This compatibility API does not satisfy the strict authored Profile media -/// contract. New authored callers must use `profile.build_authored_draft`. -pub fn to_wire_parts_with_profile_type( - p: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, -) -> Result<RadrootsNip01EventWireParts, ProfileEncodeError> { - let md = to_metadata(p)?; - let content = serde_json::to_string(&md).map_err(|_| ProfileEncodeError::Json)?; - let tags = profile_build_tags(profile_type); - Ok(RadrootsNip01EventWireParts { - kind: KIND_PROFILE, - content, - tags, - }) -} diff --git a/crates/event_codec/src/profile/error.rs b/crates/event_codec/src/profile/error.rs @@ -1,22 +0,0 @@ -use core::fmt; - -#[cfg(not(feature = "std"))] -use alloc::string::String; - -#[derive(Debug)] -pub enum ProfileEncodeError { - InvalidUrl(&'static str, String), - Json, -} - -impl fmt::Display for ProfileEncodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidUrl(field, val) => write!(f, "invalid URL for {}: {}", field, val), - Self::Json => write!(f, "failed to serialize metadata JSON"), - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ProfileEncodeError {} diff --git a/crates/event_codec/src/profile/mod.rs b/crates/event_codec/src/profile/mod.rs @@ -3,21 +3,16 @@ extern crate alloc; use radroots_event::profile::{RadrootsProfile, RadrootsProfileType}; -pub mod error; - #[cfg(feature = "serde_json")] pub mod authored; -#[cfg(feature = "nostr")] -pub mod encode; - #[cfg(feature = "serde_json")] pub mod decode; #[cfg(feature = "serde_json")] pub mod inbound; -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] #[derive(Clone, Debug)] pub struct RadrootsProfileData { pub profile_type: Option<RadrootsProfileType>, diff --git a/crates/event_codec/src/tag_builders.rs b/crates/event_codec/src/tag_builders.rs @@ -3,8 +3,6 @@ #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; -use core::convert::Infallible; - #[cfg(feature = "knowledge")] use radroots_event::knowledge::{ RadrootsContributionAttestation, RadrootsEvidenceBounty, RadrootsKnowledgeChangeProposal, @@ -19,7 +17,7 @@ use radroots_event::{ job_request::RadrootsJobRequest, job_result::RadrootsJobResult, list::RadrootsList, list_set::RadrootsListSet, listing::RadrootsListing, message::RadrootsMessage, message_file::RadrootsMessageFile, plot::RadrootsPlot, post::RadrootsPost, - profile::RadrootsProfile, reaction::RadrootsReaction, resource_area::RadrootsResourceArea, + reaction::RadrootsReaction, resource_area::RadrootsResourceArea, resource_cap::RadrootsResourceHarvestCap, seal::RadrootsSeal, }; @@ -232,14 +230,6 @@ impl RadrootsEventTagBuilder for RadrootsGiftWrap { } } -impl RadrootsEventTagBuilder for RadrootsProfile { - type Error = Infallible; - - fn build_tags(&self) -> Result<Vec<Vec<String>>, Self::Error> { - Ok(Vec::new()) - } -} - impl RadrootsEventTagBuilder for RadrootsPost { type Error = EventEncodeError; diff --git a/crates/event_codec/tests/calendar_conformance.rs b/crates/event_codec/tests/calendar_conformance.rs @@ -79,7 +79,7 @@ const PROFILE_VECTOR_KIND_COUNTS: &[(&str, usize)] = &[ fn checked_in_baseline_vectors_execute_against_tolerant_nip52_parsers() { let suite = conformance_suite(WORKSPACE_BASELINE_PATH, PACKAGED_BASELINE); assert_eq!(suite["suite"], "calendar_nip52_baseline"); - assert_eq!(suite["contract_version"], "0.1.0"); + assert_eq!(suite["contract_version"], "1.0.0"); let vectors = suite["vectors"].as_array().expect("baseline vectors"); assert_vector_kind_inventory(vectors, BASELINE_VECTOR_KIND_COUNTS); for vector in vectors { @@ -91,7 +91,7 @@ fn checked_in_baseline_vectors_execute_against_tolerant_nip52_parsers() { fn checked_in_profile_vectors_execute_against_authored_and_admission_apis() { let suite = conformance_suite(WORKSPACE_PROFILE_PATH, PACKAGED_PROFILE); assert_eq!(suite["suite"], "calendar_radroots_profile"); - assert_eq!(suite["contract_version"], "0.1.0"); + assert_eq!(suite["contract_version"], "1.0.0"); let vectors = suite["vectors"].as_array().expect("profile vectors"); assert_vector_kind_inventory(vectors, PROFILE_VECTOR_KIND_COUNTS); for vector in vectors { diff --git a/crates/event_codec/tests/codec_error_job.rs b/crates/event_codec/tests/codec_error_job.rs @@ -9,7 +9,6 @@ use radroots_event_codec::job::encode::{ push_status_tag, }; use radroots_event_codec::job::error::JobParseError; -use radroots_event_codec::profile::error::ProfileEncodeError; #[cfg(feature = "serde_json")] use serde::Serialize; #[cfg(feature = "serde_json")] @@ -175,15 +174,3 @@ fn job_parse_error_display_and_source_covers_variants() { assert_eq!(missing_chain.to_string(), "missing required chain tag: e"); assert!(missing_chain.source().is_none()); } - -#[test] -fn profile_encode_error_display_covers_variants() { - let invalid = ProfileEncodeError::InvalidUrl("website", "ftp://example.com".to_string()); - assert_eq!( - invalid.to_string(), - "invalid URL for website: ftp://example.com" - ); - - let json = ProfileEncodeError::Json; - assert_eq!(json.to_string(), "failed to serialize metadata JSON"); -} diff --git a/crates/event_codec/tests/fixtures/calendar_nip52_baseline.v1.json b/crates/event_codec/tests/fixtures/calendar_nip52_baseline.v1.json @@ -1,6 +1,6 @@ { "suite": "calendar_nip52_baseline", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "baseline_date_minimal_001", diff --git a/crates/event_codec/tests/fixtures/calendar_radroots_profile.v1.json b/crates/event_codec/tests/fixtures/calendar_radroots_profile.v1.json @@ -1,6 +1,6 @@ { "suite": "calendar_radroots_profile", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "profile_date_parse_minimum_gregorian_001", diff --git a/crates/event_codec/tests/fixtures/profile_metadata.v1.json b/crates/event_codec/tests/fixtures/profile_metadata.v1.json @@ -1,6 +1,6 @@ { "suite": "profile_metadata", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "profile_nip05_valid_root_identifier_001", diff --git a/crates/event_codec/tests/profile_conformance.rs b/crates/event_codec/tests/profile_conformance.rs @@ -3,10 +3,12 @@ use std::{borrow::Cow, fs, path::Path}; use radroots_blossom::{RadrootsBlossomBlobDescriptor, RadrootsBlossomByteVerifiedDescriptor}; -use radroots_event::profile::{ - RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES, RadrootsAuthoredProfile, - RadrootsAuthoredProfileError, RadrootsAuthoredProfileImage, RadrootsAuthoredProfileImageError, - RadrootsNip05Identifier, +use radroots_event::{ + media::{RadrootsAuthoredImage, RadrootsAuthoredImageError}, + profile::{ + RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES, RadrootsAuthoredProfile, + RadrootsAuthoredProfileError, RadrootsNip05Identifier, + }, }; use radroots_event_codec::profile::{ authored::{RadrootsAuthoredProfileEncodeError, authored_profile_to_wire_parts}, @@ -58,7 +60,7 @@ fn checked_in_profile_vectors_execute_against_strict_and_tolerant_public_apis() let vectors = conformance_vectors(); let suite: Suite = serde_json::from_str(&vectors).expect("Profile vectors must parse"); assert_eq!(suite.suite, "profile_metadata"); - assert_eq!(suite.contract_version, "0.1.0"); + assert_eq!(suite.contract_version, "1.0.0"); assert!(!suite.vectors.is_empty()); for vector in &suite.vectors { @@ -205,9 +207,9 @@ fn authored_image_invalid(vector: &Vector) { let input: AuthoredMediaInput = serde_json::from_value(vector.input["media"].clone()) .unwrap_or_else(|error| panic!("{} media fixture failed: {error}", vector.id)); let descriptor = verified_descriptor(&input, &vector.id); - let error = RadrootsAuthoredProfileImage::try_from(descriptor) - .expect_err("non-image Profile media must fail"); - assert_eq!(error, RadrootsAuthoredProfileImageError::MediaTypeNotImage); + let error = + RadrootsAuthoredImage::try_from(descriptor).expect_err("non-image Profile media must fail"); + assert_eq!(error, RadrootsAuthoredImageError::MediaTypeNotImage); assert_eq!(error.code(), expected_str(vector, "error")); } @@ -259,8 +261,8 @@ fn authored_profile(input: &Value, vector_id: &str) -> RadrootsAuthoredProfile { profile } -fn authored_image(input: &AuthoredMediaInput, vector_id: &str) -> RadrootsAuthoredProfileImage { - RadrootsAuthoredProfileImage::try_from(verified_descriptor(input, vector_id)) +fn authored_image(input: &AuthoredMediaInput, vector_id: &str) -> RadrootsAuthoredImage { + RadrootsAuthoredImage::try_from(verified_descriptor(input, vector_id)) .unwrap_or_else(|error| panic!("{vector_id} Profile image failed: {error}")) } diff --git a/crates/event_codec/tests/profile_encode.rs b/crates/event_codec/tests/profile_encode.rs @@ -1,76 +0,0 @@ -#![cfg(all(feature = "nostr", feature = "serde_json"))] - -use radroots_event::{ - kinds::KIND_PROFILE, - profile::{ - RADROOTS_PROFILE_TYPE_TAG_FARM, RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, - RadrootsProfileType, - }, -}; -use radroots_event_codec::profile::encode::{ - to_metadata, to_wire_parts, to_wire_parts_with_profile_type, -}; -use radroots_event_codec::profile::error::ProfileEncodeError; -use serde_json::Value; - -#[test] -fn profile_to_metadata_rejects_invalid_url() { - let profile = RadrootsProfile { - name: "alice".to_string(), - display_name: None, - nip05: None, - about: None, - website: Some("not-a-url".to_string()), - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; - - let err = to_metadata(&profile).unwrap_err(); - assert!(matches!(err, ProfileEncodeError::InvalidUrl("website", _))); -} - -#[test] -fn profile_to_wire_parts_writes_json_content() { - let profile = RadrootsProfile { - name: "alice".to_string(), - display_name: Some("Alice".to_string()), - nip05: None, - about: None, - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; - - let parts = to_wire_parts(&profile).unwrap(); - assert_eq!(parts.kind, KIND_PROFILE); - - let value: Value = serde_json::from_str(&parts.content).unwrap(); - assert_eq!(value.get("name").and_then(|v| v.as_str()), Some("alice")); -} - -#[test] -fn profile_to_wire_parts_with_profile_type_sets_tag() { - let profile = RadrootsProfile { - name: "farm".to_string(), - display_name: None, - nip05: None, - about: None, - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; - - let parts = to_wire_parts_with_profile_type(&profile, Some(RadrootsProfileType::Farm)).unwrap(); - assert!(parts.tags.iter().any(|tag| tag.first().map(|v| v.as_str()) - == Some(RADROOTS_PROFILE_TYPE_TAG_KEY) - && tag.get(1).map(|v| v.as_str()) == Some(RADROOTS_PROFILE_TYPE_TAG_FARM))); -} diff --git a/crates/event_codec/tests/tag_builders.rs b/crates/event_codec/tests/tag_builders.rs @@ -35,7 +35,6 @@ use radroots_event::message::{RadrootsMessage, RadrootsMessageRecipient}; use radroots_event::message_file::RadrootsMessageFile; use radroots_event::plot::{RadrootsPlot, RadrootsPlotRef}; use radroots_event::post::RadrootsPost; -use radroots_event::profile::RadrootsProfile; use radroots_event::reaction::RadrootsReaction; use radroots_event::resource_area::{ RadrootsResourceArea, RadrootsResourceAreaLocation, RadrootsResourceAreaRef, @@ -434,20 +433,6 @@ fn event_tag_builder_impls_build_tags_for_all_supported_types() { }; assert!(!gift_wrap.build_tags().unwrap().is_empty()); - let profile = RadrootsProfile { - name: "alice".to_string(), - display_name: None, - nip05: None, - about: None, - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; - assert!(profile.build_tags().unwrap().is_empty()); - let post = RadrootsPost { content: "hello".to_string(), farm: None, diff --git a/crates/event_index/Cargo.toml b/crates/event_index/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_event_index" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/event_store/Cargo.toml b/crates/event_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_event_store" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/geocoder/Cargo.toml b/crates/geocoder/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_geocoder" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_identity" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true @@ -13,13 +13,12 @@ documentation = "https://docs.rs/radroots_identity" readme = "README" [features] -default = ["std", "profile", "json-file", "nip49"] +default = ["std", "json-file", "nip49"] std = [ "dep:radroots_protected_store", "dep:radroots_runtime_paths", "dep:radroots_secret_vault", ] -profile = ["dep:radroots_event"] json-file = ["std", "dep:radroots_runtime"] nip49 = ["std", "nostr/nip49"] secrecy = ["dep:secrecy"] @@ -34,9 +33,6 @@ radroots_runtime_paths = { workspace = true, optional = true } radroots_secret_vault = { workspace = true, optional = true, features = [ "std", ] } -radroots_event = { workspace = true, optional = true, default-features = false, features = [ - "serde", -] } nostr = { workspace = true } secrecy = { workspace = true, optional = true } serde = { workspace = true } diff --git a/crates/identity/src/identity.rs b/crates/identity/src/identity.rs @@ -7,8 +7,6 @@ use nostr::{ nips::nip19::{FromBech32, ToBech32}, nips::nip49::{EncryptedSecretKey, KeySecurity}, }; -#[cfg(feature = "profile")] -use radroots_event::profile::RadrootsProfile; use serde::{Deserialize, Serialize}; #[cfg(not(feature = "std"))] @@ -37,6 +35,7 @@ pub struct RadrootsIdentity { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsIdentityPublic { pub id: RadrootsIdentityId, pub public_key_hex: String, @@ -46,6 +45,7 @@ pub struct RadrootsIdentityPublic { } #[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsIdentityProfile { #[serde(skip_serializing_if = "Option::is_none")] pub identifier: Option<String>, @@ -53,12 +53,10 @@ pub struct RadrootsIdentityProfile { pub metadata: Option<nostr::Event>, #[serde(skip_serializing_if = "Option::is_none")] pub application_handler: Option<nostr::Event>, - #[cfg(feature = "profile")] - #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option<RadrootsProfile>, } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsIdentityFile { pub secret_key: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -69,9 +67,6 @@ pub struct RadrootsIdentityFile { pub metadata: Option<nostr::Event>, #[serde(skip_serializing_if = "Option::is_none")] pub application_handler: Option<nostr::Event>, - #[cfg(feature = "profile")] - #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option<RadrootsProfile>, } #[derive(Debug, Clone, Copy)] @@ -187,15 +182,7 @@ impl RadrootsIdentityPublic { impl RadrootsIdentityProfile { pub fn is_empty(&self) -> bool { - #[cfg(feature = "profile")] - let profile_empty = self.profile.is_none(); - #[cfg(not(feature = "profile"))] - let profile_empty = true; - - self.identifier.is_none() - && self.metadata.is_none() - && self.application_handler.is_none() - && profile_empty + self.identifier.is_none() && self.metadata.is_none() && self.application_handler.is_none() } } @@ -359,17 +346,6 @@ impl RadrootsIdentity { RadrootsIdentitySecretKeyFormat::Hex => self.secret_key_hex(), RadrootsIdentitySecretKeyFormat::Nsec => self.secret_key_nsec(), }; - #[cfg(feature = "profile")] - let (identifier, metadata, application_handler, profile) = match &self.profile { - Some(profile) => ( - profile.identifier.clone(), - profile.metadata.clone(), - profile.application_handler.clone(), - profile.profile.clone(), - ), - None => (None, None, None, None), - }; - #[cfg(not(feature = "profile"))] let (identifier, metadata, application_handler) = match &self.profile { Some(profile) => ( profile.identifier.clone(), @@ -378,26 +354,12 @@ impl RadrootsIdentity { ), None => (None, None, None), }; - #[cfg(feature = "profile")] - { - RadrootsIdentityFile { - secret_key, - public_key: Some(self.public_key_hex()), - identifier, - metadata, - application_handler, - profile, - } - } - #[cfg(not(feature = "profile"))] - { - RadrootsIdentityFile { - secret_key, - public_key: Some(self.public_key_hex()), - identifier, - metadata, - application_handler, - } + RadrootsIdentityFile { + secret_key, + public_key: Some(self.public_key_hex()), + identifier, + metadata, + application_handler, } } @@ -513,14 +475,6 @@ impl TryFrom<RadrootsIdentityFile> for RadrootsIdentity { fn try_from(file: RadrootsIdentityFile) -> Result<Self, Self::Error> { let keys = Keys::parse(&file.secret_key)?; validate_public_key(&keys, file.public_key.as_deref())?; - #[cfg(feature = "profile")] - let profile = RadrootsIdentityProfile { - identifier: file.identifier, - metadata: file.metadata, - application_handler: file.application_handler, - profile: file.profile, - }; - #[cfg(not(feature = "profile"))] let profile = RadrootsIdentityProfile { identifier: file.identifier, metadata: file.metadata, diff --git a/crates/identity/tests/identity.rs b/crates/identity/tests/identity.rs @@ -1,7 +1,6 @@ #[path = "../src/test_fixtures.rs"] mod test_fixtures; -use radroots_event::profile::RadrootsProfile; use radroots_identity::{ DEFAULT_IDENTITY_PATH, IdentityError, RadrootsIdentity, RadrootsIdentityId, RadrootsIdentityProfile, RadrootsIdentityPublic, RadrootsIdentitySecretKeyFormat, @@ -93,35 +92,55 @@ fn load_from_json_file_hex() { } #[test] -fn load_from_json_file_profile() { - let mut identity = fixture_identity(FIXTURE_ALICE); - let profile = RadrootsProfile { - name: "relay-agent".to_string(), - display_name: Some("Relay Agent".to_string()), - nip05: None, - about: Some("hello".to_string()), - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }; - identity.set_profile(RadrootsIdentityProfile { - profile: Some(profile), - ..Default::default() - }); - let json = serde_json::to_string(&identity.to_file()).unwrap(); +fn legacy_embedded_profile_is_rejected_instead_of_silently_discarded() { + let identity = fixture_identity(FIXTURE_ALICE); + let mut file = serde_json::to_value(identity.to_file()).unwrap(); + file.as_object_mut().unwrap().insert( + "profile".to_owned(), + serde_json::json!({ + "name": "legacy-profile", + "picture": "https://example.test/unverified.png" + }), + ); let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("identity.json"); - std::fs::write(&path, json).unwrap(); + let encoded = serde_json::to_vec_pretty(&file).unwrap(); + std::fs::write(&path, &encoded).unwrap(); - let loaded = RadrootsIdentity::load_from_path_auto(&path).unwrap(); - let loaded_profile = loaded.profile().and_then(|p| p.profile.as_ref()).unwrap(); - assert_eq!(loaded_profile.name, "relay-agent"); - assert_eq!(loaded_profile.display_name.as_deref(), Some("Relay Agent")); - assert_eq!(loaded_profile.about.as_deref(), Some("hello")); + let error = RadrootsIdentity::load_from_path_auto(&path).unwrap_err(); + assert!( + matches!(&error, IdentityError::InvalidJson(source) if source.to_string().contains("unknown field `profile`")), + "unexpected error: {error}" + ); + assert_eq!(std::fs::read(path).unwrap(), encoded); +} + +#[test] +fn legacy_nested_identity_profile_is_rejected_without_rewriting() { + let identity = fixture_identity(FIXTURE_ALICE); + let mut public = serde_json::to_value( + identity + .to_public() + .with_profile(profile_with_identifier("alice")), + ) + .unwrap(); + public["profile"].as_object_mut().unwrap().insert( + "profile".to_owned(), + serde_json::json!({ + "name": "legacy-profile", + "picture": "https://example.test/unverified.png" + }), + ); + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("profile.json"); + let encoded = serde_json::to_vec_pretty(&public).unwrap(); + std::fs::write(&path, &encoded).unwrap(); + + let error = load_identity_profile(&path).unwrap_err(); + assert!(matches!(error, IdentityError::InvalidJson(_))); + assert_eq!(std::fs::read(path).unwrap(), encoded); } #[test] diff --git a/crates/log/Cargo.toml b/crates/log/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_log" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/mesh/Cargo.toml b/crates/mesh/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_mesh" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/mesh_agent_client/Cargo.toml b/crates/mesh_agent_client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_mesh_agent_client" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/mesh_agent_proto/Cargo.toml b/crates/mesh_agent_proto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_mesh_agent_proto" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_net" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/net/src/nostr_client/events/profile.rs b/crates/net/src/nostr_client/events/profile.rs @@ -1,10 +1,7 @@ use crate::error::{NetError, Result}; use radroots_event_codec::parsed::RadrootsParsedData; use radroots_event_codec::profile::RadrootsProfileData; -use radroots_nostr::prelude::{ - RadrootsNostrMetadata, RadrootsNostrPublicKey, radroots_nostr_fetch_metadata_for_author, - radroots_nostr_post_metadata_event, -}; +use radroots_nostr::prelude::{RadrootsNostrPublicKey, radroots_nostr_fetch_metadata_for_author}; use crate::nostr_client::manager::NostrClientManager; @@ -39,39 +36,4 @@ impl NostrClientManager { let this = self.clone(); rt.block_on(async move { this.fetch_profile_event(author).await }) } - - /// Publishes through the legacy generic metadata compatibility surface. - /// - /// This API does not enforce the strict authored Profile media contract. - /// New strict Profile authoring must use `profile.build_authored_draft`. - pub fn publish_profile_event_blocking( - &self, - name: Option<String>, - display_name: Option<String>, - nip05: Option<String>, - about: Option<String>, - ) -> Result<String> { - let rt = self.inner.rt.clone(); - let inner_for_task = self.inner.clone(); - let event_id = rt.block_on(async move { - let mut md = RadrootsNostrMetadata::new(); - if let Some(v) = name { - md = md.name(v); - } - if let Some(v) = display_name { - md = md.display_name(v); - } - if let Some(v) = nip05 { - md = md.nip05(v); - } - if let Some(v) = about { - md = md.about(v); - } - let out = radroots_nostr_post_metadata_event(&inner_for_task.client, &md) - .await - .map_err(|e| NetError::Msg(e.to_string()))?; - Ok::<String, NetError>(out.val.to_string()) - })?; - Ok(event_id) - } } diff --git a/crates/net/src/nostr_client/status.rs b/crates/net/src/nostr_client/status.rs @@ -47,7 +47,7 @@ impl NostrClientManager { let mut connected = 0usize; let mut connecting = 0usize; - for (_url, st) in map.iter() { + for st in map.values() { match st { RadrootsNostrRelayStatus::Connected => connected += 1, RadrootsNostrRelayStatus::Connecting => connecting += 1, diff --git a/crates/nostr/Cargo.toml b/crates/nostr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostr" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/nostr/src/client.rs b/crates/nostr/src/client.rs @@ -1,6 +1,5 @@ #![forbid(unsafe_code)] -use core::ops::Deref; use core::time::Duration; use std::collections::HashMap; #[cfg(not(target_arch = "wasm32"))] @@ -10,11 +9,11 @@ use nostr_sdk::{Client, ClientBuilder, ClientOptions}; use radroots_identity::RadrootsIdentity; use crate::error::RadrootsNostrError; -use crate::types::RadrootsNostrMetadata; use crate::types::{ - RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrEventId, RadrootsNostrFilter, - RadrootsNostrKeys, RadrootsNostrMonitor, RadrootsNostrOutput, RadrootsNostrRelay, - RadrootsNostrRelayUrl, RadrootsNostrSubscribeAutoCloseOptions, RadrootsNostrSubscriptionId, + RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrEventId, RadrootsNostrEventStream, + RadrootsNostrFilter, RadrootsNostrKeys, RadrootsNostrMonitor, RadrootsNostrOutput, + RadrootsNostrPublicKey, RadrootsNostrRelay, RadrootsNostrRelayUrl, + RadrootsNostrSubscribeAutoCloseOptions, RadrootsNostrSubscriptionId, }; #[derive(Clone)] @@ -142,10 +141,35 @@ impl RadrootsNostrClient { Self { inner } } + /// Returns the underlying SDK client for explicit low-level interoperability. pub fn into_inner(self) -> Client { self.inner } + pub async fn has_signer(&self) -> bool { + self.inner.has_signer().await + } + + pub async fn public_key(&self) -> Result<RadrootsNostrPublicKey, RadrootsNostrError> { + Ok(self.inner.public_key().await?) + } + + pub fn monitor(&self) -> Option<&RadrootsNostrMonitor> { + self.inner.monitor() + } + + pub async fn connect(&self) { + self.inner.connect().await; + } + + pub async fn wait_for_connection(&self, timeout: Duration) { + self.inner.wait_for_connection(timeout).await; + } + + pub async fn try_connect(&self, timeout: Duration) -> RadrootsNostrOutput<()> { + self.inner.try_connect(timeout).await + } + pub async fn add_relay(&self, url: &str) -> Result<bool, RadrootsNostrError> { Ok(self.inner.add_relay(url).await?) } @@ -176,6 +200,21 @@ impl RadrootsNostrClient { Ok(events.to_vec()) } + pub async fn query_database( + &self, + filter: RadrootsNostrFilter, + ) -> Result<Vec<RadrootsNostrEvent>, RadrootsNostrError> { + Ok(self.inner.database().query(filter).await?.to_vec()) + } + + pub async fn stream_events( + &self, + filter: RadrootsNostrFilter, + timeout: Duration, + ) -> Result<RadrootsNostrEventStream, RadrootsNostrError> { + Ok(self.inner.stream_events(filter, timeout).await?) + } + pub async fn subscribe( &self, filter: RadrootsNostrFilter, @@ -225,23 +264,12 @@ impl RadrootsNostrClient { .await?) } - /// Publishes through the legacy generic metadata compatibility surface. - /// - /// This API does not enforce the strict authored Profile media contract. - /// New strict Profile authoring must use `profile.build_authored_draft`. - pub async fn set_metadata( + pub async fn send_event_to( &self, - md: &RadrootsNostrMetadata, + relays: Vec<String>, + event: &RadrootsNostrEvent, ) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { - Ok(self.inner.set_metadata(md).await?) - } -} - -impl Deref for RadrootsNostrClient { - type Target = Client; - - fn deref(&self) -> &Self::Target { - &self.inner + Ok(self.inner.send_event_to(relays, event).await?) } } diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs @@ -56,10 +56,6 @@ pub enum RadrootsNostrError { #[error("Filter tag error: {0}")] FilterTagError(String), - - #[cfg(feature = "codec")] - #[error("Profile encode error: {0}")] - ProfileEncodeError(#[from] radroots_event_codec::profile::error::ProfileEncodeError), } #[derive(Debug, Error)] diff --git a/crates/nostr/src/events/metadata.rs b/crates/nostr/src/events/metadata.rs @@ -1,42 +1,14 @@ -use crate::types::{RadrootsNostrEventBuilder, RadrootsNostrMetadata}; - #[cfg(feature = "client")] use crate::client::RadrootsNostrClient; #[cfg(feature = "client")] use crate::error::RadrootsNostrError; #[cfg(feature = "client")] use crate::types::{ - RadrootsNostrEvent, RadrootsNostrEventId, RadrootsNostrFilter, RadrootsNostrKind, - RadrootsNostrOutput, RadrootsNostrPublicKey, + RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, }; #[cfg(feature = "client")] use core::time::Duration; -/// Builds kind-0 metadata through the legacy generic metadata surface. -/// -/// This compatibility API does not enforce the strict authored Profile media -/// contract. New strict Profile authoring must use -/// `profile.build_authored_draft` before signing. -pub fn radroots_nostr_build_metadata_event( - md: &RadrootsNostrMetadata, -) -> RadrootsNostrEventBuilder { - RadrootsNostrEventBuilder::metadata(md) -} - -#[cfg(feature = "client")] -/// Publishes kind-0 metadata through the legacy generic metadata surface. -/// -/// This compatibility API does not enforce the strict authored Profile media -/// or upload-completion contract. New strict Profile authoring must use -/// `profile.build_authored_draft` and prove BUD-02 completion before signing. -pub async fn radroots_nostr_post_metadata_event( - client: &RadrootsNostrClient, - md: &RadrootsNostrMetadata, -) -> Result<RadrootsNostrOutput<RadrootsNostrEventId>, RadrootsNostrError> { - let builder = radroots_nostr_build_metadata_event(md); - client.send_event_builder(builder).await -} - #[cfg(feature = "client")] /// Fetches metadata through the legacy compatibility path. /// @@ -50,7 +22,7 @@ pub async fn radroots_nostr_fetch_metadata_for_author( let filter = RadrootsNostrFilter::new() .authors(vec![author]) .kind(RadrootsNostrKind::Metadata); - let stored = client.database().query(filter.clone()).await?; + let stored = client.query_database(filter.clone()).await?; let fetched = client.fetch_events(filter, timeout).await?; let mut latest: Option<RadrootsNostrEvent> = None; diff --git a/crates/nostr/src/identity_profile.rs b/crates/nostr/src/identity_profile.rs @@ -1,108 +0,0 @@ -use crate::client::RadrootsNostrClient; -use crate::error::RadrootsNostrError; -#[cfg(feature = "events")] -use crate::events::application_handler::{ - RadrootsNostrApplicationHandlerSpec, radroots_nostr_metadata_has_fields, - radroots_nostr_publish_application_handler, -}; -use crate::events::metadata::radroots_nostr_build_metadata_event; -#[cfg(feature = "events")] -use crate::types::RadrootsNostrMetadata; -use crate::types::{ - RadrootsNostrEventId, RadrootsNostrOutput, RadrootsNostrTag, RadrootsNostrTagKind, -}; -#[cfg(feature = "events")] -use core::time::Duration; -use radroots_event::profile::RadrootsProfileType; -use radroots_event_codec::profile::encode::profile_build_tags; -use radroots_identity::RadrootsIdentity; - -/// Publishes an identity Profile through the legacy compatibility model. -/// -/// This API does not enforce the strict authored Profile media contract. New -/// Profile product authoring must use `profile.build_authored_draft`. -pub async fn radroots_nostr_publish_identity_profile( - client: &RadrootsNostrClient, - identity: &RadrootsIdentity, -) -> Result<Option<RadrootsNostrOutput<RadrootsNostrEventId>>, RadrootsNostrError> { - radroots_nostr_publish_identity_profile_with_type(client, identity, None).await -} - -/// Publishes a typed identity Profile through the legacy compatibility model. -/// -/// This API can emit the legacy Radroots marker tag and does not enforce the -/// strict authored Profile media contract. New Profile product authoring must -/// use `profile.build_authored_draft`. -pub async fn radroots_nostr_publish_identity_profile_with_type( - client: &RadrootsNostrClient, - identity: &RadrootsIdentity, - profile_type: Option<RadrootsProfileType>, -) -> Result<Option<RadrootsNostrOutput<RadrootsNostrEventId>>, RadrootsNostrError> { - let Some(profile) = identity.profile().and_then(|p| p.profile.as_ref()) else { - return Ok(None); - }; - let metadata = radroots_event_codec::profile::encode::to_metadata(profile)?; - let tags = profile_build_tags(profile_type); - let mut tag_list: Vec<RadrootsNostrTag> = Vec::new(); - for mut tag in tags { - if tag.is_empty() { - continue; - } - let key = tag.remove(0); - tag_list.push(RadrootsNostrTag::custom( - RadrootsNostrTagKind::Custom(key.into()), - tag, - )); - } - let builder = radroots_nostr_build_metadata_event(&metadata).tags(tag_list); - let out = client.send_event_builder(builder).await?; - Ok(Some(out)) -} - -#[cfg(feature = "events")] -/// Bootstraps service presence through legacy Profile publication surfaces. -/// -/// This compatibility API does not enforce the strict authored Profile media -/// contract. New Profile product authoring must use -/// `profile.build_authored_draft`. -pub async fn radroots_nostr_bootstrap_service_presence( - client: &RadrootsNostrClient, - identity: &RadrootsIdentity, - profile_type: Option<RadrootsProfileType>, - metadata: &RadrootsNostrMetadata, - handler_spec: &RadrootsNostrApplicationHandlerSpec, - connect_timeout: Duration, -) -> Result<(), RadrootsNostrError> { - client.connect().await; - client.wait_for_connection(connect_timeout).await; - - let profile_published = - radroots_nostr_publish_identity_profile_with_type(client, identity, profile_type) - .await? - .is_some(); - - if radroots_nostr_metadata_has_fields(metadata) && !profile_published { - let builder = - radroots_nostr_build_metadata_event(metadata).tags(profile_type_tags(profile_type)); - client.send_event_builder(builder).await?; - } - - radroots_nostr_publish_application_handler(client, handler_spec).await?; - Ok(()) -} - -#[cfg(feature = "events")] -fn profile_type_tags(profile_type: Option<RadrootsProfileType>) -> Vec<RadrootsNostrTag> { - let mut tag_list: Vec<RadrootsNostrTag> = Vec::new(); - for mut tag in profile_build_tags(profile_type) { - if tag.is_empty() { - continue; - } - let key = tag.remove(0); - tag_list.push(RadrootsNostrTag::custom( - RadrootsNostrTagKind::Custom(key.into()), - tag, - )); - } - tag_list -} diff --git a/crates/nostr/src/lib.rs b/crates/nostr/src/lib.rs @@ -43,9 +43,6 @@ pub mod event_convert; #[cfg(feature = "events")] pub mod event_verify; -#[cfg(all(feature = "client", feature = "codec"))] -pub mod identity_profile; - pub mod prelude { pub use crate::events::radroots_nostr_build_event; @@ -72,7 +69,6 @@ pub mod prelude { pub use crate::events::{ jobs::{radroots_nostr_build_event_job_feedback, radroots_nostr_build_event_job_result}, - metadata::radroots_nostr_build_metadata_event, post::{ radroots_nostr_build_post_event, radroots_nostr_build_post_reply_event, radroots_nostr_post_events_filter, @@ -89,17 +85,7 @@ pub mod prelude { pub use crate::events::application_handler::radroots_nostr_publish_application_handler; #[cfg(feature = "client")] - pub use crate::events::metadata::{ - radroots_nostr_fetch_metadata_for_author, radroots_nostr_post_metadata_event, - }; - - #[cfg(all(feature = "client", feature = "codec"))] - pub use crate::identity_profile::{ - radroots_nostr_publish_identity_profile, radroots_nostr_publish_identity_profile_with_type, - }; - - #[cfg(all(feature = "client", feature = "codec", feature = "events"))] - pub use crate::identity_profile::radroots_nostr_bootstrap_service_presence; + pub use crate::events::metadata::radroots_nostr_fetch_metadata_for_author; #[cfg(all(feature = "client", feature = "events"))] pub use crate::events::post::radroots_nostr_fetch_post_events; @@ -120,9 +106,9 @@ pub mod prelude { }; #[cfg(feature = "client")] pub use crate::types::{ - RadrootsNostrMonitor, RadrootsNostrMonitorNotification, RadrootsNostrOutput, - RadrootsNostrRelay, RadrootsNostrRelayPoolNotification, RadrootsNostrRelayStatus, - RadrootsNostrSubscribeAutoCloseOptions, + RadrootsNostrEventStream, RadrootsNostrMonitor, RadrootsNostrMonitorNotification, + RadrootsNostrOutput, RadrootsNostrRelay, RadrootsNostrRelayPoolNotification, + RadrootsNostrRelayStatus, RadrootsNostrSubscribeAutoCloseOptions, }; pub use crate::util::radroots_nostr_npub_string; diff --git a/crates/nostr/src/relays.rs b/crates/nostr/src/relays.rs @@ -13,7 +13,7 @@ pub async fn radroots_nostr_remove_relay( client: &RadrootsNostrClient, url: &str, ) -> Result<(), RadrootsNostrError> { - client.force_remove_relay(url).await?; + client.remove_relay(url).await?; Ok(()) } diff --git a/crates/nostr/src/types.rs b/crates/nostr/src/types.rs @@ -33,6 +33,9 @@ pub type RadrootsNostrMonitorNotification = nostr_sdk::prelude::MonitorNotificat pub type RadrootsNostrOutput<T> = nostr_sdk::prelude::Output<T>; #[cfg(feature = "client")] +pub type RadrootsNostrEventStream = nostr_sdk::pool::stream::BoxedStream<RadrootsNostrEvent>; + +#[cfg(feature = "client")] pub type RadrootsNostrRelay = nostr_sdk::Relay; #[cfg(feature = "client")] diff --git a/crates/nostr/tests/blossom_conformance.rs b/crates/nostr/tests/blossom_conformance.rs @@ -52,7 +52,7 @@ fn checked_in_blossom_bud11_nostr_vectors_execute_against_public_api() { let vectors = conformance_vectors(); let suite: Suite = serde_json::from_str(&vectors).expect("BUD-11 Nostr vectors must parse"); assert_eq!(suite.suite, "blossom_bud11_nostr_adapter"); - assert_eq!(suite.contract_version, "0.1.0"); + assert_eq!(suite.contract_version, "1.0.0"); assert!(!suite.vectors.is_empty()); for vector in &suite.vectors { diff --git a/crates/nostr/tests/coverage.rs b/crates/nostr/tests/coverage.rs @@ -8,7 +8,6 @@ use radroots_nostr::error::RadrootsNostrTagsResolveError; use radroots_nostr::events::jobs::{ radroots_nostr_build_event_job_feedback, radroots_nostr_build_event_job_result, }; -use radroots_nostr::events::metadata::radroots_nostr_build_metadata_event; use radroots_nostr::events::post::{ radroots_nostr_build_post_event, radroots_nostr_build_post_reply_event, radroots_nostr_post_events_filter, @@ -27,9 +26,8 @@ use radroots_nostr::tags::{ radroots_nostr_tags_resolve, }; use radroots_nostr::types::{ - RadrootsNostrEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrMetadata, - RadrootsNostrRelayUrl, RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, - RadrootsNostrTimestamp, + RadrootsNostrEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrRelayUrl, + RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard, RadrootsNostrTimestamp, }; use radroots_nostr::util::{ created_at_u32_saturating, event_created_at_u32_saturating, radroots_nostr_npub_string, @@ -135,14 +133,6 @@ fn job_event_builders_are_callable() { } #[test] -fn metadata_builder_is_callable() { - let keys = make_keys(); - let metadata = RadrootsNostrMetadata::default(); - let builder = radroots_nostr_build_metadata_event(&metadata); - let _ = builder.build(keys.public_key()); -} - -#[test] fn post_helpers_cover_success_and_error_paths() { let keys = make_keys(); let parent = text_event_with_tags(&keys, Vec::new()); diff --git a/crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json b/crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json @@ -1,6 +1,6 @@ { "suite": "blossom_bud11_nostr_adapter", - "contract_version": "0.1.0", + "contract_version": "1.0.0", "vectors": [ { "id": "decode-verified-upload", diff --git a/crates/nostr_accounts/Cargo.toml b/crates/nostr_accounts/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostr_accounts" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true @@ -31,7 +31,6 @@ nostrdb-bridge = ["std", "dep:radroots_nostrdb"] [dependencies] radroots_identity = { workspace = true, optional = true, default-features = false, features = [ "std", - "profile", "json-file", ] } radroots_nostr_signer = { workspace = true, optional = true } diff --git a/crates/nostr_connect/Cargo.toml b/crates/nostr_connect/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostr_connect" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/nostr_connect/tests/protocol.rs b/crates/nostr_connect/tests/protocol.rs @@ -430,7 +430,7 @@ fn checked_in_current_session_vectors_match_protocol_behavior() { include_str!("../../../contracts/conformance/vectors/nip46/current_session.v1.json"); let document: Value = serde_json::from_str(vectors).expect("NIP-46 vector JSON"); assert_eq!(document["suite"], "nip46_current_session"); - assert_eq!(document["contract_version"], "0.1.0"); + assert_eq!(document["contract_version"], "1.0.0"); let entries = document["vectors"].as_array().expect("NIP-46 vectors"); for entry in entries { diff --git a/crates/nostr_runtime/Cargo.toml b/crates/nostr_runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostr_runtime" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/nostr_signer/Cargo.toml b/crates/nostr_signer/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostr_signer" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true @@ -21,7 +21,6 @@ hex = { workspace = true } nostr = { workspace = true } radroots_identity = { workspace = true, default-features = false, features = [ "std", - "profile", ] } radroots_nostr = { workspace = true, features = ["std"] } radroots_nostr_connect = { workspace = true } diff --git a/crates/nostrdb/Cargo.toml b/crates/nostrdb/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_nostrdb" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/nostrdb/src/nostrdb.rs b/crates/nostrdb/src/nostrdb.rs @@ -357,8 +357,8 @@ mod tests { use crate::query::RadrootsNostrdbQuerySpec; use crate::test_fixtures::{FIXTURE_ALICE_EMAIL, FIXTURE_ALICE_USERNAME}; use futures::StreamExt; + use radroots_nostr::prelude::RadrootsNostrMetadata; use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; - use radroots_nostr::prelude::{RadrootsNostrMetadata, radroots_nostr_build_metadata_event}; use std::sync::atomic::Ordering; use std::sync::{Mutex, OnceLock}; use std::time::Duration; @@ -805,7 +805,7 @@ mod tests { .display_name(FIXTURE_ALICE_USERNAME) .about("coffee operator") .lud16(FIXTURE_ALICE_EMAIL); - let metadata_event = radroots_nostr_build_metadata_event(&metadata) + let metadata_event = RadrootsNostrEventBuilder::metadata(&metadata) .sign_with_keys(&keys) .expect("metadata event should sign"); nostrdb diff --git a/crates/outbox/Cargo.toml b/crates/outbox/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_outbox" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/protected_store/Cargo.toml b/crates/protected_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_protected_store" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/protocol_contract_v1/Cargo.toml b/crates/protocol_contract_v1/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_protocol_contract_v1" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/replica_schema/Cargo.toml b/crates/replica_schema/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_replica_schema" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/replica_store/Cargo.toml b/crates/replica_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_replica_store" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/replica_store/src/backup.rs b/crates/replica_store/src/backup.rs @@ -4,7 +4,9 @@ use serde_json::{Map, Value}; use std::collections::{BTreeMap, HashMap}; pub const DATABASE_BACKUP_VERSION: &str = "1.0.0"; -pub const REPLICA_STORE_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// Replica schema compatibility version recorded in backups and export manifests. +pub const REPLICA_STORE_SCHEMA_VERSION: &str = "1.0.0"; +const LEGACY_COMPATIBLE_REPLICA_STORE_SCHEMA_VERSIONS: [&str; 1] = ["0.1.0-alpha.2"]; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SchemaEntry { @@ -32,6 +34,7 @@ pub struct MigrationBackup { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseBackup { pub format_version: String, + /// Backup schema compatibility version; the field name is retained on wire. pub replica_store_version: String, pub schema: Vec<SchemaEntry>, pub migrations: Vec<MigrationBackup>, @@ -44,7 +47,7 @@ pub fn export_database_backup(executor: &dyn SqlExecutor) -> Result<DatabaseBack let migrations = export_migrations(); Ok(DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema, migrations, data, @@ -277,10 +280,13 @@ fn validate_backup_version(backup: &DatabaseBackup) -> Result<(), SqlError> { backup.format_version, DATABASE_BACKUP_VERSION ))); } - if backup.replica_store_version != REPLICA_STORE_VERSION { + if backup.replica_store_version != REPLICA_STORE_SCHEMA_VERSION + && !LEGACY_COMPATIBLE_REPLICA_STORE_SCHEMA_VERSIONS + .contains(&backup.replica_store_version.as_str()) + { return Err(SqlError::InvalidArgument(format!( - "unsupported replica-store version {}, expected {}", - backup.replica_store_version, REPLICA_STORE_VERSION + "unsupported replica-store backup schema version {}, expected {}", + backup.replica_store_version, REPLICA_STORE_SCHEMA_VERSION ))); } Ok(()) @@ -433,7 +439,7 @@ mod tests { ); let backup = DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema: vec![SchemaEntry { object_type: String::from("table"), name: String::from("fail_table"), @@ -558,7 +564,7 @@ mod tests { row.insert(String::from("co\"l"), Value::from(7)); let backup = DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema: vec![ SchemaEntry { object_type: String::from("table"), @@ -634,7 +640,7 @@ mod tests { #[test] fn validate_backup_version_rejects_invalid_versions() { - let wrong_format = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + let wrong_format = backup_with_versions("0.0.1", REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( validate_backup_version(&wrong_format), "ERR_INVALID_ARGUMENT", @@ -645,6 +651,10 @@ mod tests { validate_backup_version(&wrong_db_version), "ERR_INVALID_ARGUMENT", ); + + let previous_alpha = backup_with_versions(DATABASE_BACKUP_VERSION, "0.1.0-alpha.2"); + validate_backup_version(&previous_alpha) + .expect("schema-compatible alpha.2 backup must remain restorable"); } #[test] @@ -656,7 +666,7 @@ mod tests { )], None, ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); let matched = executor .query_raw("select type, name from sqlite_master", "[]") @@ -692,7 +702,7 @@ mod tests { )], None, ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); let backup_json = serde_json::to_string(&backup).expect("serialize backup"); restore_database_backup_json(&executor, &backup_json).expect("restore should succeed"); @@ -749,6 +759,7 @@ mod tests { None, ); let backup = export_database_backup(&executor).expect("backup success"); + assert_eq!(backup.replica_store_version, "1.0.0"); assert!(backup.schema.is_empty()); assert!(backup.data.is_empty()); } @@ -843,7 +854,7 @@ mod tests { #[test] fn restore_database_backup_rejects_invalid_versions_before_transaction() { let executor = MockExecutor::new(Vec::new(), None); - let backup = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + let backup = backup_with_versions("0.0.1", REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_ARGUMENT", @@ -860,7 +871,7 @@ mod tests { )], Some(String::from("PRAGMA foreign_keys = OFF;")), ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_QUERY", @@ -877,7 +888,7 @@ mod tests { None, ) .with_begin_failure(); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_QUERY", @@ -888,7 +899,7 @@ mod tests { fn restore_database_backup_fails_when_drop_query_fails() { let executor = MockExecutor::new(Vec::new(), None) .with_query_failure("select type, name from sqlite_master"); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_QUERY", @@ -906,7 +917,7 @@ mod tests { ); let backup = DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema: vec![SchemaEntry { object_type: String::from("table"), name: String::from("tb_a"), @@ -939,7 +950,7 @@ mod tests { row.insert(String::from("id"), Value::from("1")); let backup = DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema: vec![SchemaEntry { object_type: String::from("table"), name: String::from("tb_a"), @@ -968,7 +979,7 @@ mod tests { None, ) .with_commit_failure(); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_QUERY", @@ -984,7 +995,7 @@ mod tests { )], Some(String::from("PRAGMA foreign_keys = ON;")), ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_QUERY", diff --git a/crates/replica_store/src/export.rs b/crates/replica_store/src/export.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use crate::backup::{ - DATABASE_BACKUP_VERSION, MigrationBackup, REPLICA_STORE_VERSION, SchemaEntry, + DATABASE_BACKUP_VERSION, MigrationBackup, REPLICA_STORE_SCHEMA_VERSION, SchemaEntry, escape_identifier, export_migrations, load_schema, }; @@ -18,6 +18,7 @@ pub struct TableCount { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReplicaStoreExportManifestRs { pub export_version: String, + /// Replica schema compatibility version, independent of package SemVer. pub replica_store_version: String, pub backup_format_version: String, pub schema_hash: String, @@ -35,7 +36,7 @@ pub fn export_manifest( let schema_hash = schema_hash(&schema); Ok(ReplicaStoreExportManifestRs { export_version: REPLICA_STORE_EXPORT_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), backup_format_version: DATABASE_BACKUP_VERSION.to_string(), schema_hash, schema, @@ -235,6 +236,7 @@ mod tests { None, ); let manifest = export_manifest(&executor).expect("export should succeed"); + assert_eq!(manifest.replica_store_version, "1.0.0"); assert_eq!(manifest.table_counts.len(), 1); assert_eq!(manifest.table_counts[0].name, "tb_a"); assert_eq!(manifest.table_counts[0].row_count, 0); diff --git a/crates/replica_store/tests/backup_export_paths.rs b/crates/replica_store/tests/backup_export_paths.rs @@ -2,7 +2,7 @@ use std::collections::VecDeque; use std::sync::Mutex; use radroots_replica_store::backup::{ - DATABASE_BACKUP_VERSION, DatabaseBackup, REPLICA_STORE_VERSION, SchemaEntry, TableData, + DATABASE_BACKUP_VERSION, DatabaseBackup, REPLICA_STORE_SCHEMA_VERSION, SchemaEntry, TableData, export_database_backup, export_database_backup_json, restore_database_backup, }; use radroots_replica_store::export::export_manifest; @@ -171,13 +171,13 @@ fn backup_public_api_error_paths_cover_library_instantiations() { assert!(backup_json.contains("\"schema\":[]")); let executor = PatternExecutor::new(); - let backup = backup_with_versions("0.0.1", REPLICA_STORE_VERSION); + let backup = backup_with_versions("0.0.1", REPLICA_STORE_SCHEMA_VERSION); assert_sql_error_code( restore_database_backup(&executor, &backup), "ERR_INVALID_ARGUMENT", ); - let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_VERSION); + let backup = backup_with_versions(DATABASE_BACKUP_VERSION, REPLICA_STORE_SCHEMA_VERSION); let executor = PatternExecutor::new().with_exec_failure("PRAGMA foreign_keys = OFF;"); assert_sql_error_code( restore_database_backup(&executor, &backup), @@ -214,7 +214,7 @@ fn backup_public_api_error_paths_cover_library_instantiations() { fn backup_public_api_insert_and_parse_failures_cover_library_instantiations() { let backup = DatabaseBackup { format_version: DATABASE_BACKUP_VERSION.to_string(), - replica_store_version: REPLICA_STORE_VERSION.to_string(), + replica_store_version: REPLICA_STORE_SCHEMA_VERSION.to_string(), schema: vec![SchemaEntry { object_type: String::from("table"), name: String::from("tb_a"), diff --git a/crates/replica_sync/Cargo.toml b/crates/replica_sync/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_replica_sync" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/replica_sync/src/emit.rs b/crates/replica_sync/src/emit.rs @@ -9,15 +9,18 @@ use alloc::{ #[cfg(feature = "std")] use std::collections::BTreeMap; +use crate::canonical::canonical_json_string; +use crate::error::RadrootsReplicaEventsError; +use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84}; +use crate::types::{ + RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, + RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, +}; use radroots_event::farm::{RadrootsFarm, RadrootsFarmPublicLocation, RadrootsFarmRef}; use radroots_event::gcs::{RadrootsGcsLocation, RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; use radroots_event::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT}; use radroots_event::location::{has_textual_locality, is_public_geohash5}; use radroots_event::plot::RadrootsPlot; -use radroots_event::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_from_tag_value, radroots_profile_type_tag_value, -}; use radroots_event::wire::RadrootsNip01EventWireParts; use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::farm::list_sets as farm_list_sets; @@ -39,28 +42,16 @@ use radroots_replica_schema::farm_tag::{IFarmTagFieldsFilter, IFarmTagFindMany}; use radroots_replica_schema::gcs_location::{ GcsLocation, GcsLocationQueryBindValues, IGcsLocationFindOne, IGcsLocationFindOneArgs, }; -use radroots_replica_schema::nostr_profile::{ - INostrProfileFindOne, INostrProfileFindOneArgs, NostrProfileQueryBindValues, -}; use radroots_replica_schema::plot::{IPlotFieldsFilter, IPlotFindMany, Plot}; use radroots_replica_schema::plot_gcs_location::{ IPlotGcsLocationFieldsFilter, IPlotGcsLocationFindMany, PlotGcsLocation, }; use radroots_replica_schema::plot_tag::{IPlotTagFieldsFilter, IPlotTagFindMany}; use radroots_replica_store::{ - farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, nostr_profile, - plot, plot_gcs_location, plot_tag, + farm, farm_gcs_location, farm_member, farm_member_claim, farm_tag, gcs_location, plot, + plot_gcs_location, plot_tag, }; use radroots_sql_core::SqlExecutor; -use serde_json::Value; - -use crate::canonical::canonical_json_string; -use crate::error::RadrootsReplicaEventsError; -use crate::geo::{geojson_point_from_lat_lng, geojson_polygon_circle_wgs84}; -use crate::types::{ - RADROOTS_REPLICA_TRANSFER_VERSION, RadrootsReplicaEventDraft, RadrootsReplicaFarmSelector, - RadrootsReplicaSyncBundle, RadrootsReplicaSyncOptions, RadrootsReplicaSyncRequest, -}; const ROLE_PRIMARY: &str = "primary"; const ROLE_MEMBER: &str = "member"; @@ -103,8 +94,8 @@ pub(crate) mod failpoints { /// Builds the full replica transfer bundle. /// -/// When Profile inclusion is enabled, the bundle contains compatibility-only -/// legacy Profile drafts that do not satisfy `profile.build_authored_draft`. +/// Profile events are intentionally excluded. Ingested Profile rows are lossy +/// projections and cannot prove author intent for a complete kind-0 snapshot. pub fn radroots_replica_sync_all( exec: &dyn SqlExecutor, request: &RadrootsReplicaSyncRequest, @@ -114,15 +105,13 @@ pub fn radroots_replica_sync_all( /// Builds a replica transfer bundle using explicit inclusion options. /// -/// Included Profile drafts use the compatibility-only legacy Profile model and -/// do not satisfy `profile.build_authored_draft`. +/// Profile events are intentionally excluded from replica emission. pub fn radroots_replica_sync_all_with_options( exec: &dyn SqlExecutor, farm_selector: &RadrootsReplicaFarmSelector, options: Option<&RadrootsReplicaSyncOptions>, ) -> Result<RadrootsReplicaSyncBundle, RadrootsReplicaEventsError> { let farm = resolve_farm(exec, farm_selector)?; - let include_profiles = options.and_then(|opt| opt.include_profiles).unwrap_or(true); let include_list_sets = options .and_then(|opt| opt.include_list_sets) .unwrap_or(true); @@ -132,11 +121,6 @@ pub fn radroots_replica_sync_all_with_options( let mut events = Vec::new(); - if include_profiles { - let profiles = radroots_replica_profile_events(exec, &farm)?; - events.extend(profiles); - } - events.push(radroots_replica_farm_event(exec, &farm)?); let plots = radroots_replica_plot_events(exec, &farm)?; @@ -158,26 +142,6 @@ pub fn radroots_replica_sync_all_with_options( }) } -/// Builds compatibility-only legacy Profile drafts for replica transfer. -/// -/// These drafts do not satisfy `profile.build_authored_draft`. -pub fn radroots_replica_profile_events( - exec: &dyn SqlExecutor, - farm: &Farm, -) -> Result<Vec<RadrootsReplicaEventDraft>, RadrootsReplicaEventsError> { - let mut pubkeys = collect_profile_pubkeys(exec, farm)?; - pubkeys.sort(); - pubkeys.dedup(); - - let mut events = Vec::new(); - for pubkey in pubkeys { - if let Some(profile) = load_profile(exec, &pubkey)? { - events.push(profile_event(&pubkey, profile)?); - } - } - Ok(events) -} - pub fn radroots_replica_farm_event( exec: &dyn SqlExecutor, farm: &Farm, @@ -762,120 +726,6 @@ fn parse_polygon(value: &str, lat: f64, lng: f64) -> RadrootsGeoJsonPolygon { geojson_polygon_circle_wgs84(lat, lng, 100.0, 64) } -fn load_profile( - exec: &dyn SqlExecutor, - pubkey: &str, -) -> Result<Option<radroots_replica_schema::nostr_profile::NostrProfile>, RadrootsReplicaEventsError> -{ - let result_query = nostr_profile::find_one( - exec, - &INostrProfileFindOne::On(INostrProfileFindOneArgs { - on: NostrProfileQueryBindValues::PublicKey { - public_key: pubkey.to_string(), - }, - }), - ); - let result = result_query?; - Ok(result.result) -} - -fn profile_event( - pubkey: &str, - profile: radroots_replica_schema::nostr_profile::NostrProfile, -) -> Result<RadrootsReplicaEventDraft, RadrootsReplicaEventsError> { - let profile_type = match profile.profile_type.as_str() { - "individual" | "farmer" => Some(RadrootsProfileType::Individual), - "farm" => Some(RadrootsProfileType::Farm), - "coop" => Some(RadrootsProfileType::Coop), - "any" => Some(RadrootsProfileType::Any), - other => radroots_profile_type_from_tag_value(other), - }; - let profile_event = RadrootsProfile { - name: profile.name, - display_name: profile.display_name, - nip05: profile.nip05, - about: profile.about, - website: profile.website, - picture: profile.picture, - banner: profile.banner, - lud06: profile.lud06, - lud16: profile.lud16, - bot: None, - }; - let content = serialize_profile_content(&profile_event)?; - let mut tags = Vec::new(); - if let Some(profile_type) = profile_type { - let tag = vec![ - RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), - radroots_profile_type_tag_value(profile_type).to_string(), - ]; - tags.push(tag); - } - Ok(RadrootsReplicaEventDraft { - kind: radroots_event::kinds::KIND_PROFILE, - author: pubkey.to_string(), - content, - tags, - }) -} - -fn serialize_profile_content( - profile: &RadrootsProfile, -) -> Result<String, RadrootsReplicaEventsError> { - let mut obj = serde_json::Map::new(); - obj.insert("name".to_string(), Value::from(profile.name.clone())); - if let Some(value) = profile.display_name.as_ref() { - obj.insert("display_name".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.nip05.as_ref() { - obj.insert("nip05".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.about.as_ref() { - obj.insert("about".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.website.as_ref() { - obj.insert("website".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.picture.as_ref() { - obj.insert("picture".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.banner.as_ref() { - obj.insert("banner".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.lud06.as_ref() { - obj.insert("lud06".to_string(), Value::from(value.clone())); - } - if let Some(value) = profile.lud16.as_ref() { - obj.insert("lud16".to_string(), Value::from(value.clone())); - } - canonical_json_string(&Value::Object(obj)) -} - -fn collect_member_pubkeys( - exec: &dyn SqlExecutor, - farm_id: &str, -) -> Result<Vec<String>, RadrootsReplicaEventsError> { - let members = load_farm_members(exec, farm_id)?; - let mut pubkeys = members - .into_iter() - .map(|row| row.member_pubkey) - .collect::<Vec<_>>(); - pubkeys.sort(); - pubkeys.dedup(); - Ok(pubkeys) -} - -fn collect_profile_pubkeys( - exec: &dyn SqlExecutor, - farm: &Farm, -) -> Result<Vec<String>, RadrootsReplicaEventsError> { - let mut pubkeys = collect_member_pubkeys(exec, &farm.id)?; - let claims = load_member_claims(exec, &farm.pubkey)?; - pubkeys.extend(claims.into_iter().map(|claim| claim.member_pubkey)); - pubkeys.push(farm.pubkey.clone()); - Ok(pubkeys) -} - fn load_member_claims( exec: &dyn SqlExecutor, farm_pubkey: &str, @@ -1513,87 +1363,12 @@ mod tests { let polygon_blank = parse_polygon("", 3.0, 4.0); assert!(!polygon_blank.coordinates[0].is_empty()); - assert!( - load_profile(&exec, &farm_row.pubkey) - .expect("farm profile") - .is_some() - ); - assert!( - load_profile(&exec, &"3".repeat(64)) - .expect("missing profile") - .is_none() - ); - - let profile_event_farm = profile_event( - &farm_row.pubkey, - radroots_replica_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-000000000001".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: farm_row.pubkey.clone(), - profile_type: "farm".to_string(), - name: "farm".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile farm"); - assert!(!profile_event_farm.tags.is_empty()); - let profile_event_unknown = profile_event( - &"6".repeat(64), - radroots_replica_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-000000000002".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: "6".repeat(64), - profile_type: "legacy".to_string(), - name: "legacy".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile legacy"); - assert!(profile_event_unknown.tags.is_empty()); - - let profile_content = serialize_profile_content(&RadrootsProfile { - name: "name".to_string(), - display_name: Some("display".to_string()), - nip05: Some("nip05".to_string()), - about: Some("about".to_string()), - website: Some("website".to_string()), - picture: Some("picture".to_string()), - banner: Some("banner".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }) - .expect("serialize profile"); - assert!(profile_content.contains("\"name\":\"name\"")); - - let member_pubkeys = collect_member_pubkeys(&exec, &farm_row.id).expect("member pubkeys"); - assert!(!member_pubkeys.is_empty()); - let profile_pubkeys = collect_profile_pubkeys(&exec, &farm_row).expect("profile pubkeys"); - assert!(!profile_pubkeys.is_empty()); let claims = load_member_claims(&exec, &farm_row.pubkey).expect("claims"); assert!(!claims.is_empty()); let member_claims = load_member_claims_for_member(&exec, &"6".repeat(64)).expect("claims by member"); assert!(!member_claims.is_empty()); - let profile_events = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); - assert!(!profile_events.is_empty()); let farm_event = radroots_replica_farm_event(&exec, &farm_row).expect("farm event"); assert_eq!(farm_event.kind, KIND_FARM); let plot_events = radroots_replica_plot_events(&exec, &farm_row).expect("plot events"); @@ -1611,7 +1386,6 @@ mod tests { pubkey: None, }, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(true), include_list_sets: Some(true), include_membership_claims: Some(true), }), @@ -1724,7 +1498,6 @@ mod tests { &exec, &selector, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(false), include_membership_claims: Some(false), }), @@ -1754,84 +1527,6 @@ mod tests { } #[test] - fn emit_profile_variants_and_missing_profiles_are_handled() { - let exec = SqlxSqliteExecutor::open_memory().expect("db"); - let (farm_row, _, _) = seed(&exec); - - let _ = farm_member::create( - &exec, - &IFarmMemberFields { - farm_id: farm_row.id.clone(), - member_pubkey: "3".repeat(64), - role: ROLE_MEMBER.to_string(), - }, - ) - .expect("member"); - - let profiles = radroots_replica_profile_events(&exec, &farm_row).expect("profiles"); - assert!(!profiles.is_empty()); - - let profile_coop = profile_event( - &"c".repeat(64), - radroots_replica_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-0000000000c0".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: "c".repeat(64), - profile_type: "coop".to_string(), - name: "coop".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile coop"); - assert!(!profile_coop.tags.is_empty()); - - let profile_any = profile_event( - &"a".repeat(64), - radroots_replica_schema::nostr_profile::NostrProfile { - id: "00000000-0000-0000-0000-0000000000a0".to_string(), - created_at: "2024-01-01T00:00:00.000Z".to_string(), - updated_at: "2024-01-01T00:00:00.000Z".to_string(), - public_key: "a".repeat(64), - profile_type: "any".to_string(), - name: "any".to_string(), - display_name: None, - about: None, - website: None, - picture: None, - banner: None, - nip05: None, - lud06: None, - lud16: None, - }, - ) - .expect("profile any"); - assert!(!profile_any.tags.is_empty()); - - let profile_sparse = serialize_profile_content(&RadrootsProfile { - name: "sparse".to_string(), - display_name: None, - nip05: None, - about: None, - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - }) - .expect("serialize"); - assert!(profile_sparse.contains("\"name\"")); - } - - #[test] fn emit_query_error_paths_are_reported() { let exec = SqlxSqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); @@ -1908,13 +1603,6 @@ mod tests { }; assert!(load_plot_location(&plot_location_fail, &plot).is_err()); - let profile_fail = QueryFailExecutor { - inner: &exec, - needle: "nostr_profile", - err: SqlError::Internal, - }; - assert!(load_profile(&profile_fail, "p").is_err()); - let claims_fail = QueryFailExecutor { inner: &exec, needle: "farm_member_claim", @@ -1922,7 +1610,6 @@ mod tests { }; assert!(load_member_claims(&claims_fail, "p").is_err()); assert!(load_member_claims_for_member(&claims_fail, "p").is_err()); - assert!(collect_profile_pubkeys(&claims_fail, &farm).is_err()); } #[test] @@ -1962,19 +1649,6 @@ mod tests { pubkey: None, }; - let profile_query_fail = QueryFailExecutor { - inner: &exec, - needle: "farm_member_claim", - err: SqlError::Internal, - }; - assert!(radroots_replica_profile_events(&profile_query_fail, &farm_row).is_err()); - let profile_load_fail = QueryFailExecutor { - inner: &exec, - needle: "nostr_profile", - err: SqlError::Internal, - }; - assert!(radroots_replica_profile_events(&profile_load_fail, &farm_row).is_err()); - let farm_tag_fail = QueryFailExecutor { inner: &exec, needle: "farm_tag", @@ -2003,23 +1677,6 @@ mod tests { }; assert!(radroots_replica_membership_claim_events(&claims_fail, &farm_row.pubkey).is_err()); - let sync_profiles_fail = QueryFailExecutor { - inner: &exec, - needle: "nostr_profile", - err: SqlError::Internal, - }; - assert!( - radroots_replica_sync_all_with_options( - &sync_profiles_fail, - &selector, - Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(true), - include_list_sets: Some(false), - include_membership_claims: Some(false), - }), - ) - .is_err() - ); let sync_farm_fail = QueryFailExecutor { inner: &exec, needle: "farm_tag", @@ -2030,7 +1687,6 @@ mod tests { &sync_farm_fail, &selector, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(false), include_membership_claims: Some(false), }), @@ -2047,7 +1703,6 @@ mod tests { &sync_plot_fail, &selector, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(false), include_membership_claims: Some(false), }), @@ -2064,7 +1719,6 @@ mod tests { &sync_list_set_fail, &selector, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(true), include_membership_claims: Some(false), }), @@ -2081,7 +1735,6 @@ mod tests { &sync_claims_fail, &selector, Some(&RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(false), include_membership_claims: Some(true), }), @@ -2098,14 +1751,6 @@ mod tests { let exec = SqlxSqliteExecutor::open_memory().expect("db"); let (farm_row, _, _) = seed(&exec); - crate::canonical::failpoints::set_error(); - assert!(radroots_replica_profile_events(&exec, &farm_row).is_err()); - let farm_profile = load_profile(&exec, &farm_row.pubkey) - .expect("load profile") - .expect("farm profile"); - crate::canonical::failpoints::set_error(); - assert!(profile_event(&farm_row.pubkey, farm_profile).is_err()); - let farm_location_fail = QueryFailExecutor { inner: &exec, needle: "farm_gcs_location", @@ -2201,14 +1846,6 @@ mod tests { load_relation_by_role(&exec, &farm_row.id, ROLE_PRIMARY, RelationType::Farm).is_err() ); - let member_fail = QueryFailExecutor { - inner: &exec, - needle: "farm_member", - err: SqlError::Internal, - }; - assert!(collect_member_pubkeys(&member_fail, &farm_row.id).is_err()); - assert!(collect_profile_pubkeys(&member_fail, &farm_row).is_err()); - let list_plot_fail = QueryFailExecutor { inner: &exec, needle: "from plot", @@ -2456,11 +2093,6 @@ mod tests { .is_err() ); - let member_pubkeys = collect_member_pubkeys(&pass, &farm_row.id).expect("member pubkeys"); - assert!(!member_pubkeys.is_empty()); - let profile_pubkeys = collect_profile_pubkeys(&pass, &farm_row).expect("profile pubkeys"); - assert!(!profile_pubkeys.is_empty()); - assert!( load_relation_by_role(&pass, &farm_row.id, ROLE_PRIMARY, RelationType::Farm) .expect("farm primary relation") diff --git a/crates/replica_sync/src/ingest.rs b/crates/replica_sync/src/ingest.rs @@ -1545,8 +1545,7 @@ mod tests { use radroots_event::listing::RadrootsListingProduct; use radroots_event::plot::{RadrootsPlot, RadrootsPlotLocation}; use radroots_event::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_tag_value, + RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfileType, radroots_profile_type_tag_value, }; use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::farm::list_sets as farm_list_sets; @@ -1799,18 +1798,17 @@ mod tests { profile_type: Option<RadrootsProfileType>, name: &str, ) -> RadrootsEventEnvelope { - let profile = RadrootsProfile { - name: name.to_string(), - display_name: Some(format!("{name}-display")), - nip05: Some(format!("{name}@example.com")), - about: Some(format!("{name}-about")), - website: Some("https://example.com".to_string()), - picture: Some("https://example.com/p.png".to_string()), - banner: Some("https://example.com/b.png".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }; + let profile = serde_json::json!({ + "name": name, + "display_name": format!("{name}-display"), + "nip05": format!("{name}@example.com"), + "about": format!("{name}-about"), + "website": "https://example.com", + "picture": "https://example.com/p.png", + "banner": "https://example.com/b.png", + "lud06": "lud06", + "lud16": "lud16" + }); let mut tags = Vec::new(); if let Some(profile_type) = profile_type { tags.push(vec![ @@ -1824,7 +1822,7 @@ mod tests { u64::from(created_at), KIND_PROFILE, tags, - serde_json::to_string(&profile).expect("profile json"), + profile.to_string(), ) } diff --git a/crates/replica_sync/src/lib.rs b/crates/replica_sync/src/lib.rs @@ -17,8 +17,7 @@ pub mod types; pub use emit::{ radroots_replica_farm_event, radroots_replica_list_set_events, radroots_replica_membership_claim_events, radroots_replica_plot_events, - radroots_replica_profile_events, radroots_replica_sync_all, - radroots_replica_sync_all_with_options, + radroots_replica_sync_all, radroots_replica_sync_all_with_options, }; pub use error::RadrootsReplicaEventsError; pub use ingest::{ diff --git a/crates/replica_sync/src/sync_state.rs b/crates/replica_sync/src/sync_state.rs @@ -46,8 +46,8 @@ pub fn radroots_replica_sync_status<E: SqlExecutor>( /// Computes the replica drafts that have not reached their expected heads. /// -/// Profile entries originate from the compatibility-only legacy replica -/// emitter and do not satisfy `profile.build_authored_draft`. +/// Lossy stored Profile projections are excluded from pending publication; +/// Profile authoring must use `profile.build_authored_draft`. pub fn radroots_replica_pending_publish_batch<E: SqlExecutor>( exec: &E, ) -> Result<RadrootsReplicaPendingPublishBatch, RadrootsReplicaEventsError> { diff --git a/crates/replica_sync/src/tests.rs b/crates/replica_sync/src/tests.rs @@ -3,7 +3,7 @@ use crate::{ radroots_replica_sync_all, }; use radroots_event::gcs::{RadrootsGeoJsonPoint, RadrootsGeoJsonPolygon}; -use radroots_event::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT, KIND_PROFILE}; +use radroots_event::kinds::{KIND_FARM, KIND_LIST_SET_GENERIC, KIND_PLOT}; use radroots_replica_schema::ReplicaSchemaError; use radroots_replica_schema::farm::IFarmFields; use radroots_replica_schema::farm_gcs_location::IFarmGcsLocationFields; @@ -31,7 +31,7 @@ fn unwrap_sql<T>(result: Result<T, ReplicaSchemaError<SqlError>>, label: &str) - } #[test] -fn sync_all_emits_expected_order() { +fn sync_all_emits_expected_order_without_lossy_profiles() { let exec = SqlxSqliteExecutor::open_memory().expect("exec"); migrations::run_all_up(&exec).expect("migrations"); @@ -217,22 +217,54 @@ fn sync_all_emits_expected_order() { let bundle = radroots_replica_sync_all(&exec, &request).expect("sync"); assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); - assert_eq!(bundle.events.len(), 9); + assert_eq!(bundle.events.len(), 7); let kinds = bundle .events .iter() .map(|event| event.kind) .collect::<Vec<_>>(); - assert_eq!(kinds[0], KIND_PROFILE); - assert_eq!(kinds[1], KIND_PROFILE); - assert_eq!(kinds[2], KIND_FARM); - assert_eq!(kinds[3], KIND_PLOT); + assert_eq!(kinds[0], KIND_FARM); + assert_eq!(kinds[1], KIND_PLOT); + assert!(kinds[2..].iter().all(|kind| *kind == KIND_LIST_SET_GENERIC)); +} + +#[test] +fn sync_request_json_rejects_removed_profile_option() { + let encoded = serde_json::json!({ + "farm": { + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "d_tag": null, + "pubkey": null + }, + "options": { + "include_list_sets": true, + "include_membership_claims": false + } + }); + let request: RadrootsReplicaSyncRequest = + serde_json::from_value(encoded).expect("current sync request"); + let options = request.options.expect("sync options"); + assert_eq!(options.include_list_sets, Some(true)); + assert_eq!(options.include_membership_claims, Some(false)); + assert_eq!(RADROOTS_REPLICA_TRANSFER_VERSION, 2); + + let legacy = serde_json::json!({ + "farm": { + "id": "AAAAAAAAAAAAAAAAAAAAAA", + "d_tag": null, + "pubkey": null + }, + "options": { + "include_profiles": true + } + }); + let error = serde_json::from_value::<RadrootsReplicaSyncRequest>(legacy) + .expect_err("removed include_profiles option must fail closed"); assert!( - kinds[4..8] - .iter() - .all(|kind| *kind == KIND_LIST_SET_GENERIC) + error + .to_string() + .contains("unknown field `include_profiles`") ); - assert_eq!(kinds[8], KIND_LIST_SET_GENERIC); } #[test] diff --git a/crates/replica_sync/src/types.rs b/crates/replica_sync/src/types.rs @@ -3,7 +3,7 @@ use alloc::{string::String, vec::Vec}; use serde::{Deserialize, Serialize}; -pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 1; +pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 2; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct RadrootsReplicaEventDraft { @@ -20,6 +20,7 @@ pub struct RadrootsReplicaSyncBundle { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsReplicaFarmSelector { pub id: Option<String>, pub d_tag: Option<String>, @@ -27,13 +28,14 @@ pub struct RadrootsReplicaFarmSelector { } #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsReplicaSyncOptions { - pub include_profiles: Option<bool>, pub include_list_sets: Option<bool>, pub include_membership_claims: Option<bool>, } #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RadrootsReplicaSyncRequest { pub farm: RadrootsReplicaFarmSelector, pub options: Option<RadrootsReplicaSyncOptions>, diff --git a/crates/replica_sync/tests/ingest_roundtrip.rs b/crates/replica_sync/tests/ingest_roundtrip.rs @@ -7,8 +7,7 @@ use radroots_event::list::RadrootsListEntry; use radroots_event::list_set::RadrootsListSet; use radroots_event::plot::{RadrootsPlot, RadrootsPlotLocation}; use radroots_event::profile::{ - RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, - radroots_profile_type_tag_value, + RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfileType, radroots_profile_type_tag_value, }; use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_event_codec::error::{EventEncodeError, EventParseError}; @@ -462,7 +461,7 @@ fn seed_source( fn ingest_roundtrip_yields_zero_pending_sync() { let source = SqlxSqliteExecutor::open_memory().expect("source db"); let (_source_request, farm_d_tag, farm_pubkey, drafts) = seed_source(&source); - assert_eq!(drafts.len(), 10); + assert_eq!(drafts.len(), 8); let target = SqlxSqliteExecutor::open_memory().expect("target db"); migrations::run_all_up(&target).expect("target migrations"); @@ -532,7 +531,6 @@ fn sync_all_selector_and_options_paths_are_supported() { &RadrootsReplicaSyncRequest { farm: request.farm, options: Some(RadrootsReplicaSyncOptions { - include_profiles: Some(false), include_list_sets: Some(false), include_membership_claims: Some(false), }), @@ -979,18 +977,17 @@ fn profile_event( profile_type: Option<RadrootsProfileType>, name: &str, ) -> RadrootsEventEnvelope { - let profile = RadrootsProfile { - name: name.to_string(), - display_name: Some(format!("{name}_display")), - nip05: Some(format!("{name}@example.com")), - about: Some(format!("{name} about")), - website: Some("https://example.com".to_string()), - picture: Some("https://example.com/p.png".to_string()), - banner: Some("https://example.com/b.png".to_string()), - lud06: Some("lud06".to_string()), - lud16: Some("lud16".to_string()), - bot: None, - }; + let profile = serde_json::json!({ + "name": name, + "display_name": format!("{name}_display"), + "nip05": format!("{name}@example.com"), + "about": format!("{name} about"), + "website": "https://example.com", + "picture": "https://example.com/p.png", + "banner": "https://example.com/b.png", + "lud06": "lud06", + "lud16": "lud16" + }); let mut tags = Vec::new(); if let Some(kind) = profile_type { tags.push(vec![ @@ -1003,7 +1000,7 @@ fn profile_event( author, created_at, KIND_PROFILE, - serde_json::to_string(&profile).expect("profile json"), + profile.to_string(), tags, ) } @@ -2111,14 +2108,7 @@ fn sync_emit_handles_invalid_geojson_and_unknown_profile_type() { } assert!(list_set_seen); assert!(list_set_missed); - assert!(bundle.events.iter().any(|event| { - event.kind == KIND_PROFILE - && event.author == member_pubkey - && event - .tags - .iter() - .all(|tag| tag[0] != RADROOTS_PROFILE_TYPE_TAG_KEY) - })); + assert!(bundle.events.iter().all(|event| event.kind != KIND_PROFILE)); } #[test] diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_runtime" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/runtime_distribution/Cargo.toml b/crates/runtime_distribution/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_runtime_distribution" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/runtime_distribution/src/lib.rs b/crates/runtime_distribution/src/lib.rs @@ -251,7 +251,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }) .expect("resolve cli artifact"); @@ -261,7 +261,7 @@ human_installable = false assert_eq!(artifact.archive_extension, ".tar.gz"); assert_eq!( artifact.artifact_file_name, - "cli-0.1.0-alpha.2-x86_64-unknown-linux-gnu.tar.gz" + "cli-1.0.0-alpha.1-x86_64-unknown-linux-gnu.tar.gz" ); } @@ -275,7 +275,7 @@ human_installable = false runtime_id: "radrootsd", os: "linux", arch: "arm64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: None, }) .expect("resolve radrootsd artifact"); @@ -295,7 +295,7 @@ human_installable = false runtime_id: "community-app-desktop", os: "macos", arch: "arm64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }) .expect("resolve desktop artifact"); @@ -314,7 +314,7 @@ human_installable = false runtime_id: "community-app-ios", os: "macos", arch: "arm64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }) .expect_err("mobile runtime should not be installable"); @@ -358,7 +358,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("candidate"), }) .expect_err("candidate channel should be inactive"); @@ -380,7 +380,7 @@ human_installable = false runtime_id: "missing-runtime", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -402,7 +402,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("beta"), }, ); @@ -423,7 +423,7 @@ human_installable = false runtime_id: "radrootsd", os: "windows", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }) .expect_err("windows target should be unsupported"); @@ -456,7 +456,7 @@ human_installable = false runtime_id: "community-app-ios", os: "ios", arch: "arm64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -485,7 +485,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -517,7 +517,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -545,7 +545,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -576,7 +576,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }) .expect("single supported format should be inferred"); @@ -598,7 +598,7 @@ human_installable = false runtime_id: "cli", os: "linux", arch: "amd64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); @@ -627,7 +627,7 @@ human_installable = false runtime_id: "community-app-desktop", os: "macos", arch: "arm64", - version: "0.1.0-alpha.2", + version: "1.0.0-alpha.1", channel: Some("stable"), }, ); diff --git a/crates/runtime_manager/Cargo.toml b/crates/runtime_manager/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_runtime_manager" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/runtime_manager/src/lib.rs b/crates/runtime_manager/src/lib.rs @@ -321,7 +321,7 @@ preferred_cli_binding = true config_path: PathBuf::from("/tmp/config.toml"), logs_path: PathBuf::from("/tmp/logs"), run_path: PathBuf::from("/tmp/run"), - installed_version: "0.1.0-alpha.2".to_string(), + installed_version: "1.0.0-alpha.1".to_string(), health_endpoint: Some("jsonrpc_status".to_string()), secret_material_ref: Some( "shared/runtime-manager/radrootsd/local/token".to_string(), diff --git a/crates/runtime_manager/src/managed.rs b/crates/runtime_manager/src/managed.rs @@ -828,7 +828,7 @@ health_surface = "jsonrpc_status" config_path: instance_root.join("config/runtime.toml"), logs_path: instance_root.join("logs"), run_path: instance_root.join("run"), - installed_version: "0.1.0-alpha.2".to_owned(), + installed_version: "1.0.0-alpha.1".to_owned(), health_endpoint: Some("jsonrpc_status".to_owned()), secret_material_ref: None, last_started_at: None, @@ -913,7 +913,7 @@ health_surface = "jsonrpc_status" config_path: PathBuf::from("/tmp/config.toml"), logs_path: PathBuf::from("/tmp/logs"), run_path: PathBuf::from("/tmp/run"), - installed_version: "0.1.0-alpha.2".to_owned(), + installed_version: "1.0.0-alpha.1".to_owned(), health_endpoint: None, secret_material_ref: None, last_started_at: None, diff --git a/crates/runtime_manager/src/registry.rs b/crates/runtime_manager/src/registry.rs @@ -144,7 +144,7 @@ mod tests { config_path: PathBuf::from("/tmp/config.toml"), logs_path: PathBuf::from("/tmp/logs"), run_path: PathBuf::from("/tmp/run"), - installed_version: "0.1.0-alpha.2".to_string(), + installed_version: "1.0.0-alpha.1".to_string(), health_endpoint: Some("jsonrpc_status".to_string()), secret_material_ref: None, last_started_at: None, diff --git a/crates/runtime_paths/Cargo.toml b/crates/runtime_paths/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_runtime_paths" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/runtime_store/Cargo.toml b/crates/runtime_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_runtime_store" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/secret_vault/Cargo.toml b/crates/secret_vault/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_secret_vault" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_agent_proto/Cargo.toml b/crates/simplex_agent_proto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_agent_proto" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_agent_runtime/Cargo.toml b/crates/simplex_agent_runtime/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_agent_runtime" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_agent_store/Cargo.toml b/crates/simplex_agent_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_agent_store" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_app_store/Cargo.toml b/crates/simplex_app_store/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_app_store" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_chat_proto/Cargo.toml b/crates/simplex_chat_proto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_chat_proto" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_interop_tests/Cargo.toml b/crates/simplex_interop_tests/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_interop_tests" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_smp_crypto/Cargo.toml b/crates/simplex_smp_crypto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_smp_crypto" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_smp_proto/Cargo.toml b/crates/simplex_smp_proto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_smp_proto" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/simplex_smp_transport/Cargo.toml b/crates/simplex_smp_transport/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_simplex_smp_transport" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/sql_core/Cargo.toml b/crates/sql_core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_sql_core" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/test_fixtures/Cargo.toml b/crates/test_fixtures/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "radroots_test_fixtures" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true rust-version.workspace = true license = "MIT OR Apache-2.0" diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_trade" publish = ["crates-io"] -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/trade_sp1_guest/Cargo.toml b/crates/trade_sp1_guest/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_trade_sp1_guest" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/trade_sp1_host/Cargo.toml b/crates/trade_sp1_host/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_trade_sp1_host" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true build = "build.rs" authors = ["Tyson Lupul <tyson@radroots.org>"] diff --git a/crates/transport/Cargo.toml b/crates/transport/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_transport" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/transport_nostr/Cargo.toml b/crates/transport_nostr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_transport_nostr" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/transport_publish_protocol/Cargo.toml b/crates/transport_publish_protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_transport_publish_protocol" publish = false -version.workspace = true +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/crates/transport_reticulum/Cargo.toml b/crates/transport_reticulum/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "radroots_transport_reticulum" publish = false -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true authors = ["Tyson Lupul <tyson@radroots.org>"] rust-version.workspace = true diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xtask" -version = "0.1.0-alpha.2" +version = "1.0.0-alpha.1" edition.workspace = true rust-version.workspace = true description = "Workspace task runner for Radroots Rust crates" @@ -14,6 +14,7 @@ radroots_event_codec = { workspace = true, features = [ "contract-manifest", ], default-features = false } radroots_protocol_contract_v1 = { workspace = true, features = ["std"] } +semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = { workspace = true } diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] use crate::coverage::{CoveragePolicyFile, CoverageThresholds, read_coverage_policy}; +use semver::Version; use serde::Deserialize; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; @@ -20,6 +21,39 @@ const KNOWLEDGE_MANIFEST_AND_DECODE_RELATIVE: &str = "contracts/conformance/vectors/knowledge/manifest_and_decode.v1.json"; const KNOWLEDGE_PUBLIC_SURFACE_RELATIVE: &str = "contracts/conformance/vectors/knowledge/public_surface.v1.json"; +const RELEASES_ROOT_RELATIVE: &str = "contracts/releases"; +const CHANGELOG_RELATIVE: &str = "CHANGELOG.md"; +const REPLICA_CONTRACT_RELATIVE: &str = "contracts/replica.toml"; +const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract"; +const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION"; +const REPLICA_TRANSFER_VERSION: u32 = 2; +const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3"; +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 6] = [ + ( + "contracts/conformance/vectors/blossom/bud11_claims.v1.json", + "crates/blossom/tests/fixtures/bud11_claims.v1.json", + ), + ( + "contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json", + "crates/blossom/tests/fixtures/hash_path_and_descriptor.v1.json", + ), + ( + "contracts/conformance/vectors/blossom/bud11_nostr_adapter.v1.json", + "crates/nostr/tests/fixtures/bud11_nostr_adapter.v1.json", + ), + ( + "contracts/conformance/vectors/calendar/nip52_baseline.v1.json", + "crates/event_codec/tests/fixtures/calendar_nip52_baseline.v1.json", + ), + ( + "contracts/conformance/vectors/calendar/radroots_profile.v1.json", + "crates/event_codec/tests/fixtures/calendar_radroots_profile.v1.json", + ), + ( + "contracts/conformance/vectors/profile/metadata.v1.json", + "crates/event_codec/tests/fixtures/profile_metadata.v1.json", + ), +]; const KNOWLEDGE_MVP_SUPPORT_CONTRACT_IDS: [&str; 8] = [ "radroots.wiki.article.v1", "radroots.wiki.redirect.v1", @@ -415,6 +449,50 @@ pub struct ReplicaPolicy { #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] +pub struct ReplicaContractManifest { + pub schema_version: u32, + pub contract: ReplicaContractMetadata, + pub crate_family: ReplicaContractCrateFamily, + pub policy: ReplicaContractPolicy, + pub transfer: ReplicaTransferContract, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplicaContractMetadata { + pub name: String, + pub version: String, + pub purpose: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplicaContractCrateFamily { + pub schema: String, + pub storage: String, + pub sync: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplicaContractPolicy { + pub transport_agnostic_sync_core: bool, + pub deterministic_emit_and_ingest: bool, + pub forbid_legacy_alias_identifiers: bool, + pub profile_event_emission: String, + pub unknown_sync_request_fields: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplicaTransferContract { + pub version: u32, + pub source: String, + pub constant: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct OperationsContractManifest { pub contract: ManifestContract, pub public: PublicContract, @@ -522,11 +600,49 @@ pub struct ReleaseIntegrityRules { pub requires_release_notes: bool, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReleaseRecord { + schema_version: u32, + release: ReleaseRecordMetadata, + artifacts: ReleaseRecordArtifacts, + changes: Vec<ReleaseRecordChange>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReleaseRecordMetadata { + version: String, + previous_version: String, + contract_base_version: String, + status: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReleaseRecordArtifacts { + changelog: String, + manifest: String, + operations: String, + replica: String, + conformance: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReleaseRecordChange { + id: String, + classification: String, + semver_impacts: Vec<String>, + summary: String, +} + #[derive(Debug)] pub struct ContractBundle { pub root: PathBuf, pub manifest: ContractManifest, pub version: VersionPolicy, + pub replica: ReplicaContractManifest, pub operations_manifest: Option<OperationsContractManifest>, } @@ -541,6 +657,59 @@ struct WorkspaceSection { } #[derive(Debug, Deserialize)] +struct WorkspaceVersionCargoManifest { + workspace: WorkspaceVersionSection, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceVersionSection { + members: Vec<String>, + package: WorkspacePackageVersion, + dependencies: BTreeMap<String, WorkspaceDependencyVersion>, +} + +#[derive(Debug, Deserialize)] +struct WorkspacePackageVersion { + version: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceDependencyVersion { + path: Option<String>, + version: Option<String>, +} + +#[derive(Debug, Deserialize)] +struct VersionedPackageCargoManifest { + package: VersionedPackageSection, +} + +#[derive(Debug, Deserialize)] +struct VersionedPackageSection { + name: String, + version: PackageVersionSource, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum PackageVersionSource { + Literal(String), + Workspace { workspace: bool }, +} + +#[derive(Debug, Deserialize)] +struct CargoLockManifest { + package: Vec<CargoLockPackage>, +} + +#[derive(Debug, Deserialize)] +struct CargoLockPackage { + name: String, + version: String, + source: Option<String>, +} + +#[derive(Debug, Deserialize)] struct PackageCargoManifest { package: PackageSection, } @@ -593,10 +762,14 @@ struct EventBoundaryExpectation { witnesses: &'static [EventBoundarySourceWitness], } -const PROFILE_WITNESSES: [EventBoundarySourceWitness; 2] = [ +const PROFILE_WITNESSES: [EventBoundarySourceWitness; 3] = [ EventBoundarySourceWitness { relative_path: "crates/event/src/profile.rs", - required_fragments: &["pub struct RadrootsProfile"], + required_fragments: &["pub struct RadrootsAuthoredProfile"], + }, + EventBoundarySourceWitness { + relative_path: "crates/event_codec/src/profile/inbound.rs", + required_fragments: &["pub struct RadrootsInboundProfileMetadata"], }, EventBoundarySourceWitness { relative_path: "crates/event/src/kinds.rs", @@ -1221,7 +1394,7 @@ const CANONICAL_EVENT_BOUNDARY_EXPECTATIONS: [EventBoundaryExpectation; 41] = [ EventBoundaryExpectation { domain: "profile", kind: "0", - radroots_type: "RadrootsProfile", + radroots_type: "RadrootsAuthoredProfile / RadrootsInboundProfileMetadata", rpc_methods: &[ "events.profile.publish", "events.profile.list", @@ -2302,6 +2475,416 @@ fn base_contract_version(version: &str) -> &str { version.split_once('-').map_or(version, |(base, _)| base) } +fn parse_semver_version(version: &str) -> Result<Version, String> { + Version::parse(version) + .map_err(|error| format!("version {version} is not valid SemVer: {error}")) +} + +fn validate_contract_version_lockstep(bundle: &ContractBundle) -> Result<(), String> { + let contract_version = bundle.manifest.contract.version.as_str(); + parse_semver_version(contract_version)?; + if bundle.version.contract.version != contract_version { + return Err(format!( + "version contract {} must match manifest contract version {}", + bundle.version.contract.version, contract_version + )); + } + if let Some(operations) = bundle.operations_manifest.as_ref() + && operations.contract.version != contract_version + { + return Err(format!( + "operations contract version {} must match manifest contract version {}", + operations.contract.version, contract_version + )); + } + Ok(()) +} + +fn validate_workspace_version_lockstep( + workspace_root: &Path, + contract_version: &str, +) -> Result<(), String> { + let workspace_manifest = + parse_toml::<WorkspaceVersionCargoManifest>(&workspace_root.join("Cargo.toml"))?; + if workspace_manifest.workspace.package.version != contract_version { + return Err(format!( + "workspace.package.version {} must match contract version {}", + workspace_manifest.workspace.package.version, contract_version + )); + } + + let exact_requirement = format!("={contract_version}"); + let mut governed_packages = BTreeMap::new(); + for member in &workspace_manifest.workspace.members { + let package_path = workspace_root.join(member).join("Cargo.toml"); + let package = parse_toml::<VersionedPackageCargoManifest>(&package_path)?; + if member != VENDORED_WORKSPACE_MEMBER_RELATIVE { + match package.package.version { + PackageVersionSource::Literal(ref version) if version == contract_version => {} + PackageVersionSource::Literal(version) => { + return Err(format!( + "workspace member {member} package version {version} must match contract version {contract_version}" + )); + } + PackageVersionSource::Workspace { workspace } => { + return Err(format!( + "workspace member {member} must set an explicit package version {contract_version}, not version.workspace = {workspace}, so mounted path consumers preserve the public package version" + )); + } + } + governed_packages.insert(member.clone(), package.package.name.clone()); + } + + if package.package.name.starts_with("radroots_") { + let dependency = workspace_manifest + .workspace + .dependencies + .get(&package.package.name) + .ok_or_else(|| { + format!( + "workspace dependency {} is required for member {member}", + package.package.name + ) + })?; + if dependency.path.as_deref() != Some(member.as_str()) { + return Err(format!( + "workspace dependency {} path must be {member}", + package.package.name + )); + } + if dependency.version.as_deref() != Some(exact_requirement.as_str()) { + return Err(format!( + "workspace dependency {} version must be the exact requirement {}", + package.package.name, exact_requirement + )); + } + } + } + + for (dependency_name, dependency) in &workspace_manifest.workspace.dependencies { + let Some(path) = dependency.path.as_deref() else { + continue; + }; + if path == VENDORED_WORKSPACE_MEMBER_RELATIVE { + continue; + } + if governed_packages.contains_key(path) + && dependency.version.as_deref() != Some(exact_requirement.as_str()) + { + return Err(format!( + "workspace path dependency {dependency_name} version must be the exact requirement {exact_requirement}" + )); + } + } + + validate_cargo_lock_version_lockstep(workspace_root, contract_version, &governed_packages) +} + +fn validate_cargo_lock_version_lockstep( + workspace_root: &Path, + contract_version: &str, + governed_packages: &BTreeMap<String, String>, +) -> Result<(), String> { + let lock = parse_toml::<CargoLockManifest>(&workspace_root.join("Cargo.lock"))?; + for (member, package_name) in governed_packages { + let workspace_entries = lock + .package + .iter() + .filter(|package| package.name == *package_name && package.source.is_none()) + .collect::<Vec<_>>(); + if workspace_entries.len() != 1 { + return Err(format!( + "Cargo.lock must contain exactly one source-free entry for workspace member {member} ({package_name})" + )); + } + if workspace_entries[0].version != contract_version { + return Err(format!( + "Cargo.lock package {package_name} version {} must match contract version {contract_version}", + workspace_entries[0].version + )); + } + } + Ok(()) +} + +fn valid_release_change_id(value: &str) -> bool { + !value.is_empty() + && !value.starts_with('-') + && !value.ends_with('-') + && !value.contains("--") + && value + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +fn validate_release_record( + workspace_root: &Path, + contract_version: &str, + requires_operations: bool, + semver: &SemverRules, +) -> Result<(), String> { + let current_version = parse_semver_version(contract_version)?; + let releases_root = workspace_root.join(RELEASES_ROOT_RELATIVE); + if !releases_root.is_dir() { + return Err(format!( + "release records directory {RELEASES_ROOT_RELATIVE} is required" + )); + } + let record_relative = format!("{RELEASES_ROOT_RELATIVE}/{contract_version}.toml"); + let record = parse_toml::<ReleaseRecord>(&workspace_root.join(&record_relative))?; + if record.schema_version != 1 { + return Err(format!( + "release record {record_relative} schema_version must be 1" + )); + } + if record.release.version != contract_version { + return Err(format!( + "release record version {} must match contract version {contract_version}", + record.release.version + )); + } + let previous_version = parse_semver_version(&record.release.previous_version)?; + if record.release.previous_version == contract_version { + return Err("release.previous_version must differ from release.version".to_string()); + } + if current_version <= previous_version { + return Err(format!( + "release version {contract_version} must be greater than previous version {}", + record.release.previous_version + )); + } + let contract_base_version = format!( + "{}.{}.{}", + current_version.major, current_version.minor, current_version.patch + ); + if record.release.contract_base_version != contract_base_version { + return Err(format!( + "release.contract_base_version {} must match contract base version {}", + record.release.contract_base_version, contract_base_version + )); + } + if !matches!( + record.release.status.as_str(), + "unreleased" | "released" | "yanked" + ) { + return Err(format!( + "release.status {} must be unreleased, released, or yanked", + record.release.status + )); + } + + let expected_artifacts = [ + ( + record.artifacts.changelog.as_str(), + CHANGELOG_RELATIVE, + false, + ), + ( + record.artifacts.manifest.as_str(), + "contracts/manifest.toml", + false, + ), + ( + record.artifacts.operations.as_str(), + "contracts/operations.toml", + false, + ), + ( + record.artifacts.replica.as_str(), + REPLICA_CONTRACT_RELATIVE, + false, + ), + ( + record.artifacts.conformance.as_str(), + CONFORMANCE_ROOT_RELATIVE, + true, + ), + ]; + for (actual, expected, directory) in expected_artifacts { + if actual != expected { + return Err(format!( + "release artifact path {actual} must use canonical path {expected}" + )); + } + let path = workspace_root.join(actual); + let required = actual != "contracts/operations.toml" || requires_operations; + if required && (directory && !path.is_dir() || !directory && !path.is_file()) { + return Err(format!("release artifact {actual} does not exist")); + } + } + + if record.changes.is_empty() { + return Err("release record must contain at least one change".to_string()); + } + let mut change_ids = BTreeSet::new(); + let mut has_breaking_change = false; + let major_impacts = semver + .major_on + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let minor_impacts = semver + .minor_on + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let patch_impacts = semver + .patch_on + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + for change in &record.changes { + if !valid_release_change_id(&change.id) { + return Err(format!( + "release change id {} must use lowercase kebab-case", + change.id + )); + } + if !change_ids.insert(change.id.as_str()) { + return Err(format!( + "release record has duplicate change id {}", + change.id + )); + } + if !matches!( + change.classification.as_str(), + "breaking" | "feature" | "fix" | "deprecation" | "security" | "docs" + ) { + return Err(format!( + "release change {} has unsupported classification {}", + change.id, change.classification + )); + } + has_breaking_change |= change.classification == "breaking"; + if change.semver_impacts.is_empty() { + return Err(format!( + "release change {} must declare at least one exact semver impact", + change.id + )); + } + let mut change_impacts = BTreeSet::new(); + let mut has_major_impact = false; + let mut has_minor_impact = false; + let mut has_patch_impact = false; + for impact in &change.semver_impacts { + if !change_impacts.insert(impact.as_str()) { + return Err(format!( + "release change {} has duplicate semver impact {impact}", + change.id + )); + } + if major_impacts.contains(impact.as_str()) { + has_major_impact = true; + } else if minor_impacts.contains(impact.as_str()) { + has_minor_impact = true; + } else if patch_impacts.contains(impact.as_str()) { + has_patch_impact = true; + } else { + return Err(format!( + "release change {} semver impact {impact} is not governed by contracts/version.toml", + change.id + )); + } + } + let classification_matches = if has_major_impact { + change.classification == "breaking" + } else if has_minor_impact { + matches!(change.classification.as_str(), "feature" | "deprecation") + } else if has_patch_impact { + matches!(change.classification.as_str(), "fix" | "security" | "docs") + } else { + false + }; + if !classification_matches { + return Err(format!( + "release change {} classification {} does not match its governed semver impacts", + change.id, change.classification + )); + } + if change.summary.trim().is_empty() { + return Err(format!( + "release change {} summary must not be empty", + change.id + )); + } + } + if current_version.major != previous_version.major && !has_breaking_change { + return Err("a major version transition requires a breaking release change".to_string()); + } + + validate_changelog_release_notes(workspace_root, contract_version) +} + +fn validate_changelog_release_notes( + workspace_root: &Path, + contract_version: &str, +) -> Result<(), String> { + let path = workspace_root.join(CHANGELOG_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + let heading = format!("## [{contract_version}]"); + let mut in_release = false; + let mut has_release_note = false; + for line in raw.lines() { + let trimmed = line.trim(); + if trimmed == heading { + if in_release { + return Err(format!( + "{CHANGELOG_RELATIVE} contains duplicate heading {heading}" + )); + } + in_release = true; + continue; + } + if in_release && trimmed.starts_with("## [") { + break; + } + if in_release && trimmed.starts_with("- ") && trimmed.len() > 2 { + has_release_note = true; + } + } + if !in_release { + return Err(format!("{CHANGELOG_RELATIVE} is missing heading {heading}")); + } + if !has_release_note { + return Err(format!( + "{CHANGELOG_RELATIVE} release {contract_version} must contain at least one note" + )); + } + Ok(()) +} + +fn validate_conformance_vector_mirrors(workspace_root: &Path) -> Result<(), String> { + for (canonical_relative, mirror_relative) in CONFORMANCE_VECTOR_MIRRORS { + let canonical = fs::read(workspace_root.join(canonical_relative)) + .map_err(|error| format!("read {canonical_relative}: {error}"))?; + let mirror = fs::read(workspace_root.join(mirror_relative)) + .map_err(|error| format!("read {mirror_relative}: {error}"))?; + if canonical != mirror { + return Err(format!( + "packaged conformance mirror {mirror_relative} must exactly match {canonical_relative}" + )); + } + } + Ok(()) +} + +fn validate_version_governance( + bundle: &ContractBundle, + workspace_root: &Path, +) -> Result<(), String> { + validate_contract_version_lockstep(bundle)?; + let version = bundle.manifest.contract.version.as_str(); + validate_workspace_version_lockstep(workspace_root, version)?; + validate_release_record( + workspace_root, + version, + bundle.operations_manifest.is_some(), + &bundle.version.semver, + )?; + validate_conformance_vector_mirrors(workspace_root) +} + 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, @@ -3037,29 +3620,277 @@ fn validate_surface_metadata(surface: &Surface) -> Result<(), String> { } } - if let Some(replica) = &surface.internal_replica_crates { - validate_crate_identifier(&replica.schema, "surface.internal_replica_crates.schema")?; - validate_crate_identifier(&replica.storage, "surface.internal_replica_crates.storage")?; - validate_crate_identifier(&replica.sync, "surface.internal_replica_crates.sync")?; + if let Some(replica) = &surface.internal_replica_crates { + validate_crate_identifier(&replica.schema, "surface.internal_replica_crates.schema")?; + validate_crate_identifier(&replica.storage, "surface.internal_replica_crates.storage")?; + validate_crate_identifier(&replica.sync, "surface.internal_replica_crates.sync")?; + } + + Ok(()) +} + +fn validate_policy_metadata(policy: &Policy) -> Result<(), String> { + if !policy.exclude_internal_workspace_crates + || !policy.require_reproducible_exports + || !policy.require_conformance_vectors + { + return Err("contract policy flags must all be true".to_string()); + } + if let Some(replica) = &policy.replica + && (!replica.forbid_legacy_alias_identifiers + || !replica.require_transport_agnostic_sync_contract + || !replica.require_deterministic_emit_ingest) + { + return Err("contract replica policy flags must all be true".to_string()); + } + Ok(()) +} + +fn parse_replica_transfer_constant(path: &Path, name: &str) -> Result<u32, String> { + let source = + fs::read_to_string(path).map_err(|error| format!("read {}: {error}", path.display()))?; + let declaration_prefix = format!("pub const {name}: u32 ="); + let mut value = None; + for line in source.lines() { + let Some(raw_value) = line.trim().strip_prefix(&declaration_prefix) else { + continue; + }; + let raw_value = raw_value.trim().strip_suffix(';').ok_or_else(|| { + format!( + "replica transfer constant {name} in {} must terminate with a semicolon", + path.display() + ) + })?; + let parsed = raw_value.parse::<u32>().map_err(|error| { + format!( + "replica transfer constant {name} in {} must be a u32 literal: {error}", + path.display() + ) + })?; + if value.replace(parsed).is_some() { + return Err(format!( + "replica transfer constant {name} must be declared exactly once in {}", + path.display() + )); + } + } + value.ok_or_else(|| { + format!( + "replica transfer constant {name} is missing from {}", + path.display() + ) + }) +} + +fn validate_replica_policy_source_witnesses(sync_root: &Path) -> Result<(), String> { + let types_path = sync_root.join("src/types.rs"); + let types_source = fs::read_to_string(&types_path) + .map_err(|error| format!("read {}: {error}", types_path.display()))?; + for type_name in [ + "RadrootsReplicaFarmSelector", + "RadrootsReplicaSyncOptions", + "RadrootsReplicaSyncRequest", + ] { + let witness = format!("#[serde(deny_unknown_fields)]\npub struct {type_name}"); + if !types_source.contains(&witness) { + return Err(format!( + "replica request type {type_name} must place #[serde(deny_unknown_fields)] immediately before its public struct declaration in {}", + types_path.display() + )); + } + } + if types_source.contains("include_profiles") { + return Err(format!( + "retired replica request identifier include_profiles is forbidden in {}", + types_path.display() + )); + } + + let emit_path = sync_root.join("src/emit.rs"); + let emit_source = fs::read_to_string(&emit_path) + .map_err(|error| format!("read {}: {error}", emit_path.display()))?; + let test_module_marker = "#[cfg(test)]\nmod tests {"; + let test_module_start = emit_source.rfind(test_module_marker).ok_or_else(|| { + format!( + "replica emit source {} must keep its bottom test module behind #[cfg(test)]", + emit_path.display() + ) + })?; + let production_source = &emit_source[..test_module_start]; + if !production_source.contains("pub fn radroots_replica_sync_all_with_options(") { + return Err(format!( + "replica emit source {} is missing radroots_replica_sync_all_with_options", + emit_path.display() + )); + } + let production_code = production_source + .lines() + .filter(|line| !line.trim_start().starts_with("//")) + .collect::<Vec<_>>() + .join("\n"); + for identifier in production_code + .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_')) + .filter(|identifier| !identifier.is_empty()) + { + if identifier.to_ascii_lowercase().contains("profile") { + return Err(format!( + "replica emit production source {} must not contain Profile-related identifier {identifier}", + emit_path.display() + )); + } + } + let compact_production = production_code + .chars() + .filter(|character| !character.is_ascii_whitespace()) + .collect::<String>(); + if compact_production.contains("kind:0") { + return Err(format!( + "replica emit production source {} must not construct a literal kind-0 event", + emit_path.display() + )); + } + + Ok(()) +} + +fn validate_replica_contract(bundle: &ContractBundle, workspace_root: &Path) -> Result<(), String> { + let replica = &bundle.replica; + if replica.schema_version != 1 { + return Err("replica contract schema_version must be 1".to_string()); + } + if replica.contract.name != REPLICA_CONTRACT_NAME { + return Err(format!( + "replica contract name must be {REPLICA_CONTRACT_NAME}" + )); + } + if replica.contract.version != bundle.manifest.contract.version { + return Err(format!( + "replica contract version {} must match manifest contract version {}", + replica.contract.version, bundle.manifest.contract.version + )); + } + if replica.contract.purpose.trim().is_empty() { + return Err("replica contract purpose is required".to_string()); + } + + let manifest_family = bundle + .manifest + .surface + .internal_replica_crates + .as_ref() + .ok_or_else(|| "surface.internal_replica_crates is required".to_string())?; + for (field, actual, expected) in [ + ( + "schema", + replica.crate_family.schema.as_str(), + manifest_family.schema.as_str(), + ), + ( + "storage", + replica.crate_family.storage.as_str(), + manifest_family.storage.as_str(), + ), + ( + "sync", + replica.crate_family.sync.as_str(), + manifest_family.sync.as_str(), + ), + ] { + validate_crate_identifier(actual, &format!("replica.crate_family.{field}"))?; + if actual != expected { + return Err(format!( + "replica crate_family.{field} {actual} must match surface.internal_replica_crates.{field} {expected}" + )); + } + } + + let package_manifests = workspace_package_manifests(workspace_root)?; + for (field, crate_name) in [ + ("schema", replica.crate_family.schema.as_str()), + ("storage", replica.crate_family.storage.as_str()), + ("sync", replica.crate_family.sync.as_str()), + ] { + if !package_manifests.contains_key(crate_name) { + return Err(format!( + "replica crate_family.{field} {crate_name} must name a workspace package" + )); + } } - Ok(()) -} + let manifest_policy = bundle + .manifest + .policy + .replica + .as_ref() + .ok_or_else(|| "policy.replica is required".to_string())?; + let policy_parity = [ + ( + "transport_agnostic_sync_core", + replica.policy.transport_agnostic_sync_core, + manifest_policy.require_transport_agnostic_sync_contract, + ), + ( + "deterministic_emit_and_ingest", + replica.policy.deterministic_emit_and_ingest, + manifest_policy.require_deterministic_emit_ingest, + ), + ( + "forbid_legacy_alias_identifiers", + replica.policy.forbid_legacy_alias_identifiers, + manifest_policy.forbid_legacy_alias_identifiers, + ), + ]; + for (field, actual, expected) in policy_parity { + if !actual || actual != expected { + return Err(format!( + "replica policy.{field} must be true and match manifest policy.replica" + )); + } + } + if replica.policy.profile_event_emission != "excluded" { + return Err("replica policy.profile_event_emission must be excluded".to_string()); + } + if replica.policy.unknown_sync_request_fields != "reject" { + return Err("replica policy.unknown_sync_request_fields must be reject".to_string()); + } -fn validate_policy_metadata(policy: &Policy) -> Result<(), String> { - if !policy.exclude_internal_workspace_crates - || !policy.require_reproducible_exports - || !policy.require_conformance_vectors - { - return Err("contract policy flags must all be true".to_string()); + if replica.transfer.version != REPLICA_TRANSFER_VERSION { + return Err(format!( + "replica transfer.version must be {REPLICA_TRANSFER_VERSION}" + )); } - if let Some(replica) = &policy.replica - && (!replica.forbid_legacy_alias_identifiers - || !replica.require_transport_agnostic_sync_contract - || !replica.require_deterministic_emit_ingest) - { - return Err("contract replica policy flags must all be true".to_string()); + if replica.transfer.constant != REPLICA_TRANSFER_CONSTANT { + return Err(format!( + "replica transfer.constant must be {REPLICA_TRANSFER_CONSTANT}" + )); + } + let sync_manifest = package_manifests + .get(&replica.crate_family.sync) + .expect("replica sync workspace package was validated"); + let sync_root = sync_manifest + .parent() + .expect("workspace package manifest has a parent"); + validate_replica_policy_source_witnesses(sync_root)?; + let expected_source = sync_root + .strip_prefix(workspace_root) + .expect("workspace package lives under the workspace root") + .join("src/types.rs"); + if Path::new(&replica.transfer.source) != expected_source { + return Err(format!( + "replica transfer.source {} must be {}", + replica.transfer.source, + expected_source.display() + )); + } + let source_path = workspace_root.join(&replica.transfer.source); + let source_version = parse_replica_transfer_constant(&source_path, &replica.transfer.constant)?; + if source_version != replica.transfer.version { + return Err(format!( + "replica transfer source constant {} value {} must match contract version {}", + replica.transfer.constant, source_version, replica.transfer.version + )); } + Ok(()) } @@ -4198,12 +5029,14 @@ fn validate_contract_bundle_with_release_policy_override( .root .parent() .expect("contract root must have a workspace parent"); + validate_replica_contract(bundle, workspace_root)?; 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)?; + validate_version_governance(bundle, workspace_root)?; if resolve_release_contract_path_with_override(workspace_root, release_policy_override.clone()) .expect("validated release contract path resolution should not fail") .is_some() @@ -4465,6 +5298,8 @@ pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, Str let root = contract_root(workspace_root); let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?; let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?; + let replica = + parse_toml::<ReplicaContractManifest>(&workspace_root.join(REPLICA_CONTRACT_RELATIVE))?; let operations_manifest_path = root.join("operations.toml"); let operations_manifest = if operations_manifest_path.is_file() { Some(parse_toml::<OperationsContractManifest>( @@ -4477,6 +5312,7 @@ pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, Str root, manifest, version, + replica, operations_manifest, }) } @@ -4541,12 +5377,14 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { .root .parent() .expect("contract root must have a workspace parent"); + validate_replica_contract(bundle, workspace_root)?; 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)?; + validate_version_governance(bundle, workspace_root)?; if resolve_release_contract_path(workspace_root) .expect("validated release contract path resolution should not fail") .is_some() @@ -4568,6 +5406,20 @@ mod tests { use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; + const SYNTHETIC_CONFORMANCE_VECTOR: &str = r#"{ + "suite": "synthetic", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "synthetic_vector_001", + "kind": "synthetic.operation", + "input": {}, + "expected": {} + } + ] +} +"#; + fn workspace_root() -> PathBuf { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); manifest_dir @@ -4719,6 +5571,13 @@ mod tests { r#"[workspace] members = ["crates/a", "crates/b"] resolver = "2" + +[workspace.package] +version = "1.0.0" + +[workspace.dependencies] +radroots_a = { path = "crates/a", version = "=1.0.0" } +radroots_b = { path = "crates/b", version = "=1.0.0" } "#, ); write_file( @@ -4726,7 +5585,7 @@ resolver = "2" r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -4739,12 +5598,52 @@ readme = "README" &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_b" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false "#, ); write_file( + &root.join("crates").join("b").join("src").join("types.rs"), + r#"use serde::{Deserialize, Serialize}; + +pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 2; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsReplicaFarmSelector; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsReplicaSyncOptions; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RadrootsReplicaSyncRequest; +"#, + ); + write_file( + &root.join("crates").join("b").join("src").join("emit.rs"), + r#"pub fn radroots_replica_sync_all_with_options() {} + +#[cfg(test)] +mod tests {} +"#, + ); + write_file( + &root.join("Cargo.lock"), + r#"version = 4 + +[[package]] +name = "radroots_a" +version = "1.0.0" + +[[package]] +name = "radroots_b" +version = "1.0.0" +"#, + ); + write_file( &root.join("crates").join("core").join("src").join("unit.rs"), r#"pub enum RadrootsCoreUnitDimension { Count, @@ -4765,10 +5664,20 @@ source = "synthetic" model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] +[surface.internal_replica_crates] +schema = "radroots_a" +storage = "radroots_b" +sync = "radroots_b" + [policy] exclude_internal_workspace_crates = true require_reproducible_exports = true require_conformance_vectors = true + +[policy.replica] +forbid_legacy_alias_identifiers = true +require_transport_agnostic_sync_contract = true +require_deterministic_emit_ingest = true "#, ); write_file( @@ -4789,6 +5698,33 @@ requires_release_notes = true "#, ); write_file( + &root.join("contracts").join("replica.toml"), + r#"schema_version = 1 + +[contract] +name = "radroots_replica_contract" +version = "1.0.0" +purpose = "synthetic deterministic replica sync" + +[crate_family] +schema = "radroots_a" +storage = "radroots_b" +sync = "radroots_b" + +[policy] +transport_agnostic_sync_core = true +deterministic_emit_and_ingest = true +forbid_legacy_alias_identifiers = true +profile_event_emission = "excluded" +unknown_sync_request_fields = "reject" + +[transfer] +version = 2 +source = "crates/b/src/types.rs" +constant = "RADROOTS_REPLICA_TRANSFER_VERSION" +"#, + ); + write_file( &root.join("contracts").join("coverage.toml"), r#"[gate] fail_under_exec_lines = 100.0 @@ -4802,6 +5738,38 @@ crates = ["radroots_a", "radroots_b"] "#, ); write_file( + &root.join(CHANGELOG_RELATIVE), + "# Changelog\n\n## [1.0.0]\n\n- Synthetic breaking release.\n", + ); + for (canonical_relative, mirror_relative) in CONFORMANCE_VECTOR_MIRRORS { + write_file(&root.join(canonical_relative), SYNTHETIC_CONFORMANCE_VECTOR); + write_file(&root.join(mirror_relative), SYNTHETIC_CONFORMANCE_VECTOR); + } + write_file( + &root.join(RELEASES_ROOT_RELATIVE).join("1.0.0.toml"), + r#"schema_version = 1 + +[release] +version = "1.0.0" +previous_version = "0.1.0-alpha.2" +contract_base_version = "1.0.0" +status = "unreleased" + +[artifacts] +changelog = "CHANGELOG.md" +manifest = "contracts/manifest.toml" +operations = "contracts/operations.toml" +replica = "contracts/replica.toml" +conformance = "contracts/conformance" + +[[changes]] +id = "synthetic-major-release" +classification = "breaking" +semver_impacts = ["breaking"] +summary = "Exercise synthetic major release governance." +"#, + ); + write_file( &root_release_policy_path(&root), r#"[release] version = "1.0.0" @@ -4846,7 +5814,8 @@ public = [ "RadrootsEventRef", "RadrootsEventPtr", "RadrootsListingAddress", - "RadrootsProfile", + "RadrootsAuthoredProfile", + "RadrootsInboundProfileMetadata", "RadrootsFarm", "RadrootsListing", ] @@ -4858,23 +5827,23 @@ classes = ["encode_error", "parse_error", "validation_error", "address_error"] model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] -[operations.profile_build_draft] +[operations.profile_build_authored_draft] domain = "profile" -id = "profile.build_draft" +id = "profile.build_authored_draft" stability = "beta" -inputs = ["RadrootsProfile", "RadrootsProfileType?"] +inputs = ["RadrootsAuthoredProfile"] outputs = ["RadrootsNip01EventWireParts"] error_class = "encode_error" deterministic = true signing = "native" transport = "native" -[operations.profile_build_draft.implementation] +[operations.profile_build_authored_draft.implementation] rust_modules = ["crates/core/src/unit.rs"] -rust_types = ["radroots_event::profile::RadrootsProfile"] +rust_types = ["radroots_event::profile::RadrootsAuthoredProfile"] -[operations.profile_build_draft.conformance] -vector = "contracts/conformance/vectors/profile/build_draft.v1.json" +[operations.profile_build_authored_draft.conformance] +vector = "contracts/conformance/vectors/profile/metadata.v1.json" [operations.listing_build_draft] domain = "listing" @@ -4947,20 +5916,8 @@ vector = "contracts/conformance/vectors/listing/build_draft.v1.json" .join("conformance") .join("vectors") .join("profile") - .join("build_draft.v1.json"), - r#"{ - "suite": "profile", - "contract_version": "1.0.0", - "vectors": [ - { - "id": "profile_build_draft_minimal_001", - "kind": "profile.build_draft", - "input": {}, - "expected": {} - } - ] -} -"#, + .join("metadata.v1.json"), + SYNTHETIC_CONFORMANCE_VECTOR, ); write_file( &root @@ -4995,6 +5952,16 @@ vector = "contracts/conformance/vectors/listing/build_draft.v1.json" r#"[workspace] members = ["crates/a", "crates/b", "crates/c", "crates/d", "crates/e"] resolver = "2" + +[workspace.package] +version = "1.0.0" + +[workspace.dependencies] +radroots_a = { path = "crates/a", version = "=1.0.0" } +radroots_b = { path = "crates/b", version = "=1.0.0" } +radroots_c = { path = "crates/c", version = "=1.0.0" } +radroots_d = { path = "crates/d", version = "=1.0.0" } +radroots_e = { path = "crates/e", version = "=1.0.0" } "#, ); for crate_name in ["c", "d", "e"] { @@ -5003,7 +5970,7 @@ resolver = "2" &format!( r#"[package] name = "radroots_{crate_name}" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false "# @@ -5011,6 +5978,31 @@ publish = false ); } write_file( + &root.join("Cargo.lock"), + r#"version = 4 + +[[package]] +name = "radroots_a" +version = "1.0.0" + +[[package]] +name = "radroots_b" +version = "1.0.0" + +[[package]] +name = "radroots_c" +version = "1.0.0" + +[[package]] +name = "radroots_d" +version = "1.0.0" + +[[package]] +name = "radroots_e" +version = "1.0.0" +"#, + ); + write_file( &root.join("contracts").join("coverage.toml"), r#"[gate] fail_under_exec_lines = 100.0 @@ -5044,6 +6036,316 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] } #[test] + fn version_governance_rejects_contract_workspace_and_lock_drift() { + let root = create_synthetic_workspace("version_governance_drift"); + let mut bundle = load_contract_bundle(&root).expect("load contract"); + bundle.version.contract.version = "1.0.1".to_string(); + let contract_error = validate_contract_version_lockstep(&bundle) + .expect_err("contract header drift must fail"); + assert!(contract_error.contains("must match manifest contract version")); + + let member_path = root.join("crates/a/Cargo.toml"); + let member = fs::read_to_string(&member_path).expect("read member manifest"); + write_file( + &member_path, + &member.replace("version = \"1.0.0\"", "version.workspace = true"), + ); + let member_error = validate_workspace_version_lockstep(&root, "1.0.0") + .expect_err("inherited member version must fail"); + assert!(member_error.contains("must set an explicit package version")); + write_file(&member_path, &member); + + let workspace_path = root.join("Cargo.toml"); + let workspace = fs::read_to_string(&workspace_path).expect("read workspace manifest"); + write_file( + &workspace_path, + &workspace.replacen( + "radroots_a = { path = \"crates/a\", version = \"=1.0.0\" }", + "radroots_a = { path = \"crates/a\", version = \"1.0.0\" }", + 1, + ), + ); + let requirement_error = validate_workspace_version_lockstep(&root, "1.0.0") + .expect_err("non-exact internal dependency must fail"); + assert!(requirement_error.contains("exact requirement =1.0.0")); + write_file(&workspace_path, &workspace); + + let lock_path = root.join("Cargo.lock"); + let lock = fs::read_to_string(&lock_path).expect("read Cargo.lock"); + write_file(&lock_path, &lock.replacen("1.0.0", "1.0.1", 1)); + let lock_error = validate_workspace_version_lockstep(&root, "1.0.0") + .expect_err("lockfile version drift must fail"); + assert!(lock_error.contains("Cargo.lock package radroots_a version")); + + assert!(parse_semver_version("01.0.0").is_err()); + assert!(parse_semver_version("1.0").is_err()); + assert!(parse_semver_version("1.0.0-alpha_1").is_err()); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn replica_contract_governance_rejects_metadata_policy_and_transfer_drift() { + let root = create_synthetic_workspace("replica_contract_drift"); + let bundle = load_contract_bundle(&root).expect("load synthetic contract"); + validate_replica_contract(&bundle, &root).expect("validate replica contract"); + + let assert_replica_error = |expected: &str, mutator: fn(&mut ContractBundle)| { + let mut bundle = load_contract_bundle(&root).expect("load synthetic contract"); + mutator(&mut bundle); + let error = validate_replica_contract(&bundle, &root) + .expect_err("replica contract drift must fail"); + assert!( + error.contains(expected), + "expected `{expected}` in `{error}`" + ); + }; + + assert_replica_error("schema_version must be 1", |bundle| { + bundle.replica.schema_version = 2; + }); + assert_replica_error("name must be radroots_replica_contract", |bundle| { + bundle.replica.contract.name = "replica".to_string(); + }); + assert_replica_error("must match manifest contract version", |bundle| { + bundle.replica.contract.version = "1.0.1".to_string(); + }); + assert_replica_error("purpose is required", |bundle| { + bundle.replica.contract.purpose.clear(); + }); + assert_replica_error("crate_family.schema", |bundle| { + bundle.replica.crate_family.schema = "radroots_b".to_string(); + }); + assert_replica_error("must name a workspace package", |bundle| { + bundle.replica.crate_family.schema = "radroots_missing".to_string(); + bundle + .manifest + .surface + .internal_replica_crates + .as_mut() + .expect("replica family") + .schema = "radroots_missing".to_string(); + }); + assert_replica_error("policy.replica is required", |bundle| { + bundle.manifest.policy.replica = None; + }); + assert_replica_error("transport_agnostic_sync_core", |bundle| { + bundle.replica.policy.transport_agnostic_sync_core = false; + }); + assert_replica_error("profile_event_emission must be excluded", |bundle| { + bundle.replica.policy.profile_event_emission = "included".to_string(); + }); + assert_replica_error("unknown_sync_request_fields must be reject", |bundle| { + bundle.replica.policy.unknown_sync_request_fields = "ignore".to_string(); + }); + assert_replica_error("transfer.version must be 2", |bundle| { + bundle.replica.transfer.version = 1; + }); + assert_replica_error("transfer.constant", |bundle| { + bundle.replica.transfer.constant = "REPLICA_VERSION".to_string(); + }); + assert_replica_error("transfer.source", |bundle| { + bundle.replica.transfer.source = "crates/a/src/types.rs".to_string(); + }); + + let source_path = root.join("crates/b/src/types.rs"); + let source = fs::read_to_string(&source_path).expect("read replica types source"); + write_file( + &source_path, + &source.replace( + "pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 2;", + "pub const RADROOTS_REPLICA_TRANSFER_VERSION: u32 = 1;", + ), + ); + let bundle = load_contract_bundle(&root).expect("load source-drift contract"); + let source_error = validate_replica_contract(&bundle, &root) + .expect_err("source constant version drift must fail"); + assert!(source_error.contains("source constant")); + assert!(source_error.contains("must match contract version 2")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn replica_policy_source_witnesses_reject_runtime_drift() { + let root = create_synthetic_workspace("replica_policy_source_drift"); + let types_path = root.join("crates/b/src/types.rs"); + let emit_path = root.join("crates/b/src/emit.rs"); + let types = fs::read_to_string(&types_path).expect("read replica types source"); + let emit = fs::read_to_string(&emit_path).expect("read replica emit source"); + + write_file( + &types_path, + &types.replace( + "#[serde(deny_unknown_fields)]\npub struct RadrootsReplicaSyncOptions", + "pub struct RadrootsReplicaSyncOptions", + ), + ); + let bundle = load_contract_bundle(&root).expect("load attribute-drift contract"); + let attribute_error = validate_replica_contract(&bundle, &root) + .expect_err("missing fail-closed request attribute must fail"); + assert!(attribute_error.contains("RadrootsReplicaSyncOptions")); + assert!(attribute_error.contains("immediately before")); + + write_file( + &types_path, + &format!("{types}\npub struct include_profiles;\n"), + ); + let bundle = load_contract_bundle(&root).expect("load retired-option contract"); + let retired_error = validate_replica_contract(&bundle, &root) + .expect_err("retired include_profiles identifier must fail"); + assert!(retired_error.contains("include_profiles is forbidden")); + + write_file(&types_path, &types); + write_file( + &emit_path, + &emit.replace( + "#[cfg(test)]\nmod tests {}", + "fn emit_profile_event() {}\n\n#[cfg(test)]\nmod tests {}", + ), + ); + let bundle = load_contract_bundle(&root).expect("load profile-emitter contract"); + let profile_error = validate_replica_contract(&bundle, &root) + .expect_err("Profile-related production emitter must fail"); + assert!(profile_error.contains("Profile-related identifier emit_profile_event")); + + write_file( + &emit_path, + &emit.replace( + "#[cfg(test)]\nmod tests {}", + "fn emit_kind_zero() { let _event = Event { kind: 0 }; }\n\n#[cfg(test)]\nmod tests {}", + ), + ); + let bundle = load_contract_bundle(&root).expect("load literal-kind-zero contract"); + let kind_error = validate_replica_contract(&bundle, &root) + .expect_err("literal kind-0 production emitter must fail"); + assert!(kind_error.contains("must not construct a literal kind-0 event")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn replica_contract_is_required_and_rejects_unknown_fields() { + let missing_root = create_synthetic_workspace("replica_contract_missing"); + fs::remove_file(missing_root.join(REPLICA_CONTRACT_RELATIVE)) + .expect("remove replica contract"); + let missing_error = load_contract_bundle(&missing_root) + .expect_err("missing replica contract must fail bundle loading"); + assert!(missing_error.contains(REPLICA_CONTRACT_RELATIVE)); + let _ = fs::remove_dir_all(missing_root); + + let unknown_root = create_synthetic_workspace("replica_contract_unknown_field"); + let replica_path = unknown_root.join(REPLICA_CONTRACT_RELATIVE); + let replica = fs::read_to_string(&replica_path).expect("read replica contract"); + write_file(&replica_path, &format!("{replica}unexpected = true\n")); + let unknown_error = load_contract_bundle(&unknown_root) + .expect_err("unknown replica contract field must fail bundle loading"); + assert!(unknown_error.contains(REPLICA_CONTRACT_RELATIVE)); + assert!(unknown_error.contains("unexpected")); + let _ = fs::remove_dir_all(unknown_root); + } + + #[test] + fn conformance_vector_mirrors_are_required_even_when_parent_is_deleted() { + let root = create_synthetic_workspace("required_conformance_mirrors"); + validate_conformance_vector_mirrors(&root).expect("validate synthetic mirrors"); + + let (_, mirror_relative) = CONFORMANCE_VECTOR_MIRRORS[0]; + let mirror_path = root.join(mirror_relative); + fs::remove_file(&mirror_path).expect("remove required mirror"); + let missing_file_error = validate_conformance_vector_mirrors(&root) + .expect_err("missing required mirror file must fail"); + assert!(missing_file_error.contains(&format!("read {mirror_relative}"))); + + write_file(&mirror_path, SYNTHETIC_CONFORMANCE_VECTOR); + fs::remove_dir_all(mirror_path.parent().expect("mirror parent")) + .expect("remove required mirror parent"); + let missing_parent_error = validate_conformance_vector_mirrors(&root) + .expect_err("missing required mirror parent must fail"); + assert!(missing_parent_error.contains(&format!("read {mirror_relative}"))); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn release_record_and_conformance_mirror_validation_reject_drift() { + let root = create_synthetic_workspace("release_record_drift"); + let bundle = load_contract_bundle(&root).expect("load synthetic contract"); + validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect("validate release record"); + + let record_path = root.join("contracts/releases/1.0.0.toml"); + let record = fs::read_to_string(&record_path).expect("read release record"); + write_file( + &record_path, + &record.replace("status = \"unreleased\"", "status = \"pending\""), + ); + let status_error = validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("unsupported release status must fail"); + assert!(status_error.contains("must be unreleased, released, or yanked")); + + write_file( + &record_path, + &record.replace( + "replica = \"contracts/replica.toml\"", + "replica = \"contracts/replica-v1.toml\"", + ), + ); + let replica_artifact_error = + validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("noncanonical replica artifact must fail"); + assert!(replica_artifact_error.contains( + "release artifact path contracts/replica-v1.toml must use canonical path contracts/replica.toml" + )); + + write_file( + &record_path, + &record.replace("id = \"synthetic-major-release\"", "id = \"Bad_Id\""), + ); + let id_error = validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("invalid release change id must fail"); + assert!(id_error.contains("lowercase kebab-case")); + write_file(&record_path, &record); + + write_file( + &record_path, + &record.replace( + "semver_impacts = [\"breaking\"]", + "semver_impacts = [\"fix\"]", + ), + ); + let classification_error = + validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("classification and semver impact mismatch must fail"); + assert!(classification_error.contains("does not match its governed semver impacts")); + + write_file( + &record_path, + &record.replace( + "semver_impacts = [\"breaking\"]", + "semver_impacts = [\"unknown-impact\"]", + ), + ); + let impact_error = validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("unknown semver impact must fail"); + assert!(impact_error.contains("is not governed by contracts/version.toml")); + write_file(&record_path, &record); + + write_file(&root.join(CHANGELOG_RELATIVE), "# Changelog\n"); + let notes_error = validate_release_record(&root, "1.0.0", false, &bundle.version.semver) + .expect_err("missing release notes must fail"); + assert!(notes_error.contains("missing heading")); + + let (canonical_relative, mirror_relative) = CONFORMANCE_VECTOR_MIRRORS[0]; + write_file(&root.join(canonical_relative), "canonical\n"); + write_file(&root.join(mirror_relative), "drifted\n"); + let mirror_error = validate_conformance_vector_mirrors(&root) + .expect_err("packaged conformance drift must fail"); + assert!(mirror_error.contains("must exactly match")); + + let _ = fs::remove_dir_all(root); + } + + #[test] fn calendar_operation_authority_reports_exact_signature_drift() { let root = workspace_root(); let mut bundle = load_contract_bundle(&root).expect("load contract"); @@ -5218,7 +6520,7 @@ resolver = "2" &root.join("crates").join("a").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -5229,7 +6531,7 @@ edition = "2024" .join("Cargo.toml"), r#"[package] name = "libsqlite3-sys" -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -5240,7 +6542,7 @@ edition = "2024" .join("Cargo.toml"), r#"[package] name = "radroots_simplex_probe" -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -5248,7 +6550,7 @@ edition = "2024" &root.join("crates").join("simplex_probe").join("Cargo.toml"), r#"[package] name = "simplex_probe" -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -5418,7 +6720,7 @@ edition = "2024" fs::create_dir_all(&policy_dir).expect("create policy dir"); fs::write( policy_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[overrides.radroots_event_codec]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 99.946\nfail_under_branches = 100.0\ntemporary = true\nreason = \"publish 0.1.0-alpha.2 temporary coverage override\"\n\n[overrides.radroots_log]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = false\ntemporary = true\nreason = \"branch coverage is not applicable while the crate has no measured branch records\"\n\n[required]\ncrates = [\"radroots_event_codec\", \"radroots_log\"]\n", + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[overrides.radroots_event_codec]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 99.946\nfail_under_branches = 100.0\ntemporary = true\nreason = \"publish 1.0.0-alpha.1 temporary coverage override\"\n\n[overrides.radroots_log]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = false\ntemporary = true\nreason = \"branch coverage is not applicable while the crate has no measured branch records\"\n\n[required]\ncrates = [\"radroots_event_codec\", \"radroots_log\"]\n", ) .expect("write coverage policy"); let required = [ @@ -5544,7 +6846,7 @@ members = ["crates/a"] root.join("crates").join("a").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" repository = { workspace = true } homepage = { workspace = true } @@ -6167,7 +7469,7 @@ crates = ["radroots_a", "radroots_b"] r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -6183,7 +7485,7 @@ radroots_b = { path = "../b" } &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_b" -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate b" repository = "https://example.com/b" @@ -6230,7 +7532,7 @@ crates = ["radroots_a"] &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_b" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false "#, @@ -6242,7 +7544,7 @@ publish = false &root.join("crates").join("a").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false "#, @@ -6271,7 +7573,7 @@ crates = ["radroots_a"] r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -6284,7 +7586,7 @@ readme = "README" &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_b" -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -6304,7 +7606,7 @@ edition = "2024" r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -6326,8 +7628,13 @@ dto_bindgen_core = { path = "../../dto_bindgen_core", version = "0.1.0", optiona members = ["crates/a", "crates/b"] resolver = "2" +[workspace.package] +version = "1.0.0" + [workspace.dependencies] dto_bindgen = { version = "0.1.0", git = "https://example.com/dto_bindgen", rev = "abc123" } +radroots_a = { path = "crates/a", version = "=1.0.0" } +radroots_b = { path = "crates/b", version = "=1.0.0" } "#, ); write_file( @@ -6335,7 +7642,7 @@ dto_bindgen = { version = "0.1.0", git = "https://example.com/dto_bindgen", rev r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -6357,7 +7664,7 @@ dto_bindgen = { workspace = true, optional = true } r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = "https://example.com/a" @@ -6370,7 +7677,7 @@ readme = "README" &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_b" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false @@ -6616,42 +7923,42 @@ rust_package = "radroots_sdk" }, ); assert_bundle_error( - "operation profile.build_draft inputs uses retired event type RadrootsNostrEvent", + "operation profile.build_authored_draft inputs uses retired event type RadrootsNostrEvent", |bundle| { bundle .operations_manifest .as_mut() .expect("operations manifest") .operations - .get_mut("profile_build_draft") + .get_mut("profile_build_authored_draft") .expect("profile operation") .inputs .push("RadrootsNostrEvent".to_string()); }, ); assert_bundle_error( - "operation profile.build_draft outputs uses retired event type WireEventParts", + "operation profile.build_authored_draft outputs uses retired event type WireEventParts", |bundle| { bundle .operations_manifest .as_mut() .expect("operations manifest") .operations - .get_mut("profile_build_draft") + .get_mut("profile_build_authored_draft") .expect("profile operation") .outputs .push("WireEventParts".to_string()); }, ); assert_bundle_error( - "operation profile.build_draft implementation.rust_types uses retired event type RadrootsNostrEventPtr", + "operation profile.build_authored_draft implementation.rust_types uses retired event type RadrootsNostrEventPtr", |bundle| { bundle .operations_manifest .as_mut() .expect("operations manifest") .operations - .get_mut("profile_build_draft") + .get_mut("profile_build_authored_draft") .expect("profile operation") .implementation .rust_types @@ -6679,14 +7986,14 @@ rust_package = "radroots_sdk" .join("conformance") .join("vectors") .join("profile") - .join("build_draft.v1.json"), + .join("metadata.v1.json"), r#"{ "suite": "profile", "contract_version": "1.0.0", "vectors": [ { - "id": "profile_build_draft_minimal_001", - "kind": "profile.build_draft", + "id": "profile_build_authored_draft_minimal_001", + "kind": "profile.build_authored_draft", "input": {} } ] @@ -6695,7 +8002,7 @@ rust_package = "radroots_sdk" ); let bundle = load_contract_bundle(&invalid_vector_root).expect("load bundle"); let err = validate_contract_bundle(&bundle).expect_err("invalid vector should fail"); - assert!(err.contains("build_draft.v1.json")); + assert!(err.contains("metadata.v1.json")); assert!(err.contains("parse")); let _ = fs::remove_dir_all(&invalid_vector_root); @@ -6707,10 +8014,10 @@ rust_package = "radroots_sdk" .as_mut() .expect("operations manifest") .operations - .get_mut("profile_build_draft") + .get_mut("profile_build_authored_draft") .expect("profile operation") .conformance - .vector = "conformance/vectors/profile/build_draft.v1.json".to_string(); + .vector = "conformance/vectors/profile/metadata.v1.json".to_string(); let err = validate_contract_bundle(&bundle).expect_err("legacy path should fail"); assert!(err.contains("must live under contracts/conformance/")); let _ = fs::remove_dir_all(root); @@ -6877,7 +8184,7 @@ resolver = "2" &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate b duplicate name" repository = "https://example.com/b" @@ -6915,7 +8222,7 @@ publish = false &root.join("crates").join("b").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" publish = false "#, @@ -7280,7 +8587,7 @@ crates = ["radroots_a"] r#"[package] name = "radroots_a" publish = ["crates-io"] -version = "0.1.0" +version = "1.0.0" edition = "2024" "#, ); @@ -7433,7 +8740,7 @@ members = ["crates/a"] &root.join("crates").join("a").join("Cargo.toml"), r#"[package] name = "radroots_a" -version = "0.1.0" +version = "1.0.0" edition = "2024" description = "crate a" repository = { workspace = true } diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs @@ -4410,7 +4410,7 @@ test_threads = 0 ); write_file( &root.join("crates").join("core").join("Cargo.toml"), - "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha.2\"\nedition = \"2024\"\n", + "[package]\nname = \"radroots_core\"\nversion = \"1.0.0-alpha.1\"\nedition = \"2024\"\n", ); };