lib

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

commit 865378c0bdefc74fae2320e41ea5f5727d444cee
parent 1e37955cda5c65691fffe3264c8b21aa11c150e1
Author: triesap <tyson@radroots.org>
Date:   Thu,  6 Aug 2026 02:41:26 +0000

sdk: import preserved sdk history

- freeze sdk at 170ecf2b620107fadca85fcb11fbf3798b4ca1ef with bundle sha256 9d2b015bbcd5d516994eeb72064b1e938a04acd0c21eb2e22134b99599fdd2f0
- merge filtered head 016a58c4b5a2edb020a372bc08d49a1865705737 with path-map sha256 c5a4a696db94bec4658b62fe775f96b81e7ae913b5dedc10448a72a9f1bf3725
- bind commit-map and verification-report sha256 b23788e3e13e2e6f13e4b6294ace6cdb6bae6f74495dc8432327cfbb218c0f24 with final-tree sha256 456b9249343ee4fd21f7c8cda642a941062af97382520e54eefc13113da382cd
- qualify 395 rewritten commits, public APIs, 81 FFI symbols, UniFFI outputs, and the full SDK check matrix

Diffstat:
MCargo.lock | 919+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 31+++++++++++++++++++++++++++++++
Mcontracts/consolidation/history.v1.toml | 16+++++++++++++---
Acontracts/consolidation/imports/sdk.commit-map.v1.json | 11775+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/coverage.toml | 12++++++++++++
Mcontracts/crates/catalog.v1.toml | 60++++++++++++++++++++++++------------------------------------
Mcontracts/crates/generated/package_groups.v1.toml | 26+++++++++++++-------------
Mcontracts/crates/generated/platform_inventory.v1.toml | 2+-
Mcontracts/crates/generated/release_inventory.v2.toml | 4++--
Mcontracts/releases/publish_policy.toml | 20++++++++++++++++++--
Acrates/core_bindings/Cargo.toml | 15+++++++++++++++
Acrates/core_bindings/src/dto.rs | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/core_bindings/src/lib.rs | 19+++++++++++++++++++
Acrates/core_bindings/tests/fixtures/generated_types.ts | 25+++++++++++++++++++++++++
Acrates/event_bindings/Cargo.toml | 14++++++++++++++
Acrates/event_bindings/src/lib.rs | 28++++++++++++++++++++++++++++
Acrates/event_bindings/src/model.rs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec_wasm/Cargo.toml | 41+++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec_wasm/README | 24++++++++++++++++++++++++
Acrates/event_codec_wasm/src/lib.rs | 2184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/identity_bindings/Cargo.toml | 14++++++++++++++
Acrates/identity_bindings/src/lib.rs | 37+++++++++++++++++++++++++++++++++++++
Acrates/radroots/Cargo.toml | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radroots/LICENSE-APACHE | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radroots/LICENSE-MIT | 21+++++++++++++++++++++
Acrates/radroots/README.md | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radroots/examples/ordinary_memory.rs | 10++++++++++
Acrates/radroots/src/client.rs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radroots/src/event.rs | 5+++++
Acrates/radroots/src/farm.rs | 4++++
Acrates/radroots/src/identity.rs | 5+++++
Acrates/radroots/src/knowledge.rs | 8++++++++
Acrates/radroots/src/lib.rs | 18++++++++++++++++++
Acrates/radroots/src/listing.rs | 12++++++++++++
Acrates/radroots/src/signing.rs | 3+++
Acrates/radroots/src/storage.rs | 6++++++
Acrates/radroots/src/sync.rs | 1+
Acrates/radroots/src/trade.rs | 13+++++++++++++
Acrates/radroots/src/transport.rs | 10++++++++++
Acrates/radroots/tests/package_boundary.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/radroots/tests/public_surface.rs | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_schema_bindings/Cargo.toml | 14++++++++++++++
Acrates/replica_schema_bindings/src/lib.rs | 31+++++++++++++++++++++++++++++++
Acrates/replica_store_wasm/Cargo.toml | 33+++++++++++++++++++++++++++++++++
Acrates/replica_store_wasm/README | 25+++++++++++++++++++++++++
Acrates/replica_store_wasm/src/lib.rs | 10++++++++++
Acrates/replica_store_wasm/src/snapshot.rs | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_store_wasm/src/utils.rs | 13+++++++++++++
Acrates/replica_store_wasm/src/wasm_impl.rs | 923+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/replica_sync_wasm/Cargo.toml | 31+++++++++++++++++++++++++++++++
Acrates/replica_sync_wasm/README | 25+++++++++++++++++++++++++
Acrates/replica_sync_wasm/src/lib.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/Cargo.toml | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/LICENSE-APACHE | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/LICENSE-MIT | 21+++++++++++++++++++++
Acrates/sdk/README.md | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/examples/safe_memory_client.rs | 20++++++++++++++++++++
Acrates/sdk/examples/transport_profile.rs | 11+++++++++++
Acrates/sdk/src/adapters/mod.rs | 2++
Acrates/sdk/src/adapters/radrootsd.rs | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/capability.rs | 423+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/client.rs | 1668+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/diagnostics.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/error.rs | 465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/farm.rs | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/lib.rs | 44++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing.rs | 641+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing/operational_listing/draft.rs | 548+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing/operational_listing/mod.rs | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing/operational_listing/model.rs | 15+++++++++++++++
Acrates/sdk/src/listing/operational_listing/mutation.rs | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing/operational_listing/price_ext.rs | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/listing/operational_listing/validation.rs | 988+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/signing.rs | 532+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/storage.rs | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/sync.rs | 520+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/trade.rs | 868+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/src/transport.rs | 742+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/fixtures/publication.toml | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/lifecycle.rs | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/package_boundary.rs | 554+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/public_api.rs | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk/tests/unit/adapters_radrootsd_tests.rs | 940+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk_ffi/Cargo.toml | 30++++++++++++++++++++++++++++++
Acrates/sdk_ffi/README.md | 10++++++++++
Acrates/sdk_ffi/src/bin/bindgen.rs | 3+++
Acrates/sdk_ffi/src/lib.rs | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/sdk_sql_wasm_runtime/Cargo.toml | 24++++++++++++++++++++++++
Acrates/sdk_sql_wasm_runtime/src/lib.rs | 534+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/trade_bindings/Cargo.toml | 10++++++++++
Acrates/trade_bindings/src/lib.rs | 297+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/Cargo.toml.imported | 33+++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/api_qualification.rs | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/architecture.rs | 1511+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/architecture/api_leakage.rs | 1433+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/architecture/dependency_boundary.rs | 718+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/bindings.rs | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/check.rs | 3710+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/contracts.rs | 694+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/coverage.rs | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/coverage_policy.rs | 803+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/coverage_policy_tests.rs | 661+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/dto_roots.rs | 969+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/fs.rs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/generate.rs | 35+++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/main.rs | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/manifest.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/output.rs | 493+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/package_matrix.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/package_metadata.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/portable_qualification.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/release_graph.rs | 687+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/release_qualification.rs | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/smoke.rs | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/supply_chain_qualification.rs | 729+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/target_qualification.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/ts.rs | 37+++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/wasm.rs | 1147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/src/wasm_declarations.rs | 1147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/tests/fixtures/api-leakage/adr-exception.rs | 3+++
Atools/sdk_xtask_import/tests/fixtures/api-leakage/allowed-concrete-adapter.rs | 3+++
Atools/sdk_xtask_import/tests/fixtures/api-leakage/generic-renamed-tokio.rs | 5+++++
Atools/sdk_xtask_import/tests/fixtures/api-leakage/generic-sqlx.rs | 5+++++
Atools/sdk_xtask_import/tests/fixtures/api-leakage/private-implementation.rs | 11+++++++++++
Atools/sdk_xtask_import/tests/fixtures/dependency-boundaries/domain-to-storage.json | 46++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/tests/fixtures/dependency-boundaries/service-to-sdk.json | 46++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/tests/fixtures/dependency-boundaries/spi-to-adapter.json | 46++++++++++++++++++++++++++++++++++++++++++++++
Atools/sdk_xtask_import/tests/fixtures/dependency-boundaries/valid-downward.json | 46++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/architecture.rs | 24+++++++++++++++++++++++-
Mtools/xtask/src/build_control.rs | 4++--
Mtools/xtask/src/consolidation.rs | 779++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtools/xtask/src/contract.rs | 25+++++++++++++++++++++++--
Mtools/xtask/src/hygiene.rs | 63++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtools/xtask/src/main.rs | 3+++
134 files changed, 48698 insertions(+), 88 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -150,6 +150,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash 2.1.3", + "serde", + "serde_derive", + "syn 2.0.117", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] name = "asn1-rs" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -189,6 +231,17 @@ dependencies = [ ] [[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] name = "async-utility" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -235,6 +288,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" [[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -265,6 +324,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] name = "bech32" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -287,7 +355,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.117", "which", @@ -406,6 +474,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -442,6 +548,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] name = "chacha20" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -934,7 +1046,7 @@ dependencies = [ "serde_json", "sha2", "syn 2.0.117", - "toml", + "toml 0.8.23", ] [[package]] @@ -1228,6 +1340,15 @@ dependencies = [ ] [[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1381,11 +1502,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", ] [[package]] @@ -1417,6 +1540,17 @@ dependencies = [ ] [[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1547,6 +1681,29 @@ dependencies = [ ] [[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1564,6 +1721,65 @@ dependencies = [ ] [[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1740,6 +1956,12 @@ dependencies = [ ] [[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1937,6 +2159,12 @@ dependencies = [ ] [[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] name = "libredox" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2026,6 +2254,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" [[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] name = "mediatype" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2044,6 +2278,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" [[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2189,6 +2433,15 @@ dependencies = [ ] [[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] name = "num" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2271,6 +2524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2295,6 +2549,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2525,6 +2785,62 @@ dependencies = [ ] [[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.2", + "lru-slab", + "rand 0.10.1", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2546,6 +2862,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] +name = "radroots" +version = "0.1.0-alpha" +dependencies = [ + "radroots_core", + "radroots_event", + "radroots_identity", + "radroots_sdk", + "radroots_trade", + "radroots_transport", +] + +[[package]] name = "radroots_blossom" version = "0.1.0-alpha" dependencies = [ @@ -2569,6 +2897,15 @@ dependencies = [ ] [[package]] +name = "radroots_core_bindings" +version = "0.1.0-alpha" +dependencies = [ + "dto_bindgen", + "dto_bindgen_core", + "radroots_core", +] + +[[package]] name = "radroots_event" version = "0.1.0-alpha" dependencies = [ @@ -2587,6 +2924,14 @@ dependencies = [ ] [[package]] +name = "radroots_event_bindings" +version = "0.1.0-alpha" +dependencies = [ + "dto_bindgen_backend_ts", + "radroots_event", +] + +[[package]] name = "radroots_event_codec" version = "0.1.0-alpha" dependencies = [ @@ -2604,27 +2949,50 @@ dependencies = [ ] [[package]] -name = "radroots_geonames" -version = "0.1.0-alpha" -dependencies = [ - "fs2", - "rusqlite", - "sha2", - "tempfile", - "url", -] - -[[package]] -name = "radroots_identity" +name = "radroots_event_codec_wasm" version = "0.1.0-alpha" dependencies = [ - "k256", + "nostr", + "radroots_blossom", + "radroots_core", + "radroots_event", + "radroots_event_codec", + "radroots_identity", + "serde", + "serde_json", + "wasm-bindgen", +] + +[[package]] +name = "radroots_geonames" +version = "0.1.0-alpha" +dependencies = [ + "fs2", + "rusqlite", + "sha2", + "tempfile", + "url", +] + +[[package]] +name = "radroots_identity" +version = "0.1.0-alpha" +dependencies = [ + "k256", "serde", "serde_json", "thiserror 2.0.18", ] [[package]] +name = "radroots_identity_bindings" +version = "0.1.0-alpha" +dependencies = [ + "dto_bindgen_backend_ts", + "radroots_identity", +] + +[[package]] name = "radroots_mesh" version = "0.1.0-alpha" dependencies = [ @@ -2719,6 +3087,14 @@ dependencies = [ ] [[package]] +name = "radroots_replica_schema_bindings" +version = "0.1.0-alpha" +dependencies = [ + "dto_bindgen_core", + "radroots_replica_schema", +] + +[[package]] name = "radroots_replica_store" version = "0.1.0-alpha" dependencies = [ @@ -2731,6 +3107,23 @@ dependencies = [ ] [[package]] +name = "radroots_replica_store_wasm" +version = "0.1.0-alpha" +dependencies = [ + "js-sys", + "radroots_replica_schema", + "radroots_replica_store", + "radroots_replica_sync", + "radroots_sdk_sql_wasm_runtime", + "radroots_sql_core", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] name = "radroots_replica_sync" version = "0.1.0-alpha" dependencies = [ @@ -2752,12 +3145,27 @@ dependencies = [ ] [[package]] +name = "radroots_replica_sync_wasm" +version = "0.1.0-alpha" +dependencies = [ + "base64", + "radroots_event", + "radroots_replica_sync", + "radroots_sdk_sql_wasm_runtime", + "serde", + "serde-wasm-bindgen", + "serde_json", + "uuid", + "wasm-bindgen", +] + +[[package]] name = "radroots_runtime_distribution" version = "0.1.0-alpha" dependencies = [ "serde", "thiserror 1.0.69", - "toml", + "toml 0.8.23", ] [[package]] @@ -2770,7 +3178,7 @@ dependencies = [ "tar", "tempfile", "thiserror 1.0.69", - "toml", + "toml 0.8.23", ] [[package]] @@ -2782,6 +3190,56 @@ dependencies = [ ] [[package]] +name = "radroots_sdk" +version = "0.1.0-alpha" +dependencies = [ + "nostr", + "radroots_core", + "radroots_event", + "radroots_event_codec", + "radroots_geonames", + "radroots_identity", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_protocol", + "radroots_secrets", + "radroots_signing", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_sync", + "radroots_trade", + "radroots_transport", + "radroots_transport_nostr", + "reqwest", + "serde", + "serde_json", + "tempfile", + "tokio", + "uuid", +] + +[[package]] +name = "radroots_sdk_ffi" +version = "0.1.0-alpha" +dependencies = [ + "radroots_sdk", + "thiserror 1.0.69", + "tokio", + "uniffi", +] + +[[package]] +name = "radroots_sdk_sql_wasm_runtime" +version = "0.1.0-alpha" +dependencies = [ + "radroots_sql_core", + "serde", + "serde-wasm-bindgen", + "serde_json", + "wasm-bindgen", +] + +[[package]] name = "radroots_secrets" version = "0.1.0-alpha" dependencies = [ @@ -2924,7 +3382,7 @@ dependencies = [ "sqlx", "tempfile", "tokio", - "toml", + "toml 0.8.23", ] [[package]] @@ -2971,6 +3429,10 @@ dependencies = [ ] [[package]] +name = "radroots_trade_bindings" +version = "0.1.0-alpha" + +[[package]] name = "radroots_transport" version = "0.1.0-alpha" dependencies = [ @@ -3099,6 +3561,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] name = "rcgen" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3197,6 +3668,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3258,6 +3767,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3321,6 +3836,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -3342,6 +3858,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] name = "salsa20" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3351,12 +3873,41 @@ dependencies = [ ] [[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] name = "scrypt" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3456,6 +4007,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -3468,6 +4023,17 @@ dependencies = [ ] [[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3510,6 +4076,18 @@ dependencies = [ ] [[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] name = "sha1" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3584,6 +4162,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3596,6 +4180,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] name = "sntrup761" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3776,6 +4366,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3810,6 +4406,15 @@ dependencies = [ ] [[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] name = "synstructure" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3845,6 +4450,15 @@ dependencies = [ ] [[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4017,6 +4631,15 @@ dependencies = [ [[package]] name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" @@ -4057,6 +4680,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4089,6 +4757,12 @@ dependencies = [ ] [[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] name = "tungstenite" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4153,6 +4827,127 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml 0.5.11", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "toml 0.5.11", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4276,6 +5071,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4359,6 +5173,45 @@ dependencies = [ ] [[package]] +name = "wasm-bindgen-test" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4403,6 +5256,16 @@ dependencies = [ ] [[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4421,6 +5284,15 @@ dependencies = [ ] [[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] name = "which" version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4449,6 +5321,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4867,7 +5748,7 @@ dependencies = [ "sha2", "syn 2.0.117", "tempfile", - "toml", + "toml 0.8.23", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -36,6 +36,18 @@ members = [ "crates/mesh", "crates/mesh_agent_client", "crates/mesh_agent_proto", + "crates/core_bindings", + "crates/event_bindings", + "crates/event_codec_wasm", + "crates/identity_bindings", + "crates/radroots", + "crates/replica_schema_bindings", + "crates/replica_store_wasm", + "crates/replica_sync_wasm", + "crates/sdk", + "crates/sdk_ffi", + "crates/sdk_sql_wasm_runtime", + "crates/trade_bindings", "tools/xtask", ] default-members = [ @@ -56,6 +68,8 @@ default-members = [ "crates/transport_nostr", "crates/sync", "crates/geonames", + "crates/sdk", + "crates/radroots", "tools/xtask", ] resolver = "3" @@ -89,12 +103,18 @@ unimplemented = "deny" [workspace.dependencies] dto_bindgen = { version = "0.1.0" } +dto_bindgen_backend_ts = { version = "0.1.0" } dto_bindgen_core = { version = "0.1.0" } +radroots = { path = "crates/radroots", version = "=0.1.0-alpha" } +radroots_core_bindings = { path = "crates/core_bindings", version = "=0.1.0-alpha" } radroots_core = { package = "radroots_core", path = "crates/core", version = "=0.1.0-alpha", default-features = false } radroots_event = { package = "radroots_event", path = "crates/event", version = "=0.1.0-alpha", default-features = false } radroots_event_codec = { package = "radroots_event_codec", path = "crates/event_codec", version = "=0.1.0-alpha", default-features = false } +radroots_event_bindings = { path = "crates/event_bindings", version = "=0.1.0-alpha" } +radroots_event_codec_wasm = { path = "crates/event_codec_wasm", version = "=0.1.0-alpha" } radroots_blossom = { package = "radroots_blossom", path = "crates/blossom", version = "=0.1.0-alpha", default-features = false } radroots_identity = { package = "radroots_identity", path = "crates/identity", version = "=0.1.0-alpha", default-features = false } +radroots_identity_bindings = { path = "crates/identity_bindings", version = "=0.1.0-alpha" } radroots_nostr = { package = "radroots_nostr", path = "crates/nostr", version = "=0.1.0-alpha", default-features = false } radroots_nostr_connect = { package = "radroots_nostr_connect", path = "crates/nostr_connect", version = "=0.1.0-alpha", default-features = false } radroots_nostrdb = { path = "crates/nostrdb", version = "=0.1.0-alpha", default-features = false } @@ -112,8 +132,11 @@ radroots_simplex_smp_transport = { path = "crates/simplex_smp_transport", versio radroots_sql_core = { path = "crates/sql_core", version = "=0.1.0-alpha", default-features = false } radroots_test_fixtures = { path = "crates/test_fixtures", version = "=0.1.0-alpha" } radroots_replica_schema = { path = "crates/replica_schema", version = "=0.1.0-alpha", default-features = false } +radroots_replica_schema_bindings = { path = "crates/replica_schema_bindings", version = "=0.1.0-alpha" } radroots_replica_sync = { path = "crates/replica_sync", version = "=0.1.0-alpha", default-features = false } radroots_replica_store = { path = "crates/replica_store", version = "=0.1.0-alpha", default-features = false } +radroots_replica_store_wasm = { path = "crates/replica_store_wasm", version = "=0.1.0-alpha" } +radroots_replica_sync_wasm = { path = "crates/replica_sync_wasm", version = "=0.1.0-alpha" } radroots_geonames = { package = "radroots_geonames", path = "crates/geonames", version = "=0.1.0-alpha", default-features = false } radroots_secrets = { package = "radroots_secrets", path = "crates/secrets", version = "=0.1.0-alpha", default-features = false } radroots_signing = { package = "radroots_signing", path = "crates/signing", version = "=0.1.0-alpha", default-features = false } @@ -126,6 +149,10 @@ radroots_mesh = { path = "crates/mesh", version = "=0.1.0-alpha", default-featur radroots_mesh_agent_client = { path = "crates/mesh_agent_client", version = "=0.1.0-alpha", default-features = false } radroots_mesh_agent_proto = { path = "crates/mesh_agent_proto", version = "=0.1.0-alpha", default-features = false } radroots_simplex_app_store = { path = "crates/simplex_app_store", version = "=0.1.0-alpha", default-features = false } +radroots_sdk = { path = "crates/sdk", version = "=0.1.0-alpha", default-features = false } +radroots_sdk_ffi = { path = "crates/sdk_ffi", version = "=0.1.0-alpha" } +radroots_sdk_sql_wasm_runtime = { path = "crates/sdk_sql_wasm_runtime", version = "=0.1.0-alpha" } +radroots_trade_bindings = { path = "crates/trade_bindings", version = "=0.1.0-alpha" } anyhow = { version = "1" } aes-gcm = { version = "0.10.3", default-features = false, features = [ @@ -177,6 +204,7 @@ serde = { version = "1", default-features = false, features = [ "alloc", ] } serde_json = { version = "1", default-features = false, features = ["alloc"] } +serde-wasm-bindgen = { version = "0.6" } secp256k1 = { version = "0.29.1", default-features = false, features = [ "alloc", ] } @@ -224,6 +252,9 @@ unicode-general-category = { version = "=1.1.0" } url = { version = "2" } url_nostd = { package = "url", version = "2", default-features = false } uuid = { version = "1.22.0", features = ["v4", "v7"] } +uniffi = { version = "0.29.4" } +wasm-bindgen = { version = "0.2" } +wasm-bindgen-test = { version = "0.3" } x509-parser = { version = "0.17", default-features = false } zstd = { version = "0.13", default-features = false } zeroize = { version = "1" } diff --git a/contracts/consolidation/history.v1.toml b/contracts/consolidation/history.v1.toml @@ -47,6 +47,16 @@ emergency_fix_flow = "fix canonical owner, qualify, then replay the normalized p divergence_policy = "forbidden" exit_condition = "all consumers use one qualified lib revision and rollback evidence is green" +[[import]] +source_id = "sdk" +frozen_commit = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" +filtered_head = "016a58c4b5a2edb020a372bc08d49a1865705737" +artifact = "sdk.commit-map.v1.json" +sha256 = "b23788e3e13e2e6f13e4b6294ace6cdb6bae6f74495dc8432327cfbb218c0f24" +bytes = 515435 +final_tree_sha256 = "456b9249343ee4fd21f7c8cda642a941062af97382520e54eefc13113da382cd" +path_map_sha256 = "c5a4a696db94bec4658b62fe775f96b81e7ae913b5dedc10448a72a9f1bf3725" + [[archive]] source_id = "sdk" kind = "git_bundle" @@ -55,10 +65,10 @@ artifact = "sdk-170ecf2b620107fadca85fcb11fbf3798b4ca1ef.bundle" sha256 = "9d2b015bbcd5d516994eeb72064b1e938a04acd0c21eb2e22134b99599fdd2f0" bytes = 2020833 source_commit_count = 427 -source_path_commit_count = 397 +source_path_commit_count = 393 rewritten_commit_count = 395 -topology_support_commit_count = 0 -omitted_empty_commit_count = 2 +topology_support_commit_count = 2 +omitted_empty_commit_count = 0 source_object_count = 6077 refname = "refs/heads/master" bot_identity_scan = true diff --git a/contracts/consolidation/imports/sdk.commit-map.v1.json b/contracts/consolidation/imports/sdk.commit-map.v1.json @@ -0,0 +1,11775 @@ +{ + "schema_version": 1, + "schema": "radroots.history-import.commit-map.v1", + "source_id": "sdk", + "frozen_commit": "170ecf2b620107fadca85fcb11fbf3798b4ca1ef", + "filtered_head": "016a58c4b5a2edb020a372bc08d49a1865705737", + "source_commit_count": 427, + "source_path_commit_count": 393, + "rewritten_commit_count": 395, + "topology_support_commit_count": 2, + "omitted_empty_commit_count": 0, + "final_tree_sha256": "456b9249343ee4fd21f7c8cda642a941062af97382520e54eefc13113da382cd", + "path_map_sha256": "c5a4a696db94bec4658b62fe775f96b81e7ae913b5dedc10448a72a9f1bf3725", + "verifications": [ + "archive_restore", + "git_fsck", + "mapped_parent_closure", + "commit_count", + "path_coverage", + "normalized_patch_equivalence", + "final_tree_digest", + "authorship", + "timestamps", + "message_scope_only", + "git_log_follow", + "secret_scan", + "license_scan", + "bot_identity_scan", + "github_workflow_exclusion", + "context_firewall" + ], + "commits": [ + { + "source_commit": "010b90e6a19b30b257ffe7ebb91f284f387ec185", + "target_commit": "2b2e8ee930ae5ec78f2bc960e2b099926ccac6b4", + "source_parents": [ + "aac11ecc400624d8789c1dc99cc9fd5681f8a7e7" + ], + "target_parents": [ + "dd0c154c255346d99abd7baa4ac675a61ea56264" + ], + "source_subject": "sdk: harden farm location coverage", + "target_subject": "sdk: harden farm location coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-27T01:19:04Z", + "target_author_time": "2026-06-27T01:19:04Z", + "source_committer_time": "2026-06-27T01:19:04Z", + "target_committer_time": "2026-06-27T01:19:04Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/market_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "6f2cad0cce1ffd74e40c9aa073ec93a3324241ca4fbaff311b565acfecde1a6f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "011e4137dc62321e76ad03e842c6463ede1071fa", + "target_commit": "e0e4f14d2db5e35027235b7f5c4095884bce420d", + "source_parents": [ + "b81acdfee8c5ac031641b2e0f2a6f5fd801a2d9f" + ], + "target_parents": [ + "86c6fa3219fa158b888a01c50d3f8ecf5541bf0c" + ], + "source_subject": "status: enforce validation trust policy", + "target_subject": "status: enforce validation trust policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-04T01:48:09Z", + "target_author_time": "2026-07-04T01:48:09Z", + "source_committer_time": "2026-07-04T01:48:09Z", + "target_committer_time": "2026-07-04T01:48:09Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "bb4ef943bc787bd91bff67d12a11afaf1145047700ca8d955a7bdc6d73f227c2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "011f929fc8575170293e15f38556c124c0cf3934", + "target_commit": null, + "source_parents": [ + "2d05abc48a5785496e4a4f977e001ca23091bcf0" + ], + "target_parents": [], + "source_subject": "test(sdk): capture current radroots generated baseline", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:13:23-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:13:23-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "012f64d4180fdc68108d53933ea8eaa1251cec2e", + "target_commit": null, + "source_parents": [ + "a7e497dc748b64bb3831b05205081f56aa1dd7b6" + ], + "target_parents": [], + "source_subject": "docs: synchronize coverage amendment", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-28T18:00:30Z", + "target_author_time": null, + "source_committer_time": "2026-07-28T18:00:30Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "01e989020cdb3632400b4a22e4702494146033f1", + "target_commit": "028b84f7f095e2758c2ee2f7a857c03248a6d833", + "source_parents": [ + "627b6823c9f3f44f0e1bd256ba019c92a046e22d" + ], + "target_parents": [ + "e7aa04171d9f8972c8cce69129df0efec2858ce8" + ], + "source_subject": "sdk: add public outcome label helpers", + "target_subject": "sdk: add public outcome label helpers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T02:08:40Z", + "target_author_time": "2026-07-10T02:08:40Z", + "source_committer_time": "2026-07-10T02:08:40Z", + "target_committer_time": "2026-07-10T02:08:40Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "d190b782b8c6251e59bd5ded74681007d1f3f913c87f933df35ae91fb794f203", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "026dbe08a91c97f1a555d0507f3c43e10768eb91", + "target_commit": "eb7590c3fd0d587d8dc9ae503dee4ce9e60660c2", + "source_parents": [ + "5ba3d4d817e8fd17e20b791dcaae9a6869d483e9" + ], + "target_parents": [ + "218cdabd7196ba33a4a11319276d14de430bfd63" + ], + "source_subject": "xtask: make WASM smoke deterministic", + "target_subject": "xtask: make WASM smoke deterministic", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T05:01:56Z", + "target_author_time": "2026-07-06T05:01:56Z", + "source_committer_time": "2026-07-06T05:01:56Z", + "target_committer_time": "2026-07-06T05:01:56Z", + "source_paths": [ + "tools/xtask/src/smoke.rs", + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "b8d6586372166cf984097a5ac7996008d0d5ab26cab0dafb35f3ea86b6b03ad2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "03c568672034f9d33e896c9585eaea2362c664d6", + "target_commit": "4a5e94fd492c0d1b870cf45d262f0d340625ea13", + "source_parents": [ + "9aa7d727eff8cc342fe698f53903e1339772ebf0" + ], + "target_parents": [ + "70cfc205200e56f45dbfa76b983b50b7db13bbb2" + ], + "source_subject": "sdk: align package manifest and module root", + "target_subject": "sdk: align package manifest and module root", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:12:14Z", + "target_author_time": "2026-08-03T10:12:14Z", + "source_committer_time": "2026-08-03T10:12:14Z", + "target_committer_time": "2026-08-03T10:12:14Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/capability.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/diagnostics.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/src/storage.rs", + "crates/sdk/src/sync.rs", + "crates/sdk/src/trade.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "61e2dd4624aff045de9a1826043ffda41bc5cdba277ac30ffdd6b78576d7cc4b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "041f0f1ca9d128b00f6c72482b07da204d56e29b", + "target_commit": "fe9cf3259f3b663dc5c583c700d9539967b4c60b", + "source_parents": [ + "8274e3f41691db7a35678d20c02ba45878bf140b" + ], + "target_parents": [ + "9f2a3205ba3aa4b27275bef9842659ae17589dcc" + ], + "source_subject": "sdk: own Rust SDK and wasm packages", + "target_subject": "sdk: own Rust SDK and wasm packages", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-13T15:08:32-07:00", + "target_author_time": "2026-06-13T15:08:32-07:00", + "source_committer_time": "2026-06-13T15:08:32-07:00", + "target_committer_time": "2026-06-13T15:08:32-07:00", + "source_paths": [ + "crates/replica_sync_wasm/Cargo.toml", + "crates/replica_sync_wasm/README", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/relay.rs", + "crates/sdk/src/adapters/signer.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/config.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/identity.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/profile.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/config.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/radrootsd.rs", + "crates/sdk/tests/relay_direct.rs", + "crates/sdk/tests/replica_ingest.rs" + ], + "normalized_patch_sha256": "3554e258101e64bcc49a11cd81899311a8f08ec6331432caf60563bd5937554c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0455d7211ac01f0e8a6d15908285a90202bebc17", + "target_commit": "67e5089b435d89f763b73a60ed3b23e5a9f6a374", + "source_parents": [ + "a97cf153fdfd4374945c0aa83aa7026d3be4e425" + ], + "target_parents": [ + "1860ba74d6775b99b5aa5b14826a9f70d737f6f2" + ], + "source_subject": "sync: harden radrootsd proxy target boundaries", + "target_subject": "sync: harden radrootsd proxy target boundaries", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T19:40:19Z", + "target_author_time": "2026-07-07T19:40:19Z", + "source_committer_time": "2026-07-07T19:40:19Z", + "target_committer_time": "2026-07-07T19:40:19Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "1735565e356aefbd507ce8b83ab30bcc5822331a7457f633defc8107b3e15540", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0477bfd5b0cd2d6f0e6a54e7325c6dad06cd9ea6", + "target_commit": "bbac9fc9c3985ebcb4bc76b34501003caf582a87", + "source_parents": [ + "ae53d4a6a319b553e942298d86aee0846e908d11" + ], + "target_parents": [ + "1ad0ee8ee58845c5f538311214934448157467d8" + ], + "source_subject": "tests: guard trade feature matrix", + "target_subject": "tests: guard trade feature matrix", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T21:16:25Z", + "target_author_time": "2026-06-30T21:16:25Z", + "source_committer_time": "2026-06-30T21:16:25Z", + "target_committer_time": "2026-06-30T21:16:25Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_public_api.rs" + ], + "normalized_patch_sha256": "6d1112154cbe0a115c887fd347b00a255827b02c17867bfbd9d549ffbce807bd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "05dabaa5aef5b44c7979c33fe53cebea525c8eef", + "target_commit": "3aa5ea50bc6b7e313357fb243a266da70af8cc6b", + "source_parents": [ + "7a2a8d817d8c7ac0038aa46957c1d4973ed5e973" + ], + "target_parents": [ + "68aa381171e6184587537c13c6959127cd139e3d" + ], + "source_subject": "bindings: migrate canonical rust type mappings", + "target_subject": "bindings: migrate canonical rust type mappings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:17:38Z", + "target_author_time": "2026-08-03T13:17:38Z", + "source_committer_time": "2026-08-03T13:17:38Z", + "target_committer_time": "2026-08-03T13:17:38Z", + "source_paths": [ + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "ee1799f63f8f075a3f71842d0179b68c67dcd0b7af06f7705a52ef5415eebce6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "06bb24bab135a934271d11f1b637f622fe317a8b", + "target_commit": null, + "source_parents": [ + "6a362d320382a1432891a2c430a26129c37b45da" + ], + "target_parents": [], + "source_subject": "contracts: own sdk export metadata", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-22T01:34:37Z", + "target_author_time": null, + "source_committer_time": "2026-06-22T01:34:37Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "06dd4197888e88677817773173b7104e81410a88", + "target_commit": "bcb30a70846629bc858c563200ac21e8b9ab79e7", + "source_parents": [ + "be6b479c8a62cb25b89791eaa7b0966b11603051" + ], + "target_parents": [ + "682509146a99903c5eccb39e88607223f165de94" + ], + "source_subject": "facade: complete package conformance coverage", + "target_subject": "facade: complete package conformance coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:59:09Z", + "target_author_time": "2026-08-03T12:59:09Z", + "source_committer_time": "2026-08-03T12:59:09Z", + "target_committer_time": "2026-08-03T12:59:09Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "ed51a1ff8f9feb957173c16315444222c56048f465af967a438b64d02067ffd1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "082ef3b82ab1e69f070421345e1cf2ace8cfe6ab", + "target_commit": null, + "source_parents": [ + "c340d53ade6ffa40347706d1956f94ed6bc42679" + ], + "target_parents": [], + "source_subject": "test(sdk): add binding import smoke", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:26:55-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:26:55-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "0925f5849cc7af3dc70b8827f24187b57e0b24bc", + "target_commit": "b23996d81a0d7d1c4ea99caa54d610d90ca9f110", + "source_parents": [ + "4e3e52b300904045023980f91379b00e097f1c75" + ], + "target_parents": [ + "a67cf7a93a9462341307d85f361122f4e0f01d07" + ], + "source_subject": "nostr: quarantine superseded package surface", + "target_subject": "nostr: quarantine superseded package surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T16:29:02Z", + "target_author_time": "2026-07-31T16:29:02Z", + "source_committer_time": "2026-07-31T16:29:02Z", + "target_committer_time": "2026-07-31T16:29:02Z", + "source_paths": [ + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "36ac2e3b74b5de585ef274b61a5aa56c4f2bcdb664043018ba93ad8072500126", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0ac6929bf2882374f75a8eb737f3a9252a2fe9f8", + "target_commit": "21ddb9b338d217a868ff4ca64624506b740e4b1f", + "source_parents": [ + "9be9842a3d55b43d68e01b3966b710e98211220c" + ], + "target_parents": [ + "c258a0553e4b6c30cdddef86eacadaf05ad29004" + ], + "source_subject": "sdk: expose order enqueue retry advice", + "target_subject": "sdk: expose order enqueue retry advice", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T15:39:20-07:00", + "target_author_time": "2026-06-19T15:39:20-07:00", + "source_committer_time": "2026-06-19T15:39:20-07:00", + "target_committer_time": "2026-06-19T15:39:20-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "90c6b25d34f8afc9b622f8d7220ed9ffe9043af3b07f8422bc65eacf8ce2df76", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0b18741e0912f193aabc67357862ea4a18fd725e", + "target_commit": "924d91fdf4a7062c8e071610e56dcfa24003652c", + "source_parents": [ + "f5038dbc581eb48f4b238b7a5e2732948281abfb" + ], + "target_parents": [ + "06b5d28cd6a4c4388117db0757e4544c5fef95a0" + ], + "source_subject": "tests: gate local status performance", + "target_subject": "tests: gate local status performance", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T10:25:46Z", + "target_author_time": "2026-06-30T10:25:46Z", + "source_committer_time": "2026-06-30T10:25:46Z", + "target_committer_time": "2026-06-30T10:25:46Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "ab7f5cb44ac8f85a1522f5284f1e85909f3b62ad2b0c4c0c63646d739f9dd59a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0bcdc82efff93198de3fd8f53c9a2acd75a2148c", + "target_commit": "c2e68973a4e2027d891cff605cf08095186f66fb", + "source_parents": [ + "3cae34b3248f448691e05585b2030133260f5591" + ], + "target_parents": [ + "8980ce18804175ea0442ec8b8e27485f96cf9fd7" + ], + "source_subject": "sdk: align order status with trade workflow", + "target_subject": "sdk: align order status with trade workflow", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T07:17:06Z", + "target_author_time": "2026-06-26T07:17:06Z", + "source_committer_time": "2026-06-26T07:17:06Z", + "target_committer_time": "2026-06-26T07:17:06Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs" + ], + "normalized_patch_sha256": "b6c1aa172fe3b9c9124217da89be3f6e66e424ecb097be4a4d06561c6cd6634b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0bd19a81a23ee794481e684555a9175f18deb753", + "target_commit": "e13ca0e9e5862ab81a83c49ea85be73c8c3b6790", + "source_parents": [ + "5811685dc028eee45e1e0087b06ecd1a0b59fb1e" + ], + "target_parents": [ + "cea9597aa28d34ad5240836b1b2009905937d699" + ], + "source_subject": "transport: use shared proxy kind", + "target_subject": "transport: use shared proxy kind", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T09:14:28Z", + "target_author_time": "2026-07-07T09:14:28Z", + "source_committer_time": "2026-07-07T09:14:28Z", + "target_committer_time": "2026-07-07T09:14:28Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "521df94777419d4726dc1e5a2d715f8fec75d8e48fbf3fe84bc1fb3d10fc658a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0bd6b0a7fae7b4676b22bb721f5d6452863d0e89", + "target_commit": "0242454c74964a329ff2a2c304d6364f1d0a1675", + "source_parents": [ + "a5f2722e28bfc2e8969c7a368605f1fbbf1502ba" + ], + "target_parents": [ + "3d40e54f887c2916b84463f914ac8f65d30da79d" + ], + "source_subject": "tests: align proxy publish protocol fixtures", + "target_subject": "tests: align proxy publish protocol fixtures", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-14T19:18:41Z", + "target_author_time": "2026-07-14T19:18:41Z", + "source_committer_time": "2026-07-14T19:18:41Z", + "target_committer_time": "2026-07-14T19:18:41Z", + "source_paths": [ + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs" + ], + "normalized_patch_sha256": "edb9a9c3881e63442f5132a55c35468c1b99ca23157dcaa42694147565ea7b72", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0d0bf4dd72e19f44f60883244784e24271dc45e6", + "target_commit": "1590d50b1209393a5b69e36ecdecb75e59c41597", + "source_parents": [ + "c7c627d5d167106b876b0ce3ad34fa090143c61d" + ], + "target_parents": [ + "0d87b57798df35ea6f8e066f3bddf007573d4cc1" + ], + "source_subject": "sdk: add prepared listing enqueue", + "target_subject": "sdk: add prepared listing enqueue", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T22:32:35-07:00", + "target_author_time": "2026-06-15T22:32:35-07:00", + "source_committer_time": "2026-06-15T22:32:35-07:00", + "target_committer_time": "2026-06-15T22:32:35-07:00", + "source_paths": [ + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/tests/listings_runtime.rs" + ], + "normalized_patch_sha256": "7ea4e8a4c9d6a8807bf1df305c40d0a1cc2ef8e6b2c2b8c2d42c38b251e0bf77", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0d21ded103d6bccf8087fc27b824d2cc758c4372", + "target_commit": "dc41a3b3cd84b5da3b1ba8c2f94550b51e7c51b4", + "source_parents": [ + "f987cde525c7752152579bbed03a818d4658e171" + ], + "target_parents": [ + "b9bf25b4972dc36688fb823feafb0046c86602ce" + ], + "source_subject": "sdk: preserve required target proxy semantics", + "target_subject": "sdk: preserve required target proxy semantics", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T09:41:55Z", + "target_author_time": "2026-07-10T09:41:55Z", + "source_committer_time": "2026-07-10T09:41:55Z", + "target_committer_time": "2026-07-10T09:41:55Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "fc6c60fdc8e6ba987b7ec8e0a779f39a360855e01c679120e2024201d8af7114", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0d4ef63f34003da2c0c7872bd2aef0546a9041a6", + "target_commit": "4bbd8c68a2755f3e2ba9f02861818c02156b7c10", + "source_parents": [ + "fd8384aee348034e0c8ea17a868fe7f094770050" + ], + "target_parents": [ + "5ed07d69ae5739867e40aa706b5bce99d6f6cbb0" + ], + "source_subject": "transport: align sdk with sealed models", + "target_subject": "transport: align sdk with sealed models", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T20:38:52Z", + "target_author_time": "2026-07-27T20:38:52Z", + "source_committer_time": "2026-07-27T20:38:52Z", + "target_committer_time": "2026-07-27T20:38:52Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs" + ], + "normalized_patch_sha256": "0e1e7ca3c4347dc3c83c8bc15d518f82ea1ad0f7f53dddc97903e6f989ced6e0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0d774c10dcd16a9b58ff3efabb98d32e4e7f804e", + "target_commit": "f55b6d65023e475bf71faee8b7261965d7c5dee9", + "source_parents": [ + "8351f09962ab657d95b04f4d2e3ad3fe963a10d2" + ], + "target_parents": [ + "bba373365eadd4cbc4ef1e509f84200b00f5393d" + ], + "source_subject": "types: render bindings from dto registry", + "target_subject": "types: render bindings from dto registry", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T08:51:45Z", + "target_author_time": "2026-06-24T08:51:45Z", + "source_committer_time": "2026-06-24T08:51:45Z", + "target_committer_time": "2026-06-24T08:51:45Z", + "source_paths": [ + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "d50522561cefe00f1e93cbdd0d2f35e0e59a2d7c4e74dc1118289378a1d7d00a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "0ff9e2825de711d0e662fc3b3b5eda5d3cb6a38a", + "target_commit": "121ab276234198c523e1d01467e8f7cd7cedc9c7", + "source_parents": [ + "bffc3d0fe3ef8c541edacfa42daa3ab81e477641" + ], + "target_parents": [ + "111f143c5007830710b8f895ae62ab778c22c79e" + ], + "source_subject": "transport: separate radrootsd execution from transport identity", + "target_subject": "transport: separate radrootsd execution from transport identity", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T14:57:00Z", + "target_author_time": "2026-07-15T14:57:00Z", + "source_committer_time": "2026-07-15T14:57:00Z", + "target_committer_time": "2026-07-15T14:57:00Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "474b620af33b6bbd01898e015875dc07653b77a6e1fa655a8d89807837c9ea46", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "102c546191f9886958c5072582fee45ff1712919", + "target_commit": "2ba94943a3730ca9bb4eb5aa7c977ebba095b4ea", + "source_parents": [ + "e48c5227400710f2bf84234aae3827b6384937dd" + ], + "target_parents": [ + "d66458e494e87c1dc3c9ff1424e2ac8da93285e3" + ], + "source_subject": "sdk: use local-dev relay policy", + "target_subject": "sdk: use local-dev relay policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:15:38-07:00", + "target_author_time": "2026-06-16T00:15:38-07:00", + "source_committer_time": "2026-06-16T00:15:38-07:00", + "target_committer_time": "2026-06-16T00:15:38-07:00", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "cb8d9fba6c788932f7f46666cdbc6629465ce8ff2f2ec7b3bae68c2c75641a63", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "117b1a9a167f9935148aa430b72d1474c0389953", + "target_commit": null, + "source_parents": [], + "target_parents": [], + "source_subject": "Initial commit", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T05:42:56-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T05:42:56-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "14417d787b2a9216fc32c156972b21f515699f29", + "target_commit": "a1fbc9bde3039dff636afba0da88bfd9ad2d444d", + "source_parents": [ + "37b664841acf4f2fb052ec6ae3b139448a454a1b" + ], + "target_parents": [ + "1b7f454e9ba6d400b5f50cb776e42e3a507f7bc4" + ], + "source_subject": "facade: add safe convenience builders", + "target_subject": "facade: add safe convenience builders", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:43:16Z", + "target_author_time": "2026-08-03T12:43:16Z", + "source_committer_time": "2026-08-03T12:43:16Z", + "target_committer_time": "2026-08-03T12:43:16Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/src/client.rs", + "crates/radroots/src/storage.rs", + "crates/radroots/src/transport.rs", + "crates/radroots/tests/public_surface.rs", + "crates/sdk/src/client.rs" + ], + "normalized_patch_sha256": "2ca6c5a0fcc46d15f4346f2f1fbeed9dcda0a2ed46b9008a501e2c2f1a7a7e64", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "1546416bcb46198042d8a373ba7f8cdc3a9e496f", + "target_commit": "8b9fc2017a6bacf6f96d94e3a7b97b1611d26d29", + "source_parents": [ + "222e5ec6f15f42f097b655d7620b44c0aabd88a8" + ], + "target_parents": [ + "72ff0b9f324e86e00b93e280693d810a287b2ccf" + ], + "source_subject": "sdk: add order submit runtime", + "target_subject": "sdk: add order submit runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T20:37:50-07:00", + "target_author_time": "2026-06-18T20:37:50-07:00", + "source_committer_time": "2026-06-18T20:37:50-07:00", + "target_committer_time": "2026-06-18T20:37:50-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "36e27f5b6fcfb94da7507c192e9c27effafdb828b74ec4f30adcbc0fd58077f7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "16bdb7f65ec8ff902041d5736e6773e1684a5560", + "target_commit": "023faeb5a806a3494ebc08b42b0bda441dac41f7", + "source_parents": [ + "79f1018994c344f59a9b342cd43493b62d3c771f" + ], + "target_parents": [ + "7b48b68af0eaa118340c4e1ef128e4bc2972f06b" + ], + "source_subject": "sdk: refactor trade commands and queries", + "target_subject": "sdk: refactor trade commands and queries", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:40:53Z", + "target_author_time": "2026-08-03T11:40:53Z", + "source_committer_time": "2026-08-03T11:40:53Z", + "target_committer_time": "2026-08-03T11:40:53Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/trade.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs" + ], + "normalized_patch_sha256": "98637ee0c8fd3e3d13dbfe07ec581d57ae4610b91dd2525f60f8bdd160fe0e56", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "170ecf2b620107fadca85fcb11fbf3798b4ca1ef", + "target_commit": "016a58c4b5a2edb020a372bc08d49a1865705737", + "source_parents": [ + "d015b33f05653c786d2cc24fe9b0830bbdae4cda" + ], + "target_parents": [ + "b41566af5eb76943d9bed59469f5cb8ac61a95cc" + ], + "source_subject": "refactor(sdk): adopt context-bound envelopes", + "target_subject": "refactor(sdk): adopt context-bound envelopes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-05T21:53:34Z", + "target_author_time": "2026-08-05T21:53:34Z", + "source_committer_time": "2026-08-05T21:53:34Z", + "target_committer_time": "2026-08-05T21:53:34Z", + "source_paths": [ + "crates/sdk/src/trade.rs", + "tools/xtask/src/release_graph.rs" + ], + "normalized_patch_sha256": "f2dc1fe5eddcee4c819375ff8525d9ae187cf86d4adca03b4309c50a5f79690b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "17273d9cdaaa567fc988dfccb7cc5b1c6d6348c9", + "target_commit": "1b89ecc4e6ea92ff9f8f416b08db22502a7a5dde", + "source_parents": [ + "418a3ca6860407f0bc38366e27d0c269202fbb65" + ], + "target_parents": [ + "6653844ddc926c1ab2999bcb766837a60c5291e9" + ], + "source_subject": "sdk: rename signed event adapter surfaces", + "target_subject": "sdk: rename signed event adapter surfaces", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T05:58:38Z", + "target_author_time": "2026-07-13T05:58:38Z", + "source_committer_time": "2026-07-13T05:58:38Z", + "target_committer_time": "2026-07-13T05:58:38Z", + "source_paths": [ + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/signing.rs" + ], + "normalized_patch_sha256": "c6913f2a73aae660e70f56a49dda05e2d852264bb0f3df9a26f311c8b0e0a2dd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "18346a27d3db11dc82b5020d239e89ff5837f8b0", + "target_commit": "90cc25893d2105fed3ca142958e4b15fb72cc22c", + "source_parents": [ + "762f56fd0a11769ed50cdc86c8f79f66f9bd5b09" + ], + "target_parents": [ + "0fb42cc9b6d618a4ac1e6b563501d10c4d4a76f7" + ], + "source_subject": "facade: add ordinary and advanced smoke examples", + "target_subject": "facade: add ordinary and advanced smoke examples", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:54:37Z", + "target_author_time": "2026-08-03T12:54:37Z", + "source_committer_time": "2026-08-03T12:54:37Z", + "target_committer_time": "2026-08-03T12:54:37Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/examples/ordinary_memory.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "7544c876a26e05943a27eb7df7d25312b3cbcc7fa0804a8552a6d72ba21dcbaf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "1a52b44a85b52eb65e5056ffc5dc565c887c34e2", + "target_commit": "7af64268eda3a7f9aa062b1a144c65beffbb27bd", + "source_parents": [ + "7b3e3ba1595879766f8037a689f20b552a9c999a" + ], + "target_parents": [ + "05530a10ed58910f9309e1560820c9091edf300e" + ], + "source_subject": "sdk: resync trades from configured relays", + "target_subject": "sdk: resync trades from configured relays", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T07:57:28Z", + "target_author_time": "2026-07-01T07:57:28Z", + "source_committer_time": "2026-07-01T07:57:28Z", + "target_committer_time": "2026-07-01T07:57:28Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/trade_public_api.rs" + ], + "normalized_patch_sha256": "3eedd427367d203778f9b6d8c3861e2edefbe9c5503bda36e9de5071683ccc84", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "1ce1da677c6992ce87129a59cd6db2a9a720e401", + "target_commit": "79f93f6cb9ba4251dcf54308271071f1fc453bd1", + "source_parents": [ + "4f13a8cf263fd45dd2794270a40058b752f3a27c" + ], + "target_parents": [ + "652c23d6068c67e859eed256ade32684125db0e4" + ], + "source_subject": "sdk: define explicit client lifecycle and shutdown", + "target_subject": "sdk: define explicit client lifecycle and shutdown", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:29:56Z", + "target_author_time": "2026-08-03T10:29:56Z", + "source_committer_time": "2026-08-03T10:29:56Z", + "target_committer_time": "2026-08-03T10:29:56Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "b2df5c960a3175e414c59b863e0dc7b24e6d11f859174483d0549c41f26f7413", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "1d05399c5eb1097620dfb44c87824e803f37fdd6", + "target_commit": "3166eee6d9f4c71916ee6db9bc49cbaac6139538", + "source_parents": [ + "d385e6ce54d9260255e5d93ac82ceb903f34004e" + ], + "target_parents": [ + "5e0dcf40bef08898f267fd6157617e79a02ee6be" + ], + "source_subject": "transport: guard proxy completion uniqueness", + "target_subject": "transport: guard proxy completion uniqueness", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T05:30:58Z", + "target_author_time": "2026-07-08T05:30:58Z", + "source_committer_time": "2026-07-08T05:30:58Z", + "target_committer_time": "2026-07-08T05:30:58Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "9ff51f3c350bf5b2c9c03140fd528763d37484f5d89134c947a2e9d019ffb627", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "1f51e9c575cefc3897e07c53a04d632be1d630bd", + "target_commit": "b7015b3ab3ad470e115d88475b9233c95717d7c4", + "source_parents": [ + "b89497727278225eb12f243eca82b77affe5d929" + ], + "target_parents": [ + "fedbccb3bdff2d61d721b9db3119182189ecf658" + ], + "source_subject": "packages: generate npm metadata", + "target_subject": "packages: generate npm metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T04:04:05Z", + "target_author_time": "2026-06-28T04:04:05Z", + "source_committer_time": "2026-06-28T04:04:05Z", + "target_committer_time": "2026-06-28T04:04:05Z", + "source_paths": [ + "tools/xtask/src/check.rs", + "tools/xtask/src/fs.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/package_metadata.rs" + ], + "normalized_patch_sha256": "6095c2274ad490f1ebbdc9b0fc922f5c04056da8e522392f436bd5341bc241cf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "208e02d55f433fabe4f46320a577104929c20db2", + "target_commit": "9656841cfa620eca297778e5414475ab50643edc", + "source_parents": [ + "0b18741e0912f193aabc67357862ea4a18fd725e" + ], + "target_parents": [ + "924d91fdf4a7062c8e071610e56dcfa24003652c" + ], + "source_subject": "docs: align sdk product readme", + "target_subject": "docs: align sdk product readme", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T11:04:56Z", + "target_author_time": "2026-06-30T11:04:56Z", + "source_committer_time": "2026-06-30T11:04:56Z", + "target_committer_time": "2026-06-30T11:04:56Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "7c33a6d136fa6859aaece8b0fc0b653f24c4d6b140ea098e011a896958d11c86", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "213cdd50978561f954e6461cab6aafa70aa5ba83", + "target_commit": "6b5512f76de02819b2db90ddc4f3f0bc1b17af6a", + "source_parents": [ + "b7d90cdf61e4dbb31c6ed3f72df9e450827dabef" + ], + "target_parents": [ + "9b92a55a4dcfb91d91aa41d4cd347c27dc98e48d" + ], + "source_subject": "fix(release): preserve standalone sdk checks", + "target_subject": "fix(release): preserve standalone sdk checks", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T11:11:36Z", + "target_author_time": "2026-08-04T11:11:36Z", + "source_committer_time": "2026-08-04T11:11:36Z", + "target_committer_time": "2026-08-04T11:11:36Z", + "source_paths": [ + "tools/xtask/src/cli_host.rs" + ], + "normalized_patch_sha256": "f6ad37046fcd313f3a3b880145532fb0fcdc57506e1d4ef5b8f4b14bd375c3bf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "21543db0546b0768e8b3d114d7af4ae36c7e5607", + "target_commit": "586de76e625aeea89b9670470657960884f1ff3c", + "source_parents": [ + "f96206c243f1d3ea3db4e60133c27d6388b238ac" + ], + "target_parents": [ + "97fb7277a2510561461d9254beadea365d7c90e1" + ], + "source_subject": "workspace: adopt resolver 3 and rust 1.97.1", + "target_subject": "workspace: adopt resolver 3 and rust 1.97.1", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T08:19:46Z", + "target_author_time": "2026-07-27T08:19:46Z", + "source_committer_time": "2026-07-27T08:19:46Z", + "target_committer_time": "2026-07-27T08:19:46Z", + "source_paths": [ + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "ae4cc88c6aff71878d9eedca42cc0591c6727a572162c0cf7890fef599461f91", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "222e5ec6f15f42f097b655d7620b44c0aabd88a8", + "target_commit": "72ff0b9f324e86e00b93e280693d810a287b2ccf", + "source_parents": [ + "7ad62f8c3952f2a330e74234306d446ec0a9cc0d" + ], + "target_parents": [ + "9650fb7e4c03986ed44e6002779157244bb1f952" + ], + "source_subject": "sdk: add farm publish runtime", + "target_subject": "sdk: add farm publish runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T20:26:01-07:00", + "target_author_time": "2026-06-18T20:26:01-07:00", + "source_committer_time": "2026-06-18T20:26:01-07:00", + "target_committer_time": "2026-06-18T20:26:01-07:00", + "source_paths": [ + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "0ca23dda737407987e7fa554471b509034cc371b240c574efdaf2b46484de5c5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2338e109ef2ffa527f3ac4219817071acbdfbf14", + "target_commit": "93ca97a75b53d83a4c4fcc8d23e7b5437d5dbab5", + "source_parents": [ + "baf6f683acd0c16a2f3a53be85f6fa1e67cf83eb" + ], + "target_parents": [ + "731029342394b7253a20818d07806f06a7c68f29" + ], + "source_subject": "sdk: harden release product bindings", + "target_subject": "sdk: harden release product bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T12:46:45Z", + "target_author_time": "2026-07-16T12:46:45Z", + "source_committer_time": "2026-07-16T12:46:45Z", + "target_committer_time": "2026-07-16T12:46:45Z", + "source_paths": [ + "crates/event_bindings/src/lib.rs", + "crates/event_bindings/src/model.rs", + "crates/sdk/README", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "208ac3bd8f3c224056e5e4f9f2e2724a8b25bb820392d69985640821269fed94", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "235f088795e2849f1a99574f5ca68c22d75b8d0b", + "target_commit": "646e3c75b633b03aac6d1828632dd1719ed3b8fc", + "source_parents": [ + "213cdd50978561f954e6461cab6aafa70aa5ba83" + ], + "target_parents": [ + "6b5512f76de02819b2db90ddc4f3f0bc1b17af6a" + ], + "source_subject": "build(release): stage sdk crate publication", + "target_subject": "build(release): stage sdk crate publication", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T11:38:03Z", + "target_author_time": "2026-08-04T11:38:03Z", + "source_committer_time": "2026-08-04T11:38:03Z", + "target_committer_time": "2026-08-04T11:38:03Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/sdk/Cargo.toml", + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "430d9b4da5608290ecbf53b9b884fdddbc876189f6811334ef5d300daed83306", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2452accade2358dc65a38fb22f654d2eab60fd2e", + "target_commit": "b092782e95485fccb5a9d4c0b07dfa832e019ded", + "source_parents": [ + "a307d2d00aed4d37b846e6668cf482e44b3a60e4" + ], + "target_parents": [ + "fc2f1429f372153731a594808a4abb001a07c4d6" + ], + "source_subject": "architecture: add deviation and traceability controls", + "target_subject": "architecture: add deviation and traceability controls", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T08:02:11Z", + "target_author_time": "2026-07-27T08:02:11Z", + "source_committer_time": "2026-07-27T08:02:11Z", + "target_committer_time": "2026-07-27T08:02:11Z", + "source_paths": [ + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "d1f60df44160a6b6a34c2958635ffdac80839f8becb863c857b6017431eb3919", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "25afa54bbc5c2f35aa522f25021532a84b878e5e", + "target_commit": "c0ba705ff19c5951ab5ed84dc0e2d4cf5af7c577", + "source_parents": [ + "5ba42d6b2ff0f3d618d4c79478554850c2ef10a2" + ], + "target_parents": [ + "e86ac0f3532d0513a8f61ec05287bec2ac0fd5cc" + ], + "source_subject": "tests: cover Hybrid product publish satisfaction", + "target_subject": "tests: cover Hybrid product publish satisfaction", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T01:20:04Z", + "target_author_time": "2026-07-09T01:20:04Z", + "source_committer_time": "2026-07-09T01:20:04Z", + "target_committer_time": "2026-07-09T01:20:04Z", + "source_paths": [ + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs" + ], + "normalized_patch_sha256": "8b988ed8e08c319ca7d9ddbf933f58afe0ec0d57f751e8a74e291b427f32c6e3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "25d556cc18d69229df5d688b4062d9e30d0e9d2f", + "target_commit": "967c0bafd608036397f5919e2591db82f9cc0ed6", + "source_parents": [ + "abccef4bc45a47e357dd2841b2111db638055cee" + ], + "target_parents": [ + "cecaf705e888815821007710503389c2fe357883" + ], + "source_subject": "xtask: validate dto import overrides", + "target_subject": "xtask: validate dto import overrides", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-27T20:47:15Z", + "target_author_time": "2026-06-27T20:47:15Z", + "source_committer_time": "2026-06-27T20:47:15Z", + "target_committer_time": "2026-06-27T20:47:15Z", + "source_paths": [ + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "49caa8ae17edaa0b25611e9e4f7e98b4e1b29dc499e6048f7fbfb0989bb5f9a3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "267e76547d08db145df6133dc2e8ffbf25b48e50", + "target_commit": "105dea109a2356188dce16ecdebb3864e798f3f8", + "source_parents": [ + "d19a688128087c9c62b767b9fdd9e571a3e9add9" + ], + "target_parents": [ + "ff48c3eff6888f7e0aa677bd63ac33714c88ddfd" + ], + "source_subject": "sdk: quarantine NIP-46 transition surface", + "target_subject": "sdk: quarantine NIP-46 transition surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-01T07:15:00Z", + "target_author_time": "2026-08-01T07:15:00Z", + "source_committer_time": "2026-08-01T07:15:00Z", + "target_committer_time": "2026-08-01T07:15:00Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "cee176f030863e03f19760e8b6773f4fe13745d487a663a17428a27babedefe1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "26e03efae10d928d57a0a979be1f39327ade52d4", + "target_commit": "9a680cced44a26ed41491386d1057c4d23c77642", + "source_parents": [ + "e2a22515e1ff15baca756071ec5a0df082cc9c19" + ], + "target_parents": [ + "836e5e7cd2888bbbc5e5d504fb91e6f4ae5855d9" + ], + "source_subject": "sdk: cover product trade lifecycle mutations", + "target_subject": "sdk: cover product trade lifecycle mutations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T04:09:16Z", + "target_author_time": "2026-06-30T04:09:16Z", + "source_committer_time": "2026-06-30T04:09:16Z", + "target_committer_time": "2026-06-30T04:09:16Z", + "source_paths": [ + "crates/sdk/src/privacy.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "66f61796abbad50f71095e207a413799b2c626cc8b090537fa1369ac92bb56b8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "27c621961ecfd7fcd0c1aa3513d285596378ccd6", + "target_commit": "db9845f621613817827051d3d3d863597879d567", + "source_parents": [ + "6435ca9c20de00ce8cf0c832068d90f3607412cf" + ], + "target_parents": [ + "89406244f80d695a8b0d7d24f8fdebdd164ad12f" + ], + "source_subject": "sdk: migrate synchronization to radroots_sync", + "target_subject": "sdk: migrate synchronization to radroots_sync", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:16:26Z", + "target_author_time": "2026-08-03T11:16:26Z", + "source_committer_time": "2026-08-03T11:16:26Z", + "target_committer_time": "2026-08-03T11:16:26Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/sync.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "4923d5ec331c9313ec57333e57bc4d82d1b6e336b69200d49213555912281e16", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2932428d1fd3aab889b3044ba98e7e0843a58956", + "target_commit": "4232c5e9c6dca190027ed09be88d56f9d3d34468", + "source_parents": [ + "06bb24bab135a934271d11f1b637f622fe317a8b" + ], + "target_parents": [ + "b3094a4922f880ddc75d50e558ccf1f15632549b" + ], + "source_subject": "xtask: move automation to tools", + "target_subject": "xtask: move automation to tools", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T01:36:40Z", + "target_author_time": "2026-06-23T01:36:40Z", + "source_committer_time": "2026-06-23T01:36:40Z", + "target_committer_time": "2026-06-23T01:36:40Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/fs.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/manifest.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs", + "tools/xtask/src/ts.rs", + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "675e770924f9154d09b1d51d9633afbae9007fb8057a8c53dc714a10c31928d7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "29a7d1f5b96eabc36dbab20eacc3b2947a5f22ee", + "target_commit": "b28d22eaf1be1bedbc417a305c16497162c6ea8c", + "source_parents": [ + "6060e2303856227a0f78207e95926f09b99b62f9" + ], + "target_parents": [ + "f54abd7f296006006d855ab37a20eba56e2c78d6" + ], + "source_subject": "sdk: publish targeted trade outbox events", + "target_subject": "sdk: publish targeted trade outbox events", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:41:06Z", + "target_author_time": "2026-06-30T03:41:06Z", + "source_committer_time": "2026-06-30T03:41:06Z", + "target_committer_time": "2026-06-30T03:41:06Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "daf05e443b5d9c4ad4cf2da1bb534ec1bdc6e082b561c1616edaa3b740c6b6e3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "29c10ac3f65668a38468f98ad361699de29aaafe", + "target_commit": null, + "source_parents": [ + "831f1eaf21ddbfd08c90e82fd4a4d4b9e0b46ba6" + ], + "target_parents": [], + "source_subject": "release-product: refresh advisory lock", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-16T22:06:22Z", + "target_author_time": null, + "source_committer_time": "2026-07-16T22:06:22Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "2a2d79afd5a9766e5e954f55c5b31d8f391dbfa9", + "target_commit": "8f8435669750f272889501b0fef0c173c32d3860", + "source_parents": [ + "425c2f721c6cefa4b90c0f4833e2fb080c45c4ec" + ], + "target_parents": [ + "6cf47f4a0daa32ea93580a848f97ed08578d9957" + ], + "source_subject": "sdk: add dvm validation runtime", + "target_subject": "sdk: add dvm validation runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T10:11:54Z", + "target_author_time": "2026-06-26T10:11:54Z", + "source_committer_time": "2026-06-26T10:11:54Z", + "target_committer_time": "2026-06-26T10:11:54Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs" + ], + "normalized_patch_sha256": "41f45bcfdece3c65209434a78ce3e630850bca2191d7d0edd19ebd19695b47e4", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2abc7f5c370201d18306f3e97c01fd98be557036", + "target_commit": "0a7e2668bc360b98a2c31b237ad26a93d7cca25d", + "source_parents": [ + "729bfba53ef37bf784ffa82fdb660797f2645c33" + ], + "target_parents": [ + "7b0e4b3ef0f7fd41a2dd13318d974d973a04fcd7" + ], + "source_subject": "sdk: pin unpublished lib dependencies by revision", + "target_subject": "sdk: pin unpublished lib dependencies by revision", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T20:15:48Z", + "target_author_time": "2026-08-04T20:15:48Z", + "source_committer_time": "2026-08-04T20:15:48Z", + "target_committer_time": "2026-08-04T20:15:48Z", + "source_paths": [ + "crates/sdk/src/sync.rs", + "crates/sdk/src/transport.rs" + ], + "normalized_patch_sha256": "f9c5c4e32f57031d656b13b725dbee9b98ab48fa1a0cd9e4f9be2d423c9d28fc", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2bb507c1cfce162e8d32f260cb6cf5254d6bd4c0", + "target_commit": "6fefb9dea42a970c106ad325ce040d122f355755", + "source_parents": [ + "36e28bc09cf7924a1f36c5a0135095d6526ab06a" + ], + "target_parents": [ + "4fda37c98b4246b39b9bcbb318092b5e10cc5f8a" + ], + "source_subject": "sdk: align README order boundary", + "target_subject": "sdk: align README order boundary", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T21:20:51-07:00", + "target_author_time": "2026-06-19T21:20:51-07:00", + "source_committer_time": "2026-06-19T21:20:51-07:00", + "target_committer_time": "2026-06-19T21:20:51-07:00", + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "topology_support" + }, + { + "source_commit": "2bea0832ee8f59de2fbb975a6ec69f0017b62b87", + "target_commit": "c2e8933ec3e7a2a42f1e106afd183f7b6a4d240d", + "source_parents": [ + "8916816e395cd70db648e48536be4f8f53628fc6" + ], + "target_parents": [ + "2126dfb990d314dc8662bb48f548e2527d4a6d6d" + ], + "source_subject": "sdk: use localhost relay policy", + "target_subject": "sdk: use localhost relay policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T14:18:21-07:00", + "target_author_time": "2026-06-16T14:18:21-07:00", + "source_committer_time": "2026-06-16T14:18:21-07:00", + "target_committer_time": "2026-06-16T14:18:21-07:00", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "571633bdbdbe37a2abc8fce1ce62aaf6705616d37cc473fa9db138b49c306af2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2c71aa959c367b2c8a565da691e47e3ff5348f69", + "target_commit": "278fbb64c1d84d4acede84a60a2afa40af82f6e9", + "source_parents": [ + "36881fa076c80c37a0cba8229093125e7fca8336" + ], + "target_parents": [ + "66c859b8ae773bdd3eb80b2894c4fcc97146f17a" + ], + "source_subject": "sdk: align runtime README wording", + "target_subject": "sdk: align runtime README wording", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T23:12:36Z", + "target_author_time": "2026-07-01T23:12:36Z", + "source_committer_time": "2026-07-01T23:12:36Z", + "target_committer_time": "2026-07-01T23:12:36Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "5f858586b3fb8c0c5a0d1e210bfeec32689f24a635c20552b5a7d195900a7906", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2d05abc48a5785496e4a4f977e001ca23091bcf0", + "target_commit": null, + "source_parents": [ + "391f8bced9b21b3655d56c3b52528322f23bdada" + ], + "target_parents": [], + "source_subject": "feat(xtask): add generation command skeleton", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:09:49-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:09:49-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "2dad39727858c557d0f10d0d3c43c2b0ca581208", + "target_commit": "5cf9993e6ad8b807623170709ec2ddeb95b07574", + "source_parents": [ + "f0f142265096cb8da7e2ede732e7c599bd243bdb" + ], + "target_parents": [ + "46ec9a40f3981d00654663abe226b8f33cd7eb39" + ], + "source_subject": "sdk: add forward farm location set", + "target_subject": "sdk: add forward farm location set", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T23:26:43Z", + "target_author_time": "2026-06-26T23:26:43Z", + "source_committer_time": "2026-06-26T23:26:43Z", + "target_committer_time": "2026-06-26T23:26:43Z", + "source_paths": [ + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/geonames.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "0e57ab558ed9769b7511f16a2ab8f658bce8f7c70d40e8d6400f926317ecde1a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2e45a4bd5bc70263cf3bfbce73345452b5c292d8", + "target_commit": "4a3cbe30a88fee436f41094acfcd02caaf8eaa1c", + "source_parents": [ + "630651c55900455d1c8acd3aa671dd919bfb45f8" + ], + "target_parents": [ + "2de5fda8382b4548f960f12e5b85de980677958d" + ], + "source_subject": "transport: reject duplicate SDK targets", + "target_subject": "transport: reject duplicate SDK targets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T05:01:24Z", + "target_author_time": "2026-07-08T05:01:24Z", + "source_committer_time": "2026-07-08T05:01:24Z", + "target_committer_time": "2026-07-08T05:01:24Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "32302cfca8dedabbfa5d26b736dc799f70f129deea8271a3aaa3f7e447655e71", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2f172e208550f7ef0177c138bf174d38ca675784", + "target_commit": "dffb7e0f1387d297ae6a41db0a807448308a9f20", + "source_parents": [ + "082ef3b82ab1e69f070421345e1cf2ace8cfe6ab" + ], + "target_parents": [ + "3d908d05c33bd949a1a173d644f14bd83973ae74" + ], + "source_subject": "chore: align source layout", + "target_subject": "chore: align source layout", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T13:40:05-07:00", + "target_author_time": "2026-06-11T13:40:05-07:00", + "source_committer_time": "2026-06-11T13:42:10-07:00", + "target_committer_time": "2026-06-11T13:42:10-07:00", + "source_paths": [ + "crates/core_bindings/src/lib.rs", + "crates/core_bindings/src/typescript/types.ts", + "crates/identity_bindings/src/lib.rs", + "crates/identity_bindings/src/typescript/constants.ts", + "crates/trade_bindings/src/lib.rs", + "crates/trade_bindings/src/typescript/types.ts" + ], + "normalized_patch_sha256": "fbee90022feeed313fefa045dec5d3a303078e5ba0ca31309707376deb085b2b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "2f960e1053ef0e0354d4575b9cd14bfdbe7739d0", + "target_commit": null, + "source_parents": [ + "b282947cc09853534f4b2e6243f9b51cbe1ccd34" + ], + "target_parents": [], + "source_subject": "docs: add repository agent instructions", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-27T07:10:34Z", + "target_author_time": null, + "source_committer_time": "2026-07-27T07:10:34Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "3020f5bb3370c0d84e28510761373d4b516491e5", + "target_commit": "6b38872141aa99e7bfe20b42e6473084abadead0", + "source_parents": [ + "59e7456f4f3d354f578fadeb642fce7c25862be9" + ], + "target_parents": [ + "9dbd53dc24b8aff3d421a6cac4c5cbec0616d7da" + ], + "source_subject": "Add SDK farm private location clearing", + "target_subject": "Add SDK farm private location clearing", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T13:15:30Z", + "target_author_time": "2026-06-26T13:15:30Z", + "source_committer_time": "2026-06-26T13:15:30Z", + "target_committer_time": "2026-06-26T13:15:30Z", + "source_paths": [ + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs" + ], + "normalized_patch_sha256": "bc5c73dd74e0b979c5954163ad7248bb59231edf47b98eeb35c97e682b1f1fa3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "30a23c4a1e0c95ffb507f57aac6a5387ac97e5d1", + "target_commit": "1558ea5a5c916729ce59fcdb0a709ce397172fc8", + "source_parents": [ + "9b073c1fec2c98393673ecff93d4424da1e71413" + ], + "target_parents": [ + "fc4d0188420ff694485e4065a44e6539255ea247" + ], + "source_subject": "sdk: make local-runtime canonical", + "target_subject": "sdk: make local-runtime canonical", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T20:10:36-07:00", + "target_author_time": "2026-06-18T20:10:36-07:00", + "source_committer_time": "2026-06-18T20:10:36-07:00", + "target_committer_time": "2026-06-18T20:10:36-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README" + ], + "normalized_patch_sha256": "6d98f26ed245f75329087ab36a82cbebbc764af8807498aad9a23a87c2d40ff5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3318ff5613a8d865fa73878d3c98570354daec27", + "target_commit": "69ffeecec1a44a4a2c08bed407fa36c9a9bfc516", + "source_parents": [ + "d554fef01ba67ccc3926138bf6cd945aa67917e4" + ], + "target_parents": [ + "5f47c85ca3f1ee7b9583b08cfe48454c6dc78797" + ], + "source_subject": "sdk: add CLI runtime feature", + "target_subject": "sdk: add CLI runtime feature", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T13:31:14-07:00", + "target_author_time": "2026-06-17T13:31:14-07:00", + "source_committer_time": "2026-06-17T13:31:14-07:00", + "target_committer_time": "2026-06-17T13:31:14-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README" + ], + "normalized_patch_sha256": "242ec176f90ddec87361047780c8801ef835cffee89af20e9b5e27be865ab807", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "34281da7f4d67aae7c6cc8508ce784c5ad8e04ed", + "target_commit": "d73d4647f35616987581e5bd9181cc5a766ec5c4", + "source_parents": [ + "9b45e557252f9db93d454a9f39bf3dd01b4f9a93" + ], + "target_parents": [ + "c3b2de702fd1a4893ea5e85175e2e0b78c4d10fe" + ], + "source_subject": "sync: scope push outbox publishes to active plans", + "target_subject": "sync: scope push outbox publishes to active plans", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T22:18:15Z", + "target_author_time": "2026-07-07T22:18:15Z", + "source_committer_time": "2026-07-07T22:18:15Z", + "target_committer_time": "2026-07-07T22:18:15Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "87ef03007994892d377207521aa99b9c49e430ca016f0947b2db731c5df98aa1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3491f2beee0f055f24aa0fd7663fa57b2f3f6695", + "target_commit": "84301e0eca8b92cd2a8d6acea447a146db9bde32", + "source_parents": [ + "5313c6fc6da6982943d54dacb01f0068d09b81ae" + ], + "target_parents": [ + "c9de1f9b7c550928f364cb53773e74ba4156e96e" + ], + "source_subject": "toolchain: raise Rust floor to 1.97", + "target_subject": "toolchain: raise Rust floor to 1.97", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T01:18:31Z", + "target_author_time": "2026-07-15T01:18:31Z", + "source_committer_time": "2026-07-15T01:18:31Z", + "target_committer_time": "2026-07-15T01:18:31Z", + "source_paths": [ + "tools/xtask/src/coverage_policy_tests.rs", + "tools/xtask/src/smoke.rs", + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "7683f7bed2fdaac5dc866397e636fec39de518371aaf6f9f9721874f20337678", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "349dcd489e63463be346566e788c0750a92d5dbe", + "target_commit": "cc8fde0343779e72dba4fde685cb5758edf73a57", + "source_parents": [ + "2c71aa959c367b2c8a565da691e47e3ff5348f69" + ], + "target_parents": [ + "278fbb64c1d84d4acede84a60a2afa40af82f6e9" + ], + "source_subject": "tests: assert root-aware trade projection schema", + "target_subject": "tests: assert root-aware trade projection schema", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-03T22:39:22Z", + "target_author_time": "2026-07-03T22:39:22Z", + "source_committer_time": "2026-07-03T22:39:22Z", + "target_committer_time": "2026-07-03T22:39:22Z", + "source_paths": [ + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "cb1f48a3bbf1b566bc4c367f2f76fdcd341f60ccbb1b599c5049352acdcbd5de", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "355a7807b9b38afcff7a47750ab09db222310df4", + "target_commit": "2e602c9c268729a5c82fcaaf85fee5112b46efdb", + "source_parents": [ + "14417d787b2a9216fc32c156972b21f515699f29" + ], + "target_parents": [ + "a1fbc9bde3039dff636afba0da88bfd9ad2d444d" + ], + "source_subject": "facade: implement façade feature forwarding", + "target_subject": "facade: implement façade feature forwarding", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:47:12Z", + "target_author_time": "2026-08-03T12:47:12Z", + "source_committer_time": "2026-08-03T12:47:12Z", + "target_committer_time": "2026-08-03T12:47:12Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/src/client.rs", + "crates/radroots/src/knowledge.rs", + "crates/radroots/src/storage.rs", + "crates/radroots/src/transport.rs", + "crates/radroots/tests/package_boundary.rs", + "crates/radroots/tests/public_surface.rs" + ], + "normalized_patch_sha256": "c18ed77692116d8fcb69e1af5ddff68f7e28a8701c05375976af7c87f96b2281", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "35da9e1b235e06e7489e4b69a5c8d058655122c5", + "target_commit": "f6d381330ac3c00ab42fd50a020b82f8891147d1", + "source_parents": [ + "603776a1d16d661d4a944f24dba5564c10c4e879" + ], + "target_parents": [ + "461bcae68c5a8bc3239b3db0e1788ecbd3c0653c" + ], + "source_subject": "event: use identity-owned public keys", + "target_subject": "event: use identity-owned public keys", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T23:48:23Z", + "target_author_time": "2026-07-28T23:48:23Z", + "source_committer_time": "2026-07-28T23:48:23Z", + "target_committer_time": "2026-07-28T23:48:23Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/event_codec_wasm/src/lib.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "8a2bfeee6c830af5d11ae7dbd381f8f326973fde3c51bd18829a627fecd922f9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "36509e62546b07c2aa2f62cd6ad07b4f39d2abff", + "target_commit": "fe1ab75ec3fdbbd2aa4470b13e7d1dd07ca5f593", + "source_parents": [ + "026dbe08a91c97f1a555d0507f3c43e10768eb91" + ], + "target_parents": [ + "eb7590c3fd0d587d8dc9ae503dee4ce9e60660c2" + ], + "source_subject": "knowledge: surface draft contract error codes", + "target_subject": "knowledge: surface draft contract error codes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T06:45:18Z", + "target_author_time": "2026-07-06T06:45:18Z", + "source_committer_time": "2026-07-06T06:45:18Z", + "target_committer_time": "2026-07-06T06:45:18Z", + "source_paths": [ + "crates/sdk/src/knowledge.rs", + "crates/sdk/tests/knowledge_public_api.rs" + ], + "normalized_patch_sha256": "c1ee3b8be198453f3e90275414ab600f67e04e8d30c95a4b8b469edad84f7fc4", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "36881fa076c80c37a0cba8229093125e7fca8336", + "target_commit": "66c859b8ae773bdd3eb80b2894c4fcc97146f17a", + "source_parents": [ + "f9a6f13f31140f26f9bda8a28aab1bafec00f6fc" + ], + "target_parents": [ + "9600dfa4b30820660154d1e28d95a7e83344d103" + ], + "source_subject": "sdk: guard production warning hygiene", + "target_subject": "sdk: guard production warning hygiene", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T21:17:52Z", + "target_author_time": "2026-07-01T21:17:52Z", + "source_committer_time": "2026-07-01T21:17:52Z", + "target_committer_time": "2026-07-01T21:17:52Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "715c3ebc52209efa6fd5596ea9615853aed6471cd9efe68b0d18560330a99387", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "36e28bc09cf7924a1f36c5a0135095d6526ab06a", + "target_commit": "4fda37c98b4246b39b9bcbb318092b5e10cc5f8a", + "source_parents": [ + "63e9e99527575e0b11767965a6839e569b6a8bcf" + ], + "target_parents": [ + "81966cf2a5604c06da6e3227ec2141b4b7ca0da2" + ], + "source_subject": "sdk: prune stale trade lifecycle bindings", + "target_subject": "sdk: prune stale trade lifecycle bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T20:48:42-07:00", + "target_author_time": "2026-06-19T20:48:42-07:00", + "source_committer_time": "2026-06-19T20:48:42-07:00", + "target_committer_time": "2026-06-19T20:48:42-07:00", + "source_paths": [ + "crates/trade_bindings/src/model.rs" + ], + "normalized_patch_sha256": "bf22f6073e36f83d869a373ec65e199322cf7e3d63ad645f1199aa1ef53e1062", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "37b664841acf4f2fb052ec6ae3b139448a454a1b", + "target_commit": "1b7f454e9ba6d400b5f50cb776e42e3a507f7bc4", + "source_parents": [ + "84ec9114bc42376f0fef5b2907b86440a9323ca5" + ], + "target_parents": [ + "3a10e0bbfccf5853deafb49e240d93b04f578920" + ], + "source_subject": "facade: define curated domain modules and reexports", + "target_subject": "facade: define curated domain modules and reexports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:38:45Z", + "target_author_time": "2026-08-03T12:38:45Z", + "source_committer_time": "2026-08-03T12:38:45Z", + "target_committer_time": "2026-08-03T12:38:45Z", + "source_paths": [ + "crates/radroots/src/event.rs", + "crates/radroots/src/farm.rs", + "crates/radroots/src/identity.rs", + "crates/radroots/src/knowledge.rs", + "crates/radroots/src/listing.rs", + "crates/radroots/src/signing.rs", + "crates/radroots/src/storage.rs", + "crates/radroots/src/trade.rs", + "crates/radroots/src/transport.rs", + "crates/radroots/tests/package_boundary.rs", + "crates/radroots/tests/public_surface.rs" + ], + "normalized_patch_sha256": "f3445fd4b63f2d3b7d587dec9dd2c370e66fd89da28f81f2393dc34f04bdd0f8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "38eacd2c501b137011aa66bfca1a966ea8ab106d", + "target_commit": "a84948593485eb4cba3e8ae3c0f4a6c112014f98", + "source_parents": [ + "a4c2ee6b98e3ffbe70d50e221193699c8aff0ae7" + ], + "target_parents": [ + "a1fd7c79f51357c783f3af5dbb78aa73ebdbc641" + ], + "source_subject": "sdk: add listing publish runtime", + "target_subject": "sdk: add listing publish runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T14:04:20-07:00", + "target_author_time": "2026-06-15T14:04:20-07:00", + "source_committer_time": "2026-06-15T14:04:20-07:00", + "target_committer_time": "2026-06-15T14:04:20-07:00", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/tests/listings_runtime.rs" + ], + "normalized_patch_sha256": "d06fc1cbb4f45cca692ec2bffde400c731956efa1dab0e87999fdb63c6307c2b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "391f8bced9b21b3655d56c3b52528322f23bdada", + "target_commit": "465c9faa8197ae16e7d6c32099d173b3ca2469a7", + "source_parents": [ + "6985c00f4f460f2c29225b7053cef0b6892aea3b" + ], + "target_parents": [], + "source_subject": "chore(sdk): add bindings workspace skeleton", + "target_subject": "chore(sdk): add bindings workspace skeleton", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T06:07:27-07:00", + "target_author_time": "2026-06-11T06:07:27-07:00", + "source_committer_time": "2026-06-11T06:07:27-07:00", + "target_committer_time": "2026-06-11T06:07:27-07:00", + "source_paths": [ + "crates/core_bindings/Cargo.toml", + "crates/core_bindings/src/lib.rs", + "crates/identity_bindings/Cargo.toml", + "crates/identity_bindings/src/lib.rs", + "crates/trade_bindings/Cargo.toml", + "crates/trade_bindings/src/lib.rs" + ], + "normalized_patch_sha256": "b96bfff2c5fc052b7fae2270c8de847a9629ccb082748f8d0051458d0cb33143", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "39b12eaa8b2b9c6246246200b888e7ce70f12f08", + "target_commit": "4d03e7cff98fe994dc698ed56fb23ec7250e8cea", + "source_parents": [ + "35da9e1b235e06e7489e4b69a5c8d058655122c5" + ], + "target_parents": [ + "f6d381330ac3c00ab42fd50a020b82f8891147d1" + ], + "source_subject": "event: migrate SDK consumers to final surface", + "target_subject": "event: migrate SDK consumers to final surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-29T06:10:36Z", + "target_author_time": "2026-07-29T06:10:36Z", + "source_committer_time": "2026-07-29T06:10:36Z", + "target_committer_time": "2026-07-29T06:10:36Z", + "source_paths": [ + "crates/event_bindings/src/lib.rs", + "crates/event_bindings/src/model.rs", + "crates/event_codec_wasm/Cargo.toml", + "crates/event_codec_wasm/src/lib.rs", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "crates/trade_bindings/Cargo.toml", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "4c859d43087dcc39689e490accd463b5d776a62dd223b933e02559b1032a79a2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "39bffb5c94cdaebfdb380f3e1843b4c75dbda3f3", + "target_commit": "755dcfd6de8c94fd145d30b4d35f3125c8d93664", + "source_parents": [ + "4954d44ba6a2d601a38925b469f2e10cc8fbae5d" + ], + "target_parents": [ + "0fc724df31b48e6bfd3cc8a477236b0ef5d5dbe9" + ], + "source_subject": "release: preserve sdk version authority", + "target_subject": "release: preserve sdk version authority", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:48:53Z", + "target_author_time": "2026-07-28T18:48:53Z", + "source_committer_time": "2026-07-28T18:48:53Z", + "target_committer_time": "2026-07-28T18:48:53Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "crates/sql_wasm_runtime/Cargo.toml", + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "981af2da0a5dd352dd1d327195aea59e30252e6e08f55e6bda504e182ed7583e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3aab8706a0d9da8e7b06f4dc8d9a5e0844da4233", + "target_commit": "79ff8c411788096c014b6478585207358274d917", + "source_parents": [ + "e0b77999abf22fccd1ea51e576bd6346f1a13d71" + ], + "target_parents": [ + "f758400e8790560105b19f393d1752d1c1b32502" + ], + "source_subject": "sdk: record local import observations for signed workflows", + "target_subject": "sdk: record local import observations for signed workflows", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T08:47:16Z", + "target_author_time": "2026-07-08T08:47:16Z", + "source_committer_time": "2026-07-08T08:47:16Z", + "target_committer_time": "2026-07-08T08:47:16Z", + "source_paths": [ + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "807f6a367ab4e96d3153060be61b0509291a91c433d3f88a0318e994f6c6d419", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3b6767ef28eae443cb29a62a88ccd649b2977274", + "target_commit": "17ade41af8b510207a4900a2a3e2ce3661495a03", + "source_parents": [ + "c57ea6bcf846ecccb8e4b8a8696a86e1a5c312ce" + ], + "target_parents": [ + "b9b574713837dcae18841849ec9262cf83f28d37" + ], + "source_subject": "workspace: remove production sibling paths", + "target_subject": "workspace: remove production sibling paths", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T10:50:23Z", + "target_author_time": "2026-07-27T10:50:23Z", + "source_committer_time": "2026-07-27T10:50:23Z", + "target_committer_time": "2026-07-27T10:50:23Z", + "source_paths": [ + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "c4c0202e03a81288f74b51810fd3780bd11730f9d483cabb794de0565428861a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3bc5714bb37b289d960d1ee45d62955f81e9734a", + "target_commit": "55d6c41bb1b6cd73061ae9a6db183600d8d1a01a", + "source_parents": [ + "c178b9495109c530abea18e0d489ae2e57d3da7a" + ], + "target_parents": [ + "d581dedb72e8293bd34929c119590f1081327328" + ], + "source_subject": "tests: remove target-dir from perf log", + "target_subject": "tests: remove target-dir from perf log", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-14T20:59:45Z", + "target_author_time": "2026-07-14T20:59:45Z", + "source_committer_time": "2026-07-14T20:59:45Z", + "target_committer_time": "2026-07-14T20:59:45Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "631797d3e9c8041a472270ff767a08540ca8ea333ab3ef97a8c1ad2c01005624", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3beba951bcc7dae327e4367ca1468448c4787696", + "target_commit": "eefc32cc7f76cd761e58b06fe3147c6fd9e298da", + "source_parents": [ + "4bb5a24bf196b6f30f78ec969a4677a85a97b49c" + ], + "target_parents": [ + "13deb322e30b9cc9250596cf61a0426a4758600f" + ], + "source_subject": "ts: enforce generated artifact inventory", + "target_subject": "ts: enforce generated artifact inventory", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T00:26:46Z", + "target_author_time": "2026-06-28T00:26:46Z", + "source_committer_time": "2026-06-28T00:26:46Z", + "target_committer_time": "2026-06-28T00:26:46Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "0a0d5330e83051c2b7193c3ab4bb844b7253a7ec439faba4a981b5f676504197", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3c0094e3b756de3ff67c82153fbfdd04e9977dc7", + "target_commit": "81fce43cda89b3a2089d1bdd45edcb43fb1c9553", + "source_parents": [ + "0d774c10dcd16a9b58ff3efabb98d32e4e7f804e" + ], + "target_parents": [ + "f55b6d65023e475bf71faee8b7261965d7c5dee9" + ], + "source_subject": "replica: render schema bindings from dto registry", + "target_subject": "replica: render schema bindings from dto registry", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T09:04:55Z", + "target_author_time": "2026-06-24T09:04:55Z", + "source_committer_time": "2026-06-24T09:04:55Z", + "target_committer_time": "2026-06-24T09:04:55Z", + "source_paths": [ + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "e547818aa60500f4370c7a488ffcff5bc85d8e30b0bef192d7915fa78d412fee", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3c4398f0fa519dd5611859f67a42c2acd0e6bdad", + "target_commit": "c9ea54fa108d5f86db3e02973286a78945dc58bb", + "source_parents": [ + "b04b893574acdd734d4a42c361ae09e627a05798" + ], + "target_parents": [ + "ee970784ddafd6ae6054360df004d1547cbe9409" + ], + "source_subject": "sync: prove proxy validation skips daemon work", + "target_subject": "sync: prove proxy validation skips daemon work", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T22:35:29Z", + "target_author_time": "2026-07-07T22:35:29Z", + "source_committer_time": "2026-07-07T22:35:29Z", + "target_committer_time": "2026-07-07T22:35:29Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "444d3ea895ed6e951fa563b40bec94827cafe9a0cfdd587f39b970a755f89e27", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3cae34b3248f448691e05585b2030133260f5591", + "target_commit": "8980ce18804175ea0442ec8b8e27485f96cf9fd7", + "source_parents": [ + "56efb90bc4b94143861ec543a9e85a4a656300df" + ], + "target_parents": [ + "5c9b7fc8242415722aa1845a751dd45879715c2e" + ], + "source_subject": "sdk: expose geonames runtime asset API", + "target_subject": "sdk: expose geonames runtime asset API", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T05:58:17Z", + "target_author_time": "2026-06-26T05:58:17Z", + "source_committer_time": "2026-06-26T05:58:17Z", + "target_committer_time": "2026-06-26T05:58:17Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/geonames.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/geonames.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "06e97ee68488191ae514259def73c7e4dac8ae1bb3e8a8ce24e4e3601f422ee8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3d45ad617d6c0f90e7417b64b4518d65c81b69f7", + "target_commit": "e069cebfce543dff4e541f8c43b0f319e0e7664c", + "source_parents": [ + "0ff9e2825de711d0e662fc3b3b5eda5d3cb6a38a" + ], + "target_parents": [ + "121ab276234198c523e1d01467e8f7cd7cedc9c7" + ], + "source_subject": "runtime: harden private store and signer boundary", + "target_subject": "runtime: harden private store and signer boundary", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T15:52:46Z", + "target_author_time": "2026-07-15T15:52:46Z", + "source_committer_time": "2026-07-15T15:52:46Z", + "target_committer_time": "2026-07-15T15:52:46Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "0eb45ebffb353359aa5262d7e51ee7cc835ce80d13260d2ea1cb46b233eea002", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3e67c7d9d5f0b12cbc5b8ab0c2ad36c25ed7caa7", + "target_commit": "6cd934641036e19250352312a252df44d2da99c5", + "source_parents": [ + "8ed1e0500f232489c23620f2ceb445bcafbb079e" + ], + "target_parents": [ + "bd217bd3577b83906d8c88a42d6dbc69b960845f" + ], + "source_subject": "workspace: standardize lint and rustdoc policy", + "target_subject": "workspace: standardize lint and rustdoc policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T09:30:00Z", + "target_author_time": "2026-07-27T09:30:00Z", + "source_committer_time": "2026-07-27T09:30:00Z", + "target_committer_time": "2026-07-27T09:30:00Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "ba0b84a9f2a5c0e368e8a3c6eb50bbcfb4d7427ea637d65ca74700e65fc97a44", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3edb09c0088a5d37d496515ee7cbbe1aadb57904", + "target_commit": "58fd4bb48ef0dd29fa3171d45607ce29f339b8e2", + "source_parents": [ + "47dabd049d57be6efd9741d966adabb01bd8b433" + ], + "target_parents": [ + "06c70ab56c883f22a30ac6f2137a5197583efefa" + ], + "source_subject": "transport: migrate workspace consumers", + "target_subject": "transport: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T23:04:03Z", + "target_author_time": "2026-07-30T23:04:03Z", + "source_committer_time": "2026-07-30T23:04:03Z", + "target_committer_time": "2026-07-30T23:04:03Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "7de63c12369980d9fc86a32f08d55ea4e23bb8c40f69e9d36005b728b389d7f8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "3fa03e7b623a09eb4da7c5b2283d3aa32aa60919", + "target_commit": "c5d6ee72094ccb1f368389569d6e179dd9a3a4e1", + "source_parents": [ + "c115963d95b5b239ce6ecd4511ee3f67ffb92708" + ], + "target_parents": [ + "575fc38393473a20587457b59689e57824cec057" + ], + "source_subject": "sdk: consume explicit Nostr adapter modules", + "target_subject": "sdk: consume explicit Nostr adapter modules", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T12:32:13Z", + "target_author_time": "2026-07-31T12:32:13Z", + "source_committer_time": "2026-07-31T12:32:13Z", + "target_committer_time": "2026-07-31T12:32:13Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "63e70fc7188e79645dfa058b1d1ecbda65b3b8a2616792482ae2178c4c5075b7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "402732b862f40142bdd6445a4d5b8187c2f0a45d", + "target_commit": "59a46b604d1ad90ce1a1a2976930e4238a27e0fe", + "source_parents": [ + "26e03efae10d928d57a0a979be1f39327ade52d4" + ], + "target_parents": [ + "9a680cced44a26ed41491386d1057c4d23c77642" + ], + "source_subject": "sdk: remove public protocol bypass surface", + "target_subject": "sdk: remove public protocol bypass surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T07:01:29Z", + "target_author_time": "2026-06-30T07:01:29Z", + "source_committer_time": "2026-06-30T07:01:29Z", + "target_committer_time": "2026-06-30T07:01:29Z", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/profile.rs", + "crates/sdk/src/protocol/events.rs", + "crates/sdk/src/protocol/farm.rs", + "crates/sdk/src/protocol/identity.rs", + "crates/sdk/src/protocol/listing.rs", + "crates/sdk/src/protocol/mod.rs", + "crates/sdk/src/protocol/order.rs", + "crates/sdk/src/protocol/profile.rs", + "crates/sdk/src/protocol/wire.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs" + ], + "normalized_patch_sha256": "d45a54fea3e742dce373b6ab5b35ee416a439e6e525e8759d1a548d522b2a9b8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "40501c57db240199ac750c62ff9f6485778d79ae", + "target_commit": "067c67150f18b47a4d18481814f5c3b43054f1f6", + "source_parents": [ + "5fadc890e491d62686cb06ebe1a0f739abc8895d" + ], + "target_parents": [ + "73924d46fc5839f31c7842ba39b2993f06df6a0c" + ], + "source_subject": "sdk: enrich order status receipts", + "target_subject": "sdk: enrich order status receipts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T17:12:34-07:00", + "target_author_time": "2026-06-15T17:12:34-07:00", + "source_committer_time": "2026-06-15T17:12:34-07:00", + "target_committer_time": "2026-06-15T17:12:34-07:00", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "b073f13e17aba7e979ae432f88c7bd2c7f1b79651d270b2d1d9b3f6ca97f4329", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "407cfcca4b418d251d0e6c12f19a69cb1e9a41bc", + "target_commit": "4f95496a65a1a258b26faf827e15920c023826eb", + "source_parents": [ + "cae6213a677329355ff32ebbdf7e6b3cce4c2577" + ], + "target_parents": [ + "878e3730f842d8a40ad35b076476b50eae7abe94" + ], + "source_subject": "sdk: add runtime relay and idempotency primitives", + "target_subject": "sdk: add runtime relay and idempotency primitives", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T16:41:07-07:00", + "target_author_time": "2026-06-15T16:41:07-07:00", + "source_committer_time": "2026-06-15T16:41:07-07:00", + "target_committer_time": "2026-06-15T16:41:07-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/runtime_targets.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "763c07722de162e5b7e16ba779bd141fa8a4d2bb9bc3948ae10c63852617a924", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "418a3ca6860407f0bc38366e27d0c269202fbb65", + "target_commit": "6653844ddc926c1ab2999bcb766837a60c5291e9", + "source_parents": [ + "521d9250e7c25a4a2d4e185e43089daa1b5f8ddf" + ], + "target_parents": [ + "4c34983d3e8a18f9f61f1bcccc82e825ac001482" + ], + "source_subject": "replica-schema: refresh error bindings", + "target_subject": "replica-schema: refresh error bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T02:21:24Z", + "target_author_time": "2026-07-13T02:21:24Z", + "source_committer_time": "2026-07-13T02:21:24Z", + "target_committer_time": "2026-07-13T02:21:24Z", + "source_paths": [ + "crates/replica_store_wasm/src/wasm_impl.rs" + ], + "normalized_patch_sha256": "8799884b13557c82b163da61d4e5a1eedece465411c74fa5688d216dfee05690", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "425c2f721c6cefa4b90c0f4833e2fb080c45c4ec", + "target_commit": "6cf47f4a0daa32ea93580a848f97ed08578d9957", + "source_parents": [ + "436d5958715f3b68e179b2dec05325d4bfc5d061" + ], + "target_parents": [ + "ba8dc9cae4d0ff645b751a4636f2d2b0c82a307f" + ], + "source_subject": "sdk: add private farm location store", + "target_subject": "sdk: add private farm location store", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T09:32:56Z", + "target_author_time": "2026-06-26T09:32:56Z", + "source_committer_time": "2026-06-26T09:32:56Z", + "target_committer_time": "2026-06-26T09:32:56Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "c14fb3146a647e2297957cead3191003145d17026fcb41a8c0646af78ce585c0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "426b7e68987c64bea50ca20ca0653e4a68d7e28f", + "target_commit": "e2533b48d5558e6f4fb477124f4039564fc1f41f", + "source_parents": [ + "2bea0832ee8f59de2fbb975a6ec69f0017b62b87" + ], + "target_parents": [ + "c2e8933ec3e7a2a42f1e106afd183f7b6a4d240d" + ], + "source_subject": "sdk: add v1 product examples", + "target_subject": "sdk: add v1 product examples", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T14:20:29-07:00", + "target_author_time": "2026-06-16T14:20:29-07:00", + "source_committer_time": "2026-06-16T14:20:29-07:00", + "target_committer_time": "2026-06-16T14:20:29-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs" + ], + "normalized_patch_sha256": "9de1cb0ea645724e6261226d4b06dc8d48e438881b26e3ea0851928b7574c529", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "42c1d200a8ebdaad0cbb67b2066ac924dc6a612a", + "target_commit": "6e6966629e309273891a98b2f6a134bf03a424c0", + "source_parents": [ + "49f38b4765e8afd432b6f45ab03bc500818582f1" + ], + "target_parents": [ + "ce7d805ae238a314dba05e35b176b3e706985a59" + ], + "source_subject": "packages: require full dist payloads", + "target_subject": "packages: require full dist payloads", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T06:19:36Z", + "target_author_time": "2026-06-28T06:19:36Z", + "source_committer_time": "2026-06-28T06:19:36Z", + "target_committer_time": "2026-06-28T06:19:36Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "238c14e46e77b2b68b4d871c8440087aae3762a176793ef32925d62ca1404824", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "436d5958715f3b68e179b2dec05325d4bfc5d061", + "target_commit": "ba8dc9cae4d0ff645b751a4636f2d2b0c82a307f", + "source_parents": [ + "b8838cb197532e955d77521a1779f3b659033e6c" + ], + "target_parents": [ + "0ce314697a6e87c6434d44e785ab7fac352348af" + ], + "source_subject": "sdk: expose market projections", + "target_subject": "sdk: expose market projections", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T09:08:53Z", + "target_author_time": "2026-06-26T09:08:53Z", + "source_committer_time": "2026-06-26T09:08:53Z", + "target_committer_time": "2026-06-26T09:08:53Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/market_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs" + ], + "normalized_patch_sha256": "f5a38a6779c5890036a29cca0a6a342b7e66ee38e07638d2193f4483a9613bae", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4472fe288ba42cbd095009309d6e11751c4b7d55", + "target_commit": "90c1ec6d55810f90b9858b4bc35881d714267ac4", + "source_parents": [ + "80af9cd1f3559dc8a71ec92518eed69337c68d32" + ], + "target_parents": [ + "de9ab46d7bed496b473ccd136d34b6f705def3a7" + ], + "source_subject": "sync: add SDK status and push policy contracts", + "target_subject": "sync: add SDK status and push policy contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T14:03:43-07:00", + "target_author_time": "2026-06-17T14:03:43-07:00", + "source_committer_time": "2026-06-17T14:03:43-07:00", + "target_committer_time": "2026-06-17T14:03:43-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "0fc54e4aa5f76b95a8a88a05a3e1f5edca0fbb14d1da173fb4418fac6ece2820", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "45179a3b17a709257296f16cc0979a76286d757d", + "target_commit": "ad894de3982f245a838cb2512f36e7e4f3dbafb4", + "source_parents": [ + "68cff3f0569132f4734339e0246e86a5b2d7175f" + ], + "target_parents": [ + "b42e6c6fe414c7ad15435c7c19e590f37a690b2d" + ], + "source_subject": "transport: allow explicit Reticulum preview scope", + "target_subject": "transport: allow explicit Reticulum preview scope", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T17:24:21Z", + "target_author_time": "2026-07-09T17:24:21Z", + "source_committer_time": "2026-07-09T17:24:21Z", + "target_committer_time": "2026-07-09T17:24:21Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "87960abc5d5ac3dcb852181381402405d8416c0bb66d0e988e8834e50f0c4a1d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "451b58b9eb465b5e0d3755f82d0459fb172de096", + "target_commit": "d7f482ab97d26dbeb8ef2ccdb3a72e71c67eac09", + "source_parents": [ + "4bf5145d67db265d6d391d382813c6da6848f566" + ], + "target_parents": [ + "2c7fbb1291918623ab8880b764ea6771864055d2" + ], + "source_subject": "signing: migrate workspace consumers", + "target_subject": "signing: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T12:58:03Z", + "target_author_time": "2026-07-30T12:58:03Z", + "source_committer_time": "2026-07-30T12:58:03Z", + "target_committer_time": "2026-07-30T12:58:03Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "22811c6eaf43c6cd8c7801af2794381642053ea7322b2b936dda9d6fcf6fb10e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4638baeffcb0be18c0ee32b799d88422f6edb7d8", + "target_commit": "597ebf9950e39c1ac775471c3e91d2368375537f", + "source_parents": [ + "ede93b05499af38054f92f827207a37a2d190cdc" + ], + "target_parents": [ + "4ec09b6d4d422d04a4762bee02586c45ec2e9006" + ], + "source_subject": "sdk: add knowledge event surface", + "target_subject": "sdk: add knowledge event surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T06:29:56Z", + "target_author_time": "2026-07-05T06:29:56Z", + "source_committer_time": "2026-07-05T06:29:56Z", + "target_committer_time": "2026-07-05T06:29:56Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "d10f7eb944476cc1bb0c7ffc8e4e98eb4ac140318c618ba05c6884969864b483", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "47656467f15b1d21b06f6b6159a8f70cb40c0d28", + "target_commit": "ddf11e6914d431f7896fedf22e4b70c7060ed185", + "source_parents": [ + "735ecaa2d0d3a9ef4d294f3c41fdbdf3761b3c36" + ], + "target_parents": [ + "fd3eb7b29f78a4e45b10f2ae453e511ed4e5611c" + ], + "source_subject": "release: qualify both Rust front doors", + "target_subject": "release: qualify both Rust front doors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T22:31:22Z", + "target_author_time": "2026-08-03T22:31:22Z", + "source_committer_time": "2026-08-03T22:31:22Z", + "target_committer_time": "2026-08-03T22:31:22Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "tools/xtask/src/main.rs", + "tools/xtask/src/release_qualification.rs" + ], + "normalized_patch_sha256": "84e615f4a45d209dfd711dd9c664c01c316568cf69d3ca1565c3135c00b41fe0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "47dabd049d57be6efd9741d966adabb01bd8b433", + "target_commit": "06c70ab56c883f22a30ac6f2137a5197583efefa", + "source_parents": [ + "451b58b9eb465b5e0d3755f82d0459fb172de096" + ], + "target_parents": [ + "d7f482ab97d26dbeb8ef2ccdb3a72e71c67eac09" + ], + "source_subject": "signing: quarantine superseded sdk surface", + "target_subject": "signing: quarantine superseded sdk surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T14:43:31Z", + "target_author_time": "2026-07-30T14:43:31Z", + "source_committer_time": "2026-07-30T14:43:31Z", + "target_committer_time": "2026-07-30T14:43:31Z", + "source_paths": [ + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "173e0fbb4f0cca8af5a10d4abb8b92aca5c8add258bcb9ef821792b2cef512f4", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "48b88b2f8df577d015a810609b31b655d80a74f3", + "target_commit": null, + "source_parents": [ + "7c0177cd5b2261e2e5bea054907aa6147d52aae7" + ], + "target_parents": [], + "source_subject": "repo: verify standalone history preservation", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-27T08:07:01Z", + "target_author_time": null, + "source_committer_time": "2026-07-27T08:07:01Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "4954d44ba6a2d601a38925b469f2e10cc8fbae5d", + "target_commit": "0fc724df31b48e6bfd3cc8a477236b0ef5d5dbe9", + "source_parents": [ + "e543680454009d3bc0b08f3c7f13685ec28d6b63" + ], + "target_parents": [ + "31a6c7d8e9cec3eac4ef1290b6cafa7ba3ec5b22" + ], + "source_subject": "release: freeze sdk crates at 0.1.0-alpha", + "target_subject": "release: freeze sdk crates at 0.1.0-alpha", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:43:51Z", + "target_author_time": "2026-07-28T18:43:51Z", + "source_committer_time": "2026-07-28T18:44:07Z", + "target_committer_time": "2026-07-28T18:44:07Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "crates/sql_wasm_runtime/Cargo.toml", + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "b1bc8846d13a48d25be7fc8b94a81caf3156503de23b508f605f8ca59478bc38", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "49f38b4765e8afd432b6f45ab03bc500818582f1", + "target_commit": "ce7d805ae238a314dba05e35b176b3e706985a59", + "source_parents": [ + "d994ba4de5d25b207489fc760eebc91c52dacf1a" + ], + "target_parents": [ + "487b85367edab1c8a30bcc19cd4d72e20a01db73" + ], + "source_subject": "packages: inspect packed manifests", + "target_subject": "packages: inspect packed manifests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T06:16:33Z", + "target_author_time": "2026-06-28T06:16:33Z", + "source_committer_time": "2026-06-28T06:16:33Z", + "target_committer_time": "2026-06-28T06:16:33Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "284a7a81f9cdce3063bfd5ef67e452e7372d2781aa17162a05b979a578645433", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4a5997a2e48a2b082f277db8d6c135a16667614f", + "target_commit": "74e31d5d30a3e0b115b7657cb4d14bfc58dfc5c7", + "source_parents": [ + "29a7d1f5b96eabc36dbab20eacc3b2947a5f22ee" + ], + "target_parents": [ + "b28d22eaf1be1bedbc417a305c16497162c6ea8c" + ], + "source_subject": "sdk: return structured trade ambiguity", + "target_subject": "sdk: return structured trade ambiguity", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:45:32Z", + "target_author_time": "2026-06-30T03:45:32Z", + "source_committer_time": "2026-06-30T03:45:32Z", + "target_committer_time": "2026-06-30T03:45:32Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "89af749783d5aa92e4bcae4eafdf7bda6fb94e357aa2532e6bdf4cc10e97eab5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4ac891a625014e3044ab343ac3a704c20882335d", + "target_commit": "11e59ce9e6b56d2d248c6be21368f091396bae2c", + "source_parents": [ + "a95445b181e108a35e2d524798201cd89919c0ce" + ], + "target_parents": [ + "deca91ffb6e04cf810080aa099646bb2b52b356d" + ], + "source_subject": "architecture: detect public implementation leakage", + "target_subject": "architecture: detect public implementation leakage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T12:15:06Z", + "target_author_time": "2026-07-27T12:15:06Z", + "source_committer_time": "2026-07-27T12:15:06Z", + "target_committer_time": "2026-07-27T12:15:06Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs", + "tools/xtask/src/architecture/api_leakage.rs", + "tools/xtask/src/main.rs", + "tools/xtask/tests/fixtures/api-leakage/adr-exception.rs", + "tools/xtask/tests/fixtures/api-leakage/allowed-concrete-adapter.rs", + "tools/xtask/tests/fixtures/api-leakage/generic-renamed-tokio.rs", + "tools/xtask/tests/fixtures/api-leakage/generic-sqlx.rs", + "tools/xtask/tests/fixtures/api-leakage/private-implementation.rs" + ], + "normalized_patch_sha256": "fde8f2c0eeb16a5822e4f4111c540f7b56e6ed473b36ae73ac4deaf219705eb5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4b653ca15108b173617adc7842684e01190dfdea", + "target_commit": "4896a04a77d2df368c801f359fcc4637c17055bf", + "source_parents": [ + "2f960e1053ef0e0354d4575b9cd14bfdbe7739d0" + ], + "target_parents": [ + "5ed07d69ae5739867e40aa706b5bce99d6f6cbb0" + ], + "source_subject": "release: freeze package publication during refactor", + "target_subject": "release: freeze package publication during refactor", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T07:32:22Z", + "target_author_time": "2026-07-27T07:32:22Z", + "source_committer_time": "2026-07-27T07:32:22Z", + "target_committer_time": "2026-07-27T07:32:22Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "ddf507c76fb4a4f6dcf5b0fa40bc4841ed962977e9643b0d279db2ed473db1eb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4bb5a24bf196b6f30f78ec969a4677a85a97b49c", + "target_commit": "13deb322e30b9cc9250596cf61a0426a4758600f", + "source_parents": [ + "a85410e02913454ff7e4bfcabf66b3828228f694" + ], + "target_parents": [ + "1050e18ed6f7f3aac073274f0048bf03ffe25a64" + ], + "source_subject": "ts: own wasm declarations with dto_bindgen", + "target_subject": "ts: own wasm declarations with dto_bindgen", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T00:23:51Z", + "target_author_time": "2026-06-28T00:23:51Z", + "source_committer_time": "2026-06-28T00:23:51Z", + "target_committer_time": "2026-06-28T00:23:51Z", + "source_paths": [ + "tools/xtask/src/check.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/wasm.rs", + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "0e93e29b475a1c4a88d4d8de29983f7c6fb8f517e4f365459c588e3a9eec7a8e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4bf5145d67db265d6d391d382813c6da6848f566", + "target_commit": "2c7fbb1291918623ab8880b764ea6771864055d2", + "source_parents": [ + "82a6137848fa527a4c8504a35801cefa52063bb7" + ], + "target_parents": [ + "6056fddcd037d07e0130b6b4ad3b03153b186d2a" + ], + "source_subject": "trade: migrate workspace consumers", + "target_subject": "trade: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T09:00:48Z", + "target_author_time": "2026-07-30T09:00:48Z", + "source_committer_time": "2026-07-30T09:00:48Z", + "target_committer_time": "2026-07-30T09:00:48Z", + "source_paths": [ + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "c7eb4492d35c11895e8a0e821a176f110913096dfa5f12a8c6331dc929e18388", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4cd485179a5f56a95fe54bcbd556becd239d1f03", + "target_commit": "172f6a83e8877b3afebfc015000be50845321525", + "source_parents": [ + "2abc7f5c370201d18306f3e97c01fd98be557036", + "cf95b5087e76b3d734971bea54b5269658d0b4f8", + "9285c57f4c8ff90363bd37940361ed85de1a1a70", + "0d4ef63f34003da2c0c7872bd2aef0546a9041a6", + "50f6d337917af8407470f239de740c7f8ce3ef2d" + ], + "target_parents": [ + "0a7e2668bc360b98a2c31b237ad26a93d7cca25d", + "a5a19bd919e68bf4115d97da5eb8efc6efaf8f64", + "4bbd8c68a2755f3e2ba9f02861818c02156b7c10", + "8b9328b550b4d4637fb045bb0685d9678527109f" + ], + "source_subject": "git: reconcile superseded branch ancestry", + "target_subject": "git: reconcile superseded branch ancestry", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T20:42:38Z", + "target_author_time": "2026-08-04T20:42:38Z", + "source_committer_time": "2026-08-04T20:42:38Z", + "target_committer_time": "2026-08-04T20:42:38Z", + "source_paths": [ + "crates/core_bindings/Cargo.toml", + "crates/core_bindings/src/dto.rs", + "crates/core_bindings/src/lib.rs", + "crates/event_bindings/Cargo.toml", + "crates/event_bindings/src/lib.rs", + "crates/event_bindings/src/model.rs", + "crates/event_codec_wasm/Cargo.toml", + "crates/event_codec_wasm/README", + "crates/event_codec_wasm/src/lib.rs", + "crates/ffi/Cargo.toml", + "crates/ffi/README.md", + "crates/ffi/src/bin/bindgen.rs", + "crates/ffi/src/lib.rs", + "crates/identity_bindings/src/lib.rs", + "crates/radroots/Cargo.toml", + "crates/radroots/LICENSE-APACHE", + "crates/radroots/LICENSE-MIT", + "crates/radroots/README.md", + "crates/radroots/examples/ordinary_memory.rs", + "crates/radroots/src/client.rs", + "crates/radroots/src/event.rs", + "crates/radroots/src/farm.rs", + "crates/radroots/src/identity.rs", + "crates/radroots/src/knowledge.rs", + "crates/radroots/src/lib.rs", + "crates/radroots/src/listing.rs", + "crates/radroots/src/signing.rs", + "crates/radroots/src/storage.rs", + "crates/radroots/src/sync.rs", + "crates/radroots/src/trade.rs", + "crates/radroots/src/transport.rs", + "crates/radroots/tests/package_boundary.rs", + "crates/radroots/tests/public_surface.rs", + "crates/replica_schema_bindings/Cargo.toml", + "crates/replica_schema_bindings/src/lib.rs", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_store_wasm/README", + "crates/replica_store_wasm/src/lib.rs", + "crates/replica_store_wasm/src/snapshot.rs", + "crates/replica_store_wasm/src/utils.rs", + "crates/replica_store_wasm/src/wasm_impl.rs", + "crates/replica_sync_wasm/Cargo.toml", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/LICENSE-APACHE", + "crates/sdk/LICENSE-MIT", + "crates/sdk/README", + "crates/sdk/README.md", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/safe_memory_client.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/examples/transport_profile.rs", + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/relay.rs", + "crates/sdk/src/adapters/signer.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/capability.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/diagnostics.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/geonames.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/identity.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/listing/operational_listing/draft.rs", + "crates/sdk/src/listing/operational_listing/mod.rs", + "crates/sdk/src/listing/operational_listing/model.rs", + "crates/sdk/src/listing/operational_listing/mutation.rs", + "crates/sdk/src/listing/operational_listing/price_ext.rs", + "crates/sdk/src/listing/operational_listing/validation.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/market_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/privacy.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/src/storage.rs", + "crates/sdk/src/studio_store.rs", + "crates/sdk/src/sync.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/trade_storage.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/geonames.rs", + "crates/sdk/tests/identity_public_api.rs", + "crates/sdk/tests/identity_retired_surface.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/lifecycle.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/public_api.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/runtime_contract_public_api.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/support/serializer_failure.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/adapters_relay_tests.rs", + "crates/sdk/tests/unit/adapters_signing_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/identity_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/market_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/relay_targets_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "crates/sql_wasm_runtime/Cargo.toml", + "crates/sql_wasm_runtime/src/lib.rs", + "crates/trade_bindings/Cargo.toml", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/api_qualification.rs", + "tools/xtask/src/architecture.rs", + "tools/xtask/src/architecture/api_leakage.rs", + "tools/xtask/src/architecture/dependency_boundary.rs", + "tools/xtask/src/bindings.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/cli_host.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/coverage.rs", + "tools/xtask/src/coverage_policy.rs", + "tools/xtask/src/coverage_policy_tests.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/fs.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/manifest.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs", + "tools/xtask/src/portable_qualification.rs", + "tools/xtask/src/release_graph.rs", + "tools/xtask/src/release_qualification.rs", + "tools/xtask/src/smoke.rs", + "tools/xtask/src/supply_chain_qualification.rs", + "tools/xtask/src/target_qualification.rs", + "tools/xtask/src/wasm.rs", + "tools/xtask/src/wasm_declarations.rs", + "tools/xtask/tests/fixtures/api-leakage/adr-exception.rs", + "tools/xtask/tests/fixtures/api-leakage/allowed-concrete-adapter.rs", + "tools/xtask/tests/fixtures/api-leakage/generic-renamed-tokio.rs", + "tools/xtask/tests/fixtures/api-leakage/generic-sqlx.rs", + "tools/xtask/tests/fixtures/api-leakage/private-implementation.rs", + "tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json", + "tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json", + "tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json", + "tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json" + ], + "normalized_patch_sha256": "b0ca53a387928fc4a15efa92a20be92605d20569ada5fa56aa057eb9ab6d6c19", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4dce5873dedc247e3dca34352a8ebba5be9cb89f", + "target_commit": "6a9cdfb1bda0181ab824967433cac737e3ceb79a", + "source_parents": [ + "5a0764a86d774f6227af846fa3dafbadb7d3dd4e" + ], + "target_parents": [ + "c5d49822d16a2653f2ec4f4e6253319f27a2dabb" + ], + "source_subject": "sdk: enforce accepted proxy satisfaction", + "target_subject": "sdk: enforce accepted proxy satisfaction", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T06:49:46Z", + "target_author_time": "2026-07-13T06:49:46Z", + "source_committer_time": "2026-07-13T06:49:46Z", + "target_committer_time": "2026-07-13T06:49:46Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "59553c164466628c505f64d2b7aa8142ad09ff7ef730f56928b3eae1d988da92", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4e264626a1932b374c1a83975e2c52c6595f800d", + "target_commit": "6a01051d3bc26836464dbdfa51a70f966ca7bbfc", + "source_parents": [ + "1546416bcb46198042d8a373ba7f8cdc3a9e496f" + ], + "target_parents": [ + "8b9fc2017a6bacf6f96d94e3a7b97b1611d26d29" + ], + "source_subject": "sdk: harden farm order runtime guards", + "target_subject": "sdk: harden farm order runtime guards", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T20:42:11-07:00", + "target_author_time": "2026-06-18T20:42:11-07:00", + "source_committer_time": "2026-06-18T20:42:11-07:00", + "target_committer_time": "2026-06-18T20:42:11-07:00", + "source_paths": [ + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "447d76ee4e8367a0445b8a11f8a91c9de9e133d78f3f212d67ea1a3e4922978d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4e3e52b300904045023980f91379b00e097f1c75", + "target_commit": "a67cf7a93a9462341307d85f361122f4e0f01d07", + "source_parents": [ + "3fa03e7b623a09eb4da7c5b2283d3aa32aa60919" + ], + "target_parents": [ + "c5d6ee72094ccb1f368389569d6e179dd9a3a4e1" + ], + "source_subject": "nostr: migrate workspace consumers", + "target_subject": "nostr: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T14:38:30Z", + "target_author_time": "2026-07-31T14:38:30Z", + "source_committer_time": "2026-07-31T14:38:30Z", + "target_committer_time": "2026-07-31T14:38:30Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "887dae7a0ca58d4a38aba3516e9ca7ebafcac1f609df38d4e43f3627ddf62615", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4ea65f211b0e3246f2563d3af1683b461ed4ce6b", + "target_commit": "958b9f6c1c056fe4a8a73ad80b3da69d0f50fda8", + "source_parents": [ + "9abf9f2b8751e9cb79da519e59e96db2fd3f4ca1" + ], + "target_parents": [ + "2d157f928e7cccad7a60d060718db448ec5905e8" + ], + "source_subject": "sync: report Reticulum preview push receipts", + "target_subject": "sync: report Reticulum preview push receipts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T04:17:40Z", + "target_author_time": "2026-07-09T04:17:40Z", + "source_committer_time": "2026-07-09T04:17:40Z", + "target_committer_time": "2026-07-09T04:17:40Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "d8827830cbab9fe949d64ac08ca738859080d234a746e78d15b03191311ffcf7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4f13a8cf263fd45dd2794270a40058b752f3a27c", + "target_commit": "652c23d6068c67e859eed256ade32684125db0e4", + "source_parents": [ + "55af5f70f803c699bab123f5f3942b4e4da448f1" + ], + "target_parents": [ + "cb5ff2e27e42c261ab3e122d8d85d75a108ce6e3" + ], + "source_subject": "sdk: compose canonical lower interfaces in ClientBuilder", + "target_subject": "sdk: compose canonical lower interfaces in ClientBuilder", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:25:25Z", + "target_author_time": "2026-08-03T10:25:25Z", + "source_committer_time": "2026-08-03T10:25:25Z", + "target_committer_time": "2026-08-03T10:25:25Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "52a16bbf411a8a6ad1d04083c971438205808aac2d5464e2035ff1504567c912", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4f3dc3dff6d07980e0ad07970991137e833acf12", + "target_commit": "b5fe8b822840439f71c697ce1b57672777c711e0", + "source_parents": [ + "f3e863b1c50384378df18549c0d08e34732b1e1b" + ], + "target_parents": [ + "7132368233259ecb1887654fc163f4ec3a5cc018" + ], + "source_subject": "sdk: cover empty configured relay enqueue", + "target_subject": "sdk: cover empty configured relay enqueue", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T13:18:04-07:00", + "target_author_time": "2026-06-16T13:18:04-07:00", + "source_committer_time": "2026-06-16T13:18:04-07:00", + "target_committer_time": "2026-06-16T13:18:04-07:00", + "source_paths": [ + "crates/sdk/tests/listings_runtime.rs" + ], + "normalized_patch_sha256": "8cc42481b57b8d7880aa5412a08caf449410a4a5ee9d776a8a8eb7544bdc02d7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4f6a1033b2e7a79ebb136fb752f6a9eda0ff2605", + "target_commit": "5c054aae7fe507850c35792741bc533bf3356a16", + "source_parents": [ + "af1ebd63d35c374c1ee97e7a32fe45063b6dde5e" + ], + "target_parents": [ + "b23996d81a0d7d1c4ea99caa54d610d90ca9f110" + ], + "source_subject": "nostr-connect: stabilize request and response envelopes", + "target_subject": "nostr-connect: stabilize request and response envelopes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T18:34:05Z", + "target_author_time": "2026-07-31T18:34:05Z", + "source_committer_time": "2026-07-31T18:34:05Z", + "target_committer_time": "2026-07-31T18:34:05Z", + "source_paths": [ + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "1a260949b85d18bbf78357f1b2c184d83669a7731188d2da48629627c2d87938", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "4f72eaf44956b54b0f5f76b7560482be9ae09843", + "target_commit": "11fca7cbb6bb8fa96080b064515605ef455d09e5", + "source_parents": [ + "102c546191f9886958c5072582fee45ff1712919" + ], + "target_parents": [ + "2ba94943a3730ca9bb4eb5aa7c977ebba095b4ea" + ], + "source_subject": "sdk: harden sync failure tests", + "target_subject": "sdk: harden sync failure tests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:17:07-07:00", + "target_author_time": "2026-06-16T00:17:07-07:00", + "source_committer_time": "2026-06-16T00:17:07-07:00", + "target_committer_time": "2026-06-16T00:17:07-07:00", + "source_paths": [ + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "4d8361aeab7a026c8f7404a644162849f4e67c8e0d26e2041a35a20ab30a4734", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "508cd34459fbb4cf68d420600c7c7a0004b67f6d", + "target_commit": "00c041790515ce7e9a45ee8d8dd4d984f9b2d7d4", + "source_parents": [ + "a699b7f2674f4fe2d2ac8ec3ad47bd6484b1bdb4" + ], + "target_parents": [ + "94589c847a864f5e2cab578473a210c32659f03c" + ], + "source_subject": "feat(bindings): generate identity and indexed packages", + "target_subject": "feat(bindings): generate identity and indexed packages", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T06:19:44-07:00", + "target_author_time": "2026-06-11T06:19:44-07:00", + "source_committer_time": "2026-06-11T06:19:44-07:00", + "target_committer_time": "2026-06-11T06:19:44-07:00", + "source_paths": [ + "crates/identity_bindings/src/lib.rs" + ], + "normalized_patch_sha256": "e9ad69f139817b12b663769b1523c658bebfab94537cae4315b79299c62def15", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "50f6d337917af8407470f239de740c7f8ce3ef2d", + "target_commit": "8b9328b550b4d4637fb045bb0685d9678527109f", + "source_parents": [ + "3bc5714bb37b289d960d1ee45d62955f81e9734a" + ], + "target_parents": [ + "55d6c41bb1b6cd73061ae9a6db183600d8d1a01a" + ], + "source_subject": "chore: snapshot local changes before remote alignment", + "target_subject": "chore: snapshot local changes before remote alignment", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-17T19:28:27Z", + "target_author_time": "2026-07-17T19:28:27Z", + "source_committer_time": "2026-07-17T19:28:27Z", + "target_committer_time": "2026-07-17T19:28:27Z", + "source_paths": [ + "tools/xtask/src/coverage_policy_tests.rs" + ], + "normalized_patch_sha256": "8a516a9e690424422fba70bad28475ad1f1fd9ab36acf80a5d10690b0055b161", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "510269bfb468fab87117686ae23ec1ad8d14732a", + "target_commit": "c13137f987420fa4944a08b40663271fd2d602c3", + "source_parents": [ + "b21032732edde890590cb3b86f2a7aa7cb252a9a" + ], + "target_parents": [ + "94ca83e4764520d260c81e2ebee2512f8747ec30" + ], + "source_subject": "sdk: quarantine superseded package surface", + "target_subject": "sdk: quarantine superseded package surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:29:52Z", + "target_author_time": "2026-08-03T12:29:52Z", + "source_committer_time": "2026-08-03T12:29:52Z", + "target_committer_time": "2026-08-03T12:29:52Z", + "source_paths": [ + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/signer.rs", + "crates/sdk/src/geonames.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/identity.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/privacy.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/geonames.rs", + "crates/sdk/tests/identity_public_api.rs", + "crates/sdk/tests/identity_retired_surface.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/secrets_migration_boundary.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/support/serializer_failure.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "96a84a32c990a019b1f45b8e736cc1cbcca40c0f5d764b422bc835d2bc326c59", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "516fb4231e6227193ca79385797d339f1a8f3dad", + "target_commit": "993a633408a9db3f2a82ee6da33f9f5767db39c9", + "source_parents": [ + "45179a3b17a709257296f16cc0979a76286d757d" + ], + "target_parents": [ + "ad894de3982f245a838cb2512f36e7e4f3dbafb4" + ], + "source_subject": "docs: update target policy README wording", + "target_subject": "docs: update target policy README wording", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T18:10:22Z", + "target_author_time": "2026-07-09T18:10:22Z", + "source_committer_time": "2026-07-09T18:10:22Z", + "target_committer_time": "2026-07-09T18:10:22Z", + "source_paths": [ + "crates/sdk/README" + ], + "normalized_patch_sha256": "0bda8fdc0fea11f4e8506635d4d64f315aab135368fda09a66944426ca37a2b8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5217d737737ba270b7bf26a4f5e426d5c985d8d3", + "target_commit": "7b0e4b3ef0f7fd41a2dd13318d974d973a04fcd7", + "source_parents": [ + "c473ea2400efba5aa003cff5bbeb1928de1872d1" + ], + "target_parents": [ + "8bc581e6809e7065beda32cb31fbb91e8d6bf68c" + ], + "source_subject": "test: preserve operational listing qualification", + "target_subject": "test: preserve operational listing qualification", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T15:32:17Z", + "target_author_time": "2026-08-04T15:32:17Z", + "source_committer_time": "2026-08-04T15:32:17Z", + "target_committer_time": "2026-08-04T15:32:17Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/listing/operational_listing/draft.rs", + "crates/sdk/src/listing/operational_listing/mod.rs", + "crates/sdk/src/listing/operational_listing/mutation.rs", + "crates/sdk/src/listing/operational_listing/price_ext.rs", + "crates/sdk/src/listing/operational_listing/validation.rs" + ], + "normalized_patch_sha256": "128866a82f0048936c2ef22c9c18a77fdb004c2593e5e855c34f01a4b6cd3c69", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "521d9250e7c25a4a2d4e185e43089daa1b5f8ddf", + "target_commit": "4c34983d3e8a18f9f61f1bcccc82e825ac001482", + "source_parents": [ + "afaa2f2853bce4fed6c45e9902a2b35a5c04fcac" + ], + "target_parents": [ + "3d534dcd4faf0042c7b538a9bb1d7ec0db3e9507" + ], + "source_subject": "event-index: refresh checkpoint bindings", + "target_subject": "event-index: refresh checkpoint bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T02:05:37Z", + "target_author_time": "2026-07-13T02:05:37Z", + "source_committer_time": "2026-07-13T02:05:37Z", + "target_committer_time": "2026-07-13T02:05:37Z", + "source_paths": [ + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "c923e0cca026e87eb85e9c20baab0ed256f42193874bc87316888ad82c5a98f9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "52be745797061ad23fe82f77470dcd58cbb5bcf0", + "target_commit": "eb827e0a4b4dd97e444ab138c1377b3884855fb7", + "source_parents": [ + "bc9de9e176233b58a46261de2be9b66b1109fda7" + ], + "target_parents": [ + "f5e7b656ce9d3dac3bc8281ee09088cc5c7a1a81" + ], + "source_subject": "status: add bounded trade status watches", + "target_subject": "status: add bounded trade status watches", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T04:33:09Z", + "target_author_time": "2026-07-05T04:33:09Z", + "source_committer_time": "2026-07-05T04:33:09Z", + "target_committer_time": "2026-07-05T04:33:09Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "03ea70adef8f15f1e72c494b38fdec60042ce6d8ccf170ad0c279ced745b9061", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "52e1cfff74b8d7720d5bac84bc2367cc8c5edee4", + "target_commit": "e9d27715b40ec1b719d0f2e8531928a12ba3b7d7", + "source_parents": [ + "82ad5c39345238c1e259ac0b8c8a76c349eea3e8" + ], + "target_parents": [ + "4c33070bfc26e31db05cd0ae0bd058ba02755dbe" + ], + "source_subject": "sdk: guard Reticulum preview push boundary", + "target_subject": "sdk: guard Reticulum preview push boundary", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T06:27:00Z", + "target_author_time": "2026-07-07T06:27:00Z", + "source_committer_time": "2026-07-07T06:27:00Z", + "target_committer_time": "2026-07-07T06:27:00Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "de78d015493383dd21b8d4eb5eb59195756b4827b309daa9d765c74784d5ebe6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5313c6fc6da6982943d54dacb01f0068d09b81ae", + "target_commit": "c9de1f9b7c550928f364cb53773e74ba4156e96e", + "source_parents": [ + "9d473e576af62208a3e35c54cd05da263196049c" + ], + "target_parents": [ + "854e2423de13c95be43a8abdbc5b7a9f3182e904" + ], + "source_subject": "contract: add runtime contract v1 root", + "target_subject": "contract: add runtime contract v1 root", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T00:25:24Z", + "target_author_time": "2026-07-15T00:25:24Z", + "source_committer_time": "2026-07-15T00:25:24Z", + "target_committer_time": "2026-07-15T00:25:24Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/identity_public_api.rs", + "crates/sdk/tests/runtime_contract_public_api.rs", + "crates/sdk/tests/source_boundary.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/contracts.rs" + ], + "normalized_patch_sha256": "f764f18b15cf4f9ddb45a2ff34b94ff83a58ad7b2d782fb6b6d8e1c26dad9660", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "544d5c40006e8e3b27e1ca1e0a13543237abf4b3", + "target_commit": "0544b1a8d9b55d462699821b3c821e0241346e60", + "source_parents": [ + "a314092543bd2eeb6fe0fef033790651083969b1" + ], + "target_parents": [ + "47938c2d6e7702401014c2cad72b33acf2c077d5" + ], + "source_subject": "orders: rename SDK Nostr evidence contracts", + "target_subject": "orders: rename SDK Nostr evidence contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T21:18:25Z", + "target_author_time": "2026-07-09T21:18:25Z", + "source_committer_time": "2026-07-09T21:18:25Z", + "target_committer_time": "2026-07-09T21:18:25Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "b0b795e7caf2f0f7eb7db9304983551dbf0f25b64063bd6a639ab22ce2a0e399", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5485b48739a027ed245e5004047de3c8ded3f4ad", + "target_commit": "c781760ba4184409b4bd598721ccbafa47901b8f", + "source_parents": [ + "4472fe288ba42cbd095009309d6e11751c4b7d55" + ], + "target_parents": [ + "90c1ec6d55810f90b9858b4bc35881d714267ac4" + ], + "source_subject": "storage: add SDK status backup and integrity APIs", + "target_subject": "storage: add SDK status backup and integrity APIs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T14:17:23-07:00", + "target_author_time": "2026-06-17T14:17:23-07:00", + "source_committer_time": "2026-06-17T14:17:23-07:00", + "target_committer_time": "2026-06-17T14:17:23-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "6ed0d2bf158f261fd111655b6dc2e954d465abd98b8318b4af6f6d14b5d453c8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "553c30f2dcc4f9e8dd5e751260705a3b6f4c4620", + "target_commit": "00b6b5e3859930989deb29c3c092f3aa544e73e0", + "source_parents": [ + "c0b741eb478814dbbeba63fd7c27f61a9f1336b7" + ], + "target_parents": [ + "4bfe8c0db49f3313146ff6f1c923fd2e9faeb0e2" + ], + "source_subject": "build: align standalone runtime dependencies", + "target_subject": "build: align standalone runtime dependencies", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T21:44:53Z", + "target_author_time": "2026-07-15T21:44:53Z", + "source_committer_time": "2026-07-15T21:44:53Z", + "target_committer_time": "2026-07-15T21:44:53Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs" + ], + "normalized_patch_sha256": "305af10acc40d30242a53a3219d0cfa78dee3e7c0ba90e1c15668ffb86259185", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "55af5f70f803c699bab123f5f3942b4e4da448f1", + "target_commit": "cb5ff2e27e42c261ab3e122d8d85d75a108ce6e3", + "source_parents": [ + "03c568672034f9d33e896c9585eaea2362c664d6" + ], + "target_parents": [ + "4a5e94fd492c0d1b870cf45d262f0d340625ea13" + ], + "source_subject": "sdk: make the SDK std-only and finalize root modules", + "target_subject": "sdk: make the SDK std-only and finalize root modules", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:14:13Z", + "target_author_time": "2026-08-03T10:14:13Z", + "source_committer_time": "2026-08-03T10:14:13Z", + "target_committer_time": "2026-08-03T10:14:13Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "ff5213db18e4180daaca0c505049a06cbc6162a3a894418ffdbe7c84ca6c95f3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "55eee4919fe3a7294bcfb2d13044e004f68d915d", + "target_commit": "685e492a97c05709849a6638a3a062a8589f584a", + "source_parents": [ + "3d45ad617d6c0f90e7417b64b4518d65c81b69f7" + ], + "target_parents": [ + "e069cebfce543dff4e541f8c43b0f319e0e7664c" + ], + "source_subject": "orders: require validator-set receipt trust", + "target_subject": "orders: require validator-set receipt trust", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T17:34:32Z", + "target_author_time": "2026-07-15T17:34:32Z", + "source_committer_time": "2026-07-15T17:34:32Z", + "target_committer_time": "2026-07-15T17:34:32Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "f801242ccb5e6e76c8536970f06c0515983f019d57a5099e06c1be98f25cbf9a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "569d2f2a8380f737177d55b1e8e106ec2451cea9", + "target_commit": "d8947e9b201efcf6cfd02d921578ebea81d04b88", + "source_parents": [ + "ab61f7abfbba4660818d29f821fd6c522e7144df" + ], + "target_parents": [ + "f60ccb9e3d7be2b69754049152d0e85b40d44873" + ], + "source_subject": "sdk: remove legacy client config facade", + "target_subject": "sdk: remove legacy client config facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T03:19:25-07:00", + "target_author_time": "2026-06-19T03:19:25-07:00", + "source_committer_time": "2026-06-19T03:19:25-07:00", + "target_committer_time": "2026-06-19T03:19:25-07:00", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/config.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/config.rs", + "crates/sdk/tests/radrootsd.rs", + "crates/sdk/tests/relay_direct.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "90189fa0c39dad3347f0cdb7baf58e674f08a191f354cde892e8287ea83df07f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "56efb90bc4b94143861ec543a9e85a4a656300df", + "target_commit": "5c9b7fc8242415722aa1845a751dd45879715c2e", + "source_parents": [ + "8ac37b1bc223c2c6a7c84435cb97fc96f4614b68" + ], + "target_parents": [ + "c6e7c084bbd4ad279c7232ba3fc37b04bf6a6e3b" + ], + "source_subject": "replica: harden schema dto parity", + "target_subject": "replica: harden schema dto parity", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T22:21:26Z", + "target_author_time": "2026-06-24T22:21:26Z", + "source_committer_time": "2026-06-24T22:21:26Z", + "target_committer_time": "2026-06-24T22:21:26Z", + "source_paths": [ + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "964175c5577e03418ba8081668d9523cf5c3f5f2d144a38fd0aab84f5d83d926", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5811685dc028eee45e1e0087b06ecd1a0b59fb1e", + "target_commit": "cea9597aa28d34ad5240836b1b2009905937d699", + "source_parents": [ + "ab124d4a7476f5fd139504c32a77df0cdec51b54" + ], + "target_parents": [ + "f23567944d586dfd03b0831aef88a5cae0052c96" + ], + "source_subject": "sync: preserve proxy preview outcomes", + "target_subject": "sync: preserve proxy preview outcomes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T09:01:12Z", + "target_author_time": "2026-07-07T09:01:12Z", + "source_committer_time": "2026-07-07T09:01:12Z", + "target_committer_time": "2026-07-07T09:01:12Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "319734e5ed96519380dfb3dd13c96fa3579b6dcdc2474f0ea034166b59d5cdd3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "58c42a7a77cb5f60dd10bb50695d92586232adfd", + "target_commit": "650aac1745fbadb4aa342677229a4193c6e15153", + "source_parents": [ + "d62f884833ebf2d27b818e73e227b6c9a9a49af6" + ], + "target_parents": [ + "ed1e55fb77759dfeb5c2f482e9e927786537a653" + ], + "source_subject": "sdk: remove Studio state from SDK storage", + "target_subject": "sdk: remove Studio state from SDK storage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:53:57Z", + "target_author_time": "2026-08-03T10:53:57Z", + "source_committer_time": "2026-08-03T10:53:57Z", + "target_committer_time": "2026-08-03T10:53:57Z", + "source_paths": [ + "crates/sdk/README.md", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/studio_store.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "2819d1eecaa1b69fd4f0c8158c4dc53cb83dd4ced6192610fdaad713668c2958", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "595bf8a14f649422e2d316fa913e3f15a728e231", + "target_commit": "90052ecf06dcaa59faf97a55894846b7e8c418b1", + "source_parents": [ + "c06fefe03f3bdb7f0e24b1027f65f18b3397e2c4" + ], + "target_parents": [ + "3273f6cd82edd0279ce186e97aa3fd7c76777ea9" + ], + "source_subject": "sdk: cover crate coverage edge paths", + "target_subject": "sdk: cover crate coverage edge paths", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T07:19:56Z", + "target_author_time": "2026-06-23T07:19:56Z", + "source_committer_time": "2026-06-23T07:19:56Z", + "target_committer_time": "2026-06-23T07:19:56Z", + "source_paths": [ + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/relay.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/identity.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/support/serializer_failure.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/adapters_relay_tests.rs", + "crates/sdk/tests/unit/adapters_signing_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/identity_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/relay_targets_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "57fcd66f069706bd90adf1e368a6687e2a94abd59af5210406b26c287f5988e1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "59a6d373ca009c152a8afad482cd7de0e4d5cf1e", + "target_commit": "95b147d7db8ed8461f7d405c403c40b3fb3afc9a", + "source_parents": [ + "3e67c7d9d5f0b12cbc5b8ab0c2ad36c25ed7caa7" + ], + "target_parents": [ + "6cd934641036e19250352312a252df44d2da99c5" + ], + "source_subject": "workspace: align public package layout and names", + "target_subject": "workspace: align public package layout and names", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T10:20:13Z", + "target_author_time": "2026-07-27T10:20:13Z", + "source_committer_time": "2026-07-27T10:20:13Z", + "target_committer_time": "2026-07-27T10:20:13Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/README.md", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "6ee0471c52063faeea178d6f8c2c13c2f24b21d6d4d5695821ed957154eb2bcb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "59b26c73bab504495c77496a752351ac9075b516", + "target_commit": "acfee770ccadad142cd6ba183db48740d2e222e8", + "source_parents": [ + "ad249fe5b25a4f782e944019b165922363983295" + ], + "target_parents": [ + "b9306511817a8abf8d560c30db4d1e7cac8866cd" + ], + "source_subject": "test(coverage): enforce sdk release floor", + "target_subject": "test(coverage): enforce sdk release floor", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T10:56:55Z", + "target_author_time": "2026-08-04T10:56:55Z", + "source_committer_time": "2026-08-04T10:56:55Z", + "target_committer_time": "2026-08-04T10:56:55Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/radroots/src/client.rs", + "crates/radroots/src/lib.rs", + "crates/radroots/tests/package_boundary.rs", + "crates/radroots/tests/public_surface.rs", + "crates/sdk/src/capability.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/src/trade.rs", + "crates/sdk/src/transport.rs", + "tools/xtask/src/coverage.rs", + "tools/xtask/src/coverage_policy.rs", + "tools/xtask/src/coverage_policy_tests.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "ffd1426cbbcc67ad9dabfaa5684528d0b81c5ccea555cc77a48477e35d9273f8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "59e7456f4f3d354f578fadeb642fce7c25862be9", + "target_commit": "9dbd53dc24b8aff3d421a6cac4c5cbec0616d7da", + "source_parents": [ + "2a2d79afd5a9766e5e954f55c5b31d8f391dbfa9" + ], + "target_parents": [ + "8f8435669750f272889501b0fef0c173c32d3860" + ], + "source_subject": "tests: harden SDK coverage gates", + "target_subject": "tests: harden SDK coverage gates", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T11:37:15Z", + "target_author_time": "2026-06-26T11:37:15Z", + "source_committer_time": "2026-06-26T11:37:15Z", + "target_committer_time": "2026-06-26T11:37:15Z", + "source_paths": [ + "crates/sdk/src/market_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/tests/geonames.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/market_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "bc4c7b5bea261d56cf0ea18567e46533d8d7554211d26606dc275271636797bb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5a0764a86d774f6227af846fa3dafbadb7d3dd4e", + "target_commit": "c5d49822d16a2653f2ec4f4e6253319f27a2dabb", + "source_parents": [ + "17273d9cdaaa567fc988dfccb7cc5b1c6d6348c9" + ], + "target_parents": [ + "1b89ecc4e6ea92ff9f8f416b08db22502a7a5dde" + ], + "source_subject": "sdk: expose transport operation capabilities", + "target_subject": "sdk: expose transport operation capabilities", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T06:17:06Z", + "target_author_time": "2026-07-13T06:17:06Z", + "source_committer_time": "2026-07-13T06:17:06Z", + "target_committer_time": "2026-07-13T06:17:06Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "3913bbd266dfaf34e444a0a47ca93391517f41f9c98e9c12c1b28d513ec10f63", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5a8c47f2a43bed9e0d8e41cc9787287b8f9944f9", + "target_commit": "acf36b2f39f4cc44b9b01519d99c31bd7a5d68e6", + "source_parents": [ + "b0b478041758ff997c99b45f682ffa7e9681adc3" + ], + "target_parents": [ + "a8e30341d98ae4b1e8ee17804fbe44e16afef697" + ], + "source_subject": "sdk: adapt to persistence-free trade boundary", + "target_subject": "sdk: adapt to persistence-free trade boundary", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T07:36:01Z", + "target_author_time": "2026-07-30T07:36:01Z", + "source_committer_time": "2026-07-30T07:36:01Z", + "target_committer_time": "2026-07-30T07:36:01Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/error.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/tests/unit/error_tests.rs" + ], + "normalized_patch_sha256": "7dac22b45835b24df3952a21a4405b4edc8ab2746fb9649201a8335d112935f3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5ba3d4d817e8fd17e20b791dcaae9a6869d483e9", + "target_commit": "218cdabd7196ba33a4a11319276d14de430bfd63", + "source_parents": [ + "e9d7d77cc105113afc209cc80949bcfb93ef4aa9" + ], + "target_parents": [ + "e3bb612d4410aa0dcdee75d0523923eb54d65a35" + ], + "source_subject": "sdk: validate knowledge builders with core models", + "target_subject": "sdk: validate knowledge builders with core models", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T04:38:37Z", + "target_author_time": "2026-07-06T04:38:37Z", + "source_committer_time": "2026-07-06T04:38:37Z", + "target_committer_time": "2026-07-06T04:38:37Z", + "source_paths": [ + "crates/sdk/src/knowledge.rs", + "crates/sdk/tests/knowledge_public_api.rs" + ], + "normalized_patch_sha256": "af3e15bfe04992c8713e5108c33a7ce06b65bbf2f93069dacadf0087a3d3ff89", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5ba42d6b2ff0f3d618d4c79478554850c2ef10a2", + "target_commit": "e86ac0f3532d0513a8f61ec05287bec2ac0fd5cc", + "source_parents": [ + "faacecec47f2727159ba4d53b0fe9d53e8786732" + ], + "target_parents": [ + "a4eb86587fdf71fa8080e5a64aa28eeafa113924" + ], + "source_subject": "sdk: remove target-bound default idempotency", + "target_subject": "sdk: remove target-bound default idempotency", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T01:05:30Z", + "target_author_time": "2026-07-09T01:05:30Z", + "source_committer_time": "2026-07-09T01:05:30Z", + "target_committer_time": "2026-07-09T01:05:30Z", + "source_paths": [ + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "75a1327d831d089346b11690dc051163f68d5562293bfbc0ccac9859df309d4d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5bff0e0b129483147a7693d25829dfdfd3f05053", + "target_commit": "c96a7c6397286f517f19c600c8da8438b159c7e8", + "source_parents": [ + "0d0bf4dd72e19f44f60883244784e24271dc45e6" + ], + "target_parents": [ + "1590d50b1209393a5b69e36ecdecb75e59c41597" + ], + "source_subject": "sdk: separate listing observation time", + "target_subject": "sdk: separate listing observation time", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T22:34:37-07:00", + "target_author_time": "2026-06-15T22:34:37-07:00", + "source_committer_time": "2026-06-15T22:34:37-07:00", + "target_committer_time": "2026-06-15T22:34:37-07:00", + "source_paths": [ + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/listings_runtime.rs" + ], + "normalized_patch_sha256": "75516921593fba9fcb2fad81638aba906179f852938d201efe7d32e2e44c1218", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5df4470a539cd0438d791780e747e53af22c0ff8", + "target_commit": "ebf590c826aeb1d22276410f6662603c55f836ab", + "source_parents": [ + "5bff0e0b129483147a7693d25829dfdfd3f05053" + ], + "target_parents": [ + "c96a7c6397286f517f19c600c8da8438b159c7e8" + ], + "source_subject": "sdk: split runtime target modules", + "target_subject": "sdk: split runtime target modules", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T22:37:58-07:00", + "target_author_time": "2026-06-15T22:37:58-07:00", + "source_committer_time": "2026-06-15T22:37:58-07:00", + "target_committer_time": "2026-06-15T22:37:58-07:00", + "source_paths": [ + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime_targets.rs" + ], + "normalized_patch_sha256": "d24737448c175f9e151fa98c1398d16f2f406011cfa776e773267653ba7def76", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5f51a1af8d85087d48dc57e9e53b3baede2eb2de", + "target_commit": "09f1b3c02b45a7ae01a0886c6ac8ab6f42e53924", + "source_parents": [ + "e1e18ae08f3942df560a881d2db669507eb0b2d7" + ], + "target_parents": [ + "6c365becb0591623964537f02d78f2809c103dce" + ], + "source_subject": "dto: render events bindings from registry", + "target_subject": "dto: render events bindings from registry", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T07:38:26Z", + "target_author_time": "2026-06-24T07:38:26Z", + "source_committer_time": "2026-06-24T07:38:26Z", + "target_committer_time": "2026-06-24T07:38:26Z", + "source_paths": [ + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "b9da40a009a15cb4c67b255b6ad9e50faaad5e2359c4462e63c65509ba4cc7a3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "5fadc890e491d62686cb06ebe1a0f739abc8895d", + "target_commit": "73924d46fc5839f31c7842ba39b2993f06df6a0c", + "source_parents": [ + "aca99c1fbf80dfa923ccaac4c353bf4d24de4419" + ], + "target_parents": [ + "a1f0387c02a864495b0f9cce4a0e585df03ba3ea" + ], + "source_subject": "sdk: harden sync push claim resilience", + "target_subject": "sdk: harden sync push claim resilience", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T17:04:49-07:00", + "target_author_time": "2026-06-15T17:04:49-07:00", + "source_committer_time": "2026-06-15T17:04:49-07:00", + "target_committer_time": "2026-06-15T17:04:49-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "28339a32ea0291cd672a62e67ec86c91c1119bd976341b86ae568b7c2686baf6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "602a9095572f9f2c02815104b5559b44c5cb326b", + "target_commit": "ab2bda16beed08db8819299d4cda167d89428da2", + "source_parents": [ + "805bda41e9075de343943e280b9233ee7462e45c" + ], + "target_parents": [ + "59b737cea14c03552f20a3cd49873906249f5c5d" + ], + "source_subject": "signing: remove generic wire-part authoring bypass", + "target_subject": "signing: remove generic wire-part authoring bypass", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-19T20:34:23Z", + "target_author_time": "2026-07-19T20:34:23Z", + "source_committer_time": "2026-07-19T20:34:23Z", + "target_committer_time": "2026-07-19T20:34:23Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_signing_tests.rs" + ], + "normalized_patch_sha256": "7267fea078a0d5365ab6592e689639792f03342c6de9c795daca7b3f58b8e257", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "603776a1d16d661d4a944f24dba5564c10c4e879", + "target_commit": "461bcae68c5a8bc3239b3db0e1788ecbd3c0653c", + "source_parents": [ + "a86d62821f7fe0eb50cf11dfe0dd5c4b7bc6b9cb" + ], + "target_parents": [ + "039d72813c0dadb0c14b10e04f68eabce6514786" + ], + "source_subject": "event: replace identifier strings with canonical owned types", + "target_subject": "event: replace identifier strings with canonical owned types", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T22:06:29Z", + "target_author_time": "2026-07-28T22:06:29Z", + "source_committer_time": "2026-07-28T22:06:29Z", + "target_committer_time": "2026-07-28T22:06:29Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "e90d4fa72a39ab2bad2c1fc13666b4fd590935fe330c7c2baaea8a97a68ad1dd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6060e2303856227a0f78207e95926f09b99b62f9", + "target_commit": "f54abd7f296006006d855ab37a20eba56e2c78d6", + "source_parents": [ + "c61a2289d2fab174168ffeb4192dad388075a5af" + ], + "target_parents": [ + "296fbbfad980580370d2f851b3f07544e50b5a75" + ], + "source_subject": "sdk: support trade product publish outcomes", + "target_subject": "sdk: support trade product publish outcomes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:34:29Z", + "target_author_time": "2026-06-30T03:34:29Z", + "source_committer_time": "2026-06-30T03:34:29Z", + "target_committer_time": "2026-06-30T03:34:29Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "407afad3f6298b98708ef2639463d9df0fa92694220cb194c8ec9f3992e6098f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "618d199c2893831d5a208080bb38fda7e750f0b5", + "target_commit": null, + "source_parents": [ + "6541c64d7db10ad1f1f8412310f16b82761ae416" + ], + "target_parents": [], + "source_subject": "build: refresh radroots lock edge", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-14T15:30:43-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-14T15:30:43-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "627b6823c9f3f44f0e1bd256ba019c92a046e22d", + "target_commit": "e7aa04171d9f8972c8cce69129df0efec2858ce8", + "source_parents": [ + "544d5c40006e8e3b27e1ca1e0a13543237abf4b3" + ], + "target_parents": [ + "0544b1a8d9b55d462699821b3c821e0241346e60" + ], + "source_subject": "sdk: harden proxy satisfaction metadata", + "target_subject": "sdk: harden proxy satisfaction metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T01:43:34Z", + "target_author_time": "2026-07-10T01:43:34Z", + "source_committer_time": "2026-07-10T01:43:34Z", + "target_committer_time": "2026-07-10T01:43:34Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "c4a64613aa16c6e87a0f8d41bf87c621b7faaa960c797c6c0b7e222bd9b5970a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "62b4062208f74b7359a75a2ca49375642c20f39c", + "target_commit": null, + "source_parents": [ + "041f0f1ca9d128b00f6c72482b07da204d56e29b" + ], + "target_parents": [], + "source_subject": "xtask: harden wasm package generation", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-13T17:00:49-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-13T17:00:49-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "630651c55900455d1c8acd3aa671dd919bfb45f8", + "target_commit": "2de5fda8382b4548f960f12e5b85de980677958d", + "source_parents": [ + "ad2543821b705477956180e7600d05958069c2ee" + ], + "target_parents": [ + "6624524c834a20482e5aed7153c9f727f40a0c00" + ], + "source_subject": "transport: return proxy error receipts", + "target_subject": "transport: return proxy error receipts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T03:40:44Z", + "target_author_time": "2026-07-08T03:40:44Z", + "source_committer_time": "2026-07-08T03:40:44Z", + "target_committer_time": "2026-07-08T03:40:44Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "e55e976fadcb53edbdab305c59b2c515b5eb0842f32b11895a785748cebbc802", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "632465a739f8fdd4b2c6911df3ed65846690694c", + "target_commit": "a454e95a7955b858351004a7d65cebe9c72e8dd5", + "source_parents": [ + "be22167ee5a1a3f4b93a8f892acbec76d34cf48e" + ], + "target_parents": [ + "50305905ff42137bc9a56c0cfb8f3a0963c43a09" + ], + "source_subject": "identity: quarantine superseded package surface", + "target_subject": "identity: quarantine superseded package surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T07:07:24Z", + "target_author_time": "2026-07-28T07:07:24Z", + "source_committer_time": "2026-07-28T07:07:24Z", + "target_committer_time": "2026-07-28T07:07:24Z", + "source_paths": [ + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/identity_retired_surface.rs" + ], + "normalized_patch_sha256": "4f5750bc3118d0f23e701ba6da30cec7ec5571e760c8f1725e308de03642833d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "635a58f0302f9e396be5ef1c132dd12f55c1c0be", + "target_commit": "a3fe117d39a53bc3bd50c23ad55f6d9a6b97d94d", + "source_parents": [ + "6f7b6560575260691c06536ce368170f03fca446" + ], + "target_parents": [ + "1f7e35f073e761fde1e9cce4695b5cac92a5532e" + ], + "source_subject": "docs: clarify transport preview behavior", + "target_subject": "docs: clarify transport preview behavior", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T08:40:34Z", + "target_author_time": "2026-07-09T08:40:34Z", + "source_committer_time": "2026-07-09T08:40:34Z", + "target_committer_time": "2026-07-09T08:40:34Z", + "source_paths": [ + "crates/sdk/README" + ], + "normalized_patch_sha256": "d049c09686b82c4eaa07d56eb23da4faa32a12d70c716609bc9690cffd5b49a1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "63e9e99527575e0b11767965a6839e569b6a8bcf", + "target_commit": "81966cf2a5604c06da6e3227ec2141b4b7ca0da2", + "source_parents": [ + "db2c6ee7b88c8663f31ec0b86d5e0f2579583b24" + ], + "target_parents": [ + "322f91976abfeeded0dfa98808317b046296c998" + ], + "source_subject": "tests: harden agreement order export guards", + "target_subject": "tests: harden agreement order export guards", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T18:20:36-07:00", + "target_author_time": "2026-06-19T18:20:36-07:00", + "source_committer_time": "2026-06-19T18:20:36-07:00", + "target_committer_time": "2026-06-19T18:20:36-07:00", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "03ea88d7940ade3ab8be05c1701ca23f5efba78d921991e7cf9bf9e6b4d2b4b5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6435ca9c20de00ce8cf0c832068d90f3607412cf", + "target_commit": "89406244f80d695a8b0d7d24f8fdebdd164ad12f", + "source_parents": [ + "915bc14f3951f99ba853667690776068f991a6af" + ], + "target_parents": [ + "9a43937b5c956970c07155ed845a499c942574e0" + ], + "source_subject": "sdk: migrate transport profiles and policies", + "target_subject": "sdk: migrate transport profiles and policies", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:09:50Z", + "target_author_time": "2026-08-03T11:09:50Z", + "source_committer_time": "2026-08-03T11:09:50Z", + "target_committer_time": "2026-08-03T11:09:50Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "c6c1964cd7a35638a730edaa31b053510a05de2e20df5b14486aeeff0d49a7c1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "649bc24b2a8b34b73fad402f2d9eda68e71748e7", + "target_commit": "4e13bba7a95eb4cfa44106049315081e57db5bed", + "source_parents": [ + "8696f09e52aad7fd7594c553c165dfa4d40c221f" + ], + "target_parents": [ + "62008c4e8ddfd188d00601637025f2a8e42f973c" + ], + "source_subject": "sdk: document product runtime completion", + "target_subject": "sdk: document product runtime completion", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T22:43:00-07:00", + "target_author_time": "2026-06-15T22:43:00-07:00", + "source_committer_time": "2026-06-15T22:43:00-07:00", + "target_committer_time": "2026-06-15T22:43:00-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs" + ], + "normalized_patch_sha256": "5a0b7ec85fa0e1924b9eac00e82ee9b47a5c9b9f415827e7ffb007cb10fc5580", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "650fc968e4a9ba68002c85470ef5fd33ca605121", + "target_commit": "a2f17aa6fcd58945c022a409f50c2ea27b2fcd3b", + "source_parents": [ + "6e2f0066a06ff9ae7eab75eeb47cb3191cca3d39" + ], + "target_parents": [ + "16613dec20b2f9174cfd1a3a265107836f496a67" + ], + "source_subject": "sdk: narrow protocol adapter surfaces", + "target_subject": "sdk: narrow protocol adapter surfaces", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T17:30:25-07:00", + "target_author_time": "2026-06-15T17:30:25-07:00", + "source_committer_time": "2026-06-15T17:30:25-07:00", + "target_committer_time": "2026-06-15T17:30:25-07:00", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/profile.rs", + "crates/sdk/src/protocol.rs", + "crates/sdk/src/protocol/events.rs", + "crates/sdk/src/protocol/farm.rs", + "crates/sdk/src/protocol/identity.rs", + "crates/sdk/src/protocol/listing.rs", + "crates/sdk/src/protocol/mod.rs", + "crates/sdk/src/protocol/order.rs", + "crates/sdk/src/protocol/profile.rs", + "crates/sdk/src/protocol/wire.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/config.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/radrootsd.rs", + "crates/sdk/tests/relay_direct.rs", + "crates/sdk/tests/replica_ingest.rs" + ], + "normalized_patch_sha256": "3d94e5dc450e3bace70d582958a3b857ebe53186b17e3e991214ac7841c1d3e2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "65263961fe1471d6516af1848499bd95d82cf0b0", + "target_commit": null, + "source_parents": [ + "06dd4197888e88677817773173b7104e81410a88" + ], + "target_parents": [], + "source_subject": "facade: migrate workspace consumers", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-08-03T13:01:11Z", + "target_author_time": null, + "source_committer_time": "2026-08-03T13:01:11Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "6541c64d7db10ad1f1f8412310f16b82761ae416", + "target_commit": "2e0ee90383bc42fc5d7b17e0c427b77db455f18d", + "source_parents": [ + "d68b9c254e4ac38411cc999286e622a398ac5bbe" + ], + "target_parents": [ + "d341186d0a819c2ed8d1a5e15985ea1eb63914ec" + ], + "source_subject": "sdk: align local relay env keys", + "target_subject": "sdk: align local relay env keys", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-14T15:07:02-07:00", + "target_author_time": "2026-06-14T15:07:02-07:00", + "source_committer_time": "2026-06-14T15:07:02-07:00", + "target_committer_time": "2026-06-14T15:07:02-07:00", + "source_paths": [ + "crates/sdk/src/config.rs", + "crates/sdk/tests/config.rs" + ], + "normalized_patch_sha256": "fac56e6c8ae970dfe0e7546ea822cfbb86377e5fb2565e9ca4b0fc69fbcb5ae4", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "68cff3f0569132f4734339e0246e86a5b2d7175f", + "target_commit": "b42e6c6fe414c7ad15435c7c19e590f37a690b2d", + "source_parents": [ + "635a58f0302f9e396be5ef1c132dd12f55c1c0be" + ], + "target_parents": [ + "a3fe117d39a53bc3bd50c23ad55f6d9a6b97d94d" + ], + "source_subject": "transport: align SDK profile and status contracts", + "target_subject": "transport: align SDK profile and status contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T17:08:06Z", + "target_author_time": "2026-07-09T17:08:06Z", + "source_committer_time": "2026-07-09T17:08:06Z", + "target_committer_time": "2026-07-09T17:08:06Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "ded0f861ea228ed9ac48580124932ed6125cb11e07e09141842402bfbcef70a0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6985c00f4f460f2c29225b7053cef0b6892aea3b", + "target_commit": null, + "source_parents": [ + "117b1a9a167f9935148aa430b72d1474c0389953" + ], + "target_parents": [], + "source_subject": "chore(sdk): bootstrap repository metadata", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:03:30-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:03:30-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "6a196b1ff23563d2b58662ed091f85208a995307", + "target_commit": "396b9ed397d2bd85935f04fc01f2348c931166f3", + "source_parents": [ + "a752e21babfaf382383b5e1f3b7005d7890d64a0" + ], + "target_parents": [ + "235a396133ca0c5adde648396c8ed983b0d1185f" + ], + "source_subject": "ios: regenerate sdk ffi bindings", + "target_subject": "ios: regenerate sdk ffi bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:45:00Z", + "target_author_time": "2026-08-03T13:45:00Z", + "source_committer_time": "2026-08-03T13:45:00Z", + "target_committer_time": "2026-08-03T13:45:00Z", + "source_paths": [ + "crates/ffi/Cargo.toml", + "crates/ffi/src/bin/bindgen.rs", + "tools/xtask/src/bindings.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "0c56c9a03aba01c00074e5f80fb8f22374a8e7f416e2faf3885519ffc98896ad", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6a362d320382a1432891a2c430a26129c37b45da", + "target_commit": "b3094a4922f880ddc75d50e558ccf1f15632549b", + "source_parents": [ + "2bb507c1cfce162e8d32f260cb6cf5254d6bd4c0" + ], + "target_parents": [ + "6fefb9dea42a970c106ad325ce040d122f355755" + ], + "source_subject": "wasm: move sql runtime into sdk", + "target_subject": "wasm: move sql runtime into sdk", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-22T01:06:52Z", + "target_author_time": "2026-06-22T01:06:52Z", + "source_committer_time": "2026-06-22T01:06:52Z", + "target_committer_time": "2026-06-22T01:06:52Z", + "source_paths": [ + "crates/replica_sync_wasm/Cargo.toml", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sql_wasm_runtime/Cargo.toml", + "crates/sql_wasm_runtime/src/lib.rs" + ], + "normalized_patch_sha256": "095717ac4009005aa6fdb8226e810079cd0305664a7bd791dbfbad071e12ed13", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6a4f55616684d0b595ef1e0479abfd8ab2152692", + "target_commit": "81fb655abd071eac6c6ebe2aa085cfa3eab04423", + "source_parents": [ + "b7cf74e494e703864fefd4a873b1177341c31721" + ], + "target_parents": [ + "633eaeabdb22538b6f52e8b6130381c7d0ead03c" + ], + "source_subject": "sdk: migrate workspace consumers", + "target_subject": "sdk: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:14:17Z", + "target_author_time": "2026-08-03T12:14:17Z", + "source_committer_time": "2026-08-03T12:14:17Z", + "target_committer_time": "2026-08-03T12:14:17Z", + "source_paths": [ + "tools/xtask/src/main.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "b6a2bb32b2460998142389549e0dbd3f417d3539deba431c67012fbc673c2014", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6ad6e010fcc5323078ddf0d7f04486e402498d81", + "target_commit": "eefd990d961244a742d126feb3b50279c920b18e", + "source_parents": [ + "fa617f1a61f01b264ec17a059c485fe7cfa99cba" + ], + "target_parents": [ + "82b7095680ad0d1c9cb9de0dfc68d8c6acd71799" + ], + "source_subject": "radrootsd: harden HTTP adapter coverage", + "target_subject": "radrootsd: harden HTTP adapter coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T02:25:28Z", + "target_author_time": "2026-06-23T02:25:28Z", + "source_committer_time": "2026-06-23T02:25:28Z", + "target_committer_time": "2026-06-23T02:25:28Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs" + ], + "normalized_patch_sha256": "bc686529d775bc07407b3497c3d136696afef53cb9678657ff8474665de19fe3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6ba234971f8f70f37fd165101f6d0fc9c0b232f1", + "target_commit": "3789cc607e3f2ad84f9cd1d03f4b1332f31152cd", + "source_parents": [ + "7bd866821394510e912384d08dbaeed4d465d9a8" + ], + "target_parents": [ + "506879327866c6428f8da8babbad228240896346" + ], + "source_subject": "sdk: enrich listing enqueue receipts", + "target_subject": "sdk: enrich listing enqueue receipts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T16:53:38-07:00", + "target_author_time": "2026-06-15T16:53:38-07:00", + "source_committer_time": "2026-06-15T16:53:38-07:00", + "target_committer_time": "2026-06-15T16:53:38-07:00", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/receipt.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "e23792de412f0ab4efbf813393a07b61c28ca34be6d828cf406203819c2777bf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6bf67b0b2b2ddbc56b07a77572ae7ea369784c92", + "target_commit": "7b7425fa32a47d0f959b5ae21b0e23f566a6ad13", + "source_parents": [ + "d2a0e99e3d1d9665952f489a2fdd82caf326d660" + ], + "target_parents": [ + "b44a2acce87b30a4bce191b7c579a2995fa6ef13" + ], + "source_subject": "sdk: align runtime with sealed event DTOs", + "target_subject": "sdk: align runtime with sealed event DTOs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-14T11:05:10Z", + "target_author_time": "2026-07-14T11:05:10Z", + "source_committer_time": "2026-07-14T11:05:10Z", + "target_committer_time": "2026-07-14T11:05:10Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/adapters_signing_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "72c66cba0d8270c8fbdc19f8791fde08137c88e9c5d41ef1d0ee4bc5d076a1f1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6c0f7c3fe57c6f3ad994848b65008c700d76198a", + "target_commit": "6b8b642e159273c34ada6dba7c46eb01edc21fb3", + "source_parents": [ + "39bffb5c94cdaebfdb380f3e1843b4c75dbda3f3" + ], + "target_parents": [ + "755dcfd6de8c94fd145d30b4d35f3125c8d93664" + ], + "source_subject": "release: apply sdk crate version authority", + "target_subject": "release: apply sdk crate version authority", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:49:35Z", + "target_author_time": "2026-07-28T18:49:35Z", + "source_committer_time": "2026-07-28T18:49:35Z", + "target_committer_time": "2026-07-28T18:49:35Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "crates/sql_wasm_runtime/Cargo.toml", + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "b1bc8846d13a48d25be7fc8b94a81caf3156503de23b508f605f8ca59478bc38", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6d68bdd73aca544d0d4cc88f785b38a22d4b85d6", + "target_commit": "c46b39be5ee7517f238ad8de29853fd3296eae3b", + "source_parents": [ + "a3781823586e4a61c49afa4fb0a8396dd309afe7" + ], + "target_parents": [ + "63f396706f8e32f8ae15e2f342d3fca3054e40c4" + ], + "source_subject": "release: qualify front-door public APIs", + "target_subject": "release: qualify front-door public APIs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T23:30:33Z", + "target_author_time": "2026-08-03T23:30:33Z", + "source_committer_time": "2026-08-03T23:30:33Z", + "target_committer_time": "2026-08-03T23:30:33Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/api_qualification.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "be33de519c8526ed985345a25566ce467da431fff82330c505a6b89984ad7b1d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6e2f0066a06ff9ae7eab75eeb47cb3191cca3d39", + "target_commit": "16613dec20b2f9174cfd1a3a265107836f496a67", + "source_parents": [ + "40501c57db240199ac750c62ff9f6485778d79ae" + ], + "target_parents": [ + "067c67150f18b47a4d18481814f5c3b43054f1f6" + ], + "source_subject": "sdk: add runtime error taxonomy", + "target_subject": "sdk: add runtime error taxonomy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T17:21:03-07:00", + "target_author_time": "2026-06-15T17:21:03-07:00", + "source_committer_time": "2026-06-15T17:21:03-07:00", + "target_committer_time": "2026-06-15T17:21:03-07:00", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/runtime_targets.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "9dfcc36b2caa5f649fd684286096ac187872ea9871e4faa371794031d7ee309e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6e3552ddca05e061bc4519b08e86edf1b069c77a", + "target_commit": "31197938097ed0790b4b551ad2ff430797947243", + "source_parents": [ + "569d2f2a8380f737177d55b1e8e106ec2451cea9" + ], + "target_parents": [ + "d8947e9b201efcf6cfd02d921578ebea81d04b88" + ], + "source_subject": "sdk: export listing publish operation kind", + "target_subject": "sdk: export listing publish operation kind", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T03:21:30-07:00", + "target_author_time": "2026-06-19T03:21:30-07:00", + "source_committer_time": "2026-06-19T03:21:30-07:00", + "target_committer_time": "2026-06-19T03:21:30-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "dad5c8f1cbdb287092690c6e167f9899ea73f7238a389de520ff04759d7078fa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6f16fa242bf1138d8e1d5210faa0438523cdd62c", + "target_commit": "e64e3db81cb82414fd851aa9a5afb13cbeef4e42", + "source_parents": [ + "0bd19a81a23ee794481e684555a9175f18deb753" + ], + "target_parents": [ + "e13ca0e9e5862ab81a83c49ea85be73c8c3b6790" + ], + "source_subject": "transport: guard proxy preview completion", + "target_subject": "transport: guard proxy preview completion", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T09:27:01Z", + "target_author_time": "2026-07-07T09:27:01Z", + "source_committer_time": "2026-07-07T09:27:01Z", + "target_committer_time": "2026-07-07T09:27:01Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "5e3d970690289007d9c5afcddfb7d356bab0fe10d89f5f915ca263e8d5995265", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "6f7b6560575260691c06536ce368170f03fca446", + "target_commit": "1f7e35f073e761fde1e9cce4695b5cac92a5532e", + "source_parents": [ + "cfcc7c99d3cbf6a215d34f7a04ebb4ae1bd3590e" + ], + "target_parents": [ + "16bd4f71f9111ac5f7cebc251e35d0529a06c958" + ], + "source_subject": "xtask: harden Reticulum preview release gates", + "target_subject": "xtask: harden Reticulum preview release gates", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T08:17:11Z", + "target_author_time": "2026-07-09T08:17:11Z", + "source_committer_time": "2026-07-09T08:17:11Z", + "target_committer_time": "2026-07-09T08:17:11Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "0db296ac145670d0f202257ef586369e8aec989d27be32af4f4247d487407f0b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "70014334c649b1b07bd7740be857daab6566e55d", + "target_commit": "2e911103ac9453c9735013228e158a633d6806f6", + "source_parents": [ + "a93c1e74cf94737d25046ac4f2212c05a4717ec6" + ], + "target_parents": [ + "fee409998f8f86e2fc415af74aa13db4b564e3eb" + ], + "source_subject": "sdk: harden runtime DTO posture", + "target_subject": "sdk: harden runtime DTO posture", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T17:45:51-07:00", + "target_author_time": "2026-06-17T17:45:51-07:00", + "source_committer_time": "2026-06-17T17:45:51-07:00", + "target_committer_time": "2026-06-17T17:45:51-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "ac68554edfece4228f654ced020c02aa5c50e673920e33fdcf0db7abfee75241", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7096217f5a5028dbfc2c1278955a689412811c9c", + "target_commit": "3774064b70f7559b19b5153ffbd3f26dd9975288", + "source_parents": [ + "f49393558a17c226bee0234138f0187258991601" + ], + "target_parents": [ + "caf57a3332451c3f1cf8e0ed360e51e86cada81e" + ], + "source_subject": "sdk: add restore dry-run preflight", + "target_subject": "sdk: add restore dry-run preflight", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T17:54:35-07:00", + "target_author_time": "2026-06-17T17:54:35-07:00", + "source_committer_time": "2026-06-17T17:54:35-07:00", + "target_committer_time": "2026-06-17T17:54:35-07:00", + "source_paths": [ + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "c89d54319e66533e643eaa6249547c1bbff8fd090ba8dcef2190cbe204238c7f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "71be029ec028696f118d8b9bfc0f4f80c39103b3", + "target_commit": "fc488d6410f06ee1166b950d88b062cb67828ccb", + "source_parents": [ + "649bc24b2a8b34b73fad402f2d9eda68e71748e7" + ], + "target_parents": [ + "4e13bba7a95eb4cfa44106049315081e57db5bed" + ], + "source_subject": "sdk: gate runtime exports", + "target_subject": "sdk: gate runtime exports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:10:22-07:00", + "target_author_time": "2026-06-16T00:10:22-07:00", + "source_committer_time": "2026-06-16T00:10:22-07:00", + "target_committer_time": "2026-06-16T00:10:22-07:00", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/config.rs", + "crates/sdk/src/lib.rs" + ], + "normalized_patch_sha256": "145b1be6fcf30fdc45a0f13c3bed2f1393c9bcb8997bd6227e23806e7da27b39", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "71d53d141f8d2dd68bf3c049a0e0d88873ab0fb3", + "target_commit": "0d83d8de960d66234fe3771d520a0924b5ae558d", + "source_parents": [ + "c2ae351210bb0e9b499922a9f234b86221125b45" + ], + "target_parents": [ + "c615d7d133a46be3bdfaa0405d1372d4f6a231da" + ], + "source_subject": "packages: gate npm pack payloads", + "target_subject": "packages: gate npm pack payloads", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T04:08:33Z", + "target_author_time": "2026-06-28T04:08:33Z", + "source_committer_time": "2026-06-28T04:08:33Z", + "target_committer_time": "2026-06-28T04:08:33Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "55d66afee6c0a36df77a06a4a07cb428aa032a98a244b22dd105b3924cb6569c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "71dc0de57ddc96754f291a6111e9e3dcc9e31234", + "target_commit": "c63dae0f1db19f4662f758f4f9bc7cd025af23d3", + "source_parents": [ + "8084b17eb3ca6113ca8e20121031684e6fbf3248" + ], + "target_parents": [ + "3bc687d567220d4ee133504ff3ebb008860f8c45" + ], + "source_subject": "runtime: add SDK journal recovery", + "target_subject": "runtime: add SDK journal recovery", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T11:37:52Z", + "target_author_time": "2026-07-16T11:37:52Z", + "source_committer_time": "2026-07-16T11:37:52Z", + "target_committer_time": "2026-07-16T11:37:52Z", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/market_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_storage.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/market_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "134ac44b097e4ac99d52706e77ca6c51df61183e5003a4f816084d78bf12cb8d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "729bfba53ef37bf784ffa82fdb660797f2645c33", + "target_commit": null, + "source_parents": [ + "5217d737737ba270b7bf26a4f5e426d5c985d8d3" + ], + "target_parents": [], + "source_subject": "release: baseline final public APIs", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-08-04T16:19:00Z", + "target_author_time": null, + "source_committer_time": "2026-08-04T16:19:00Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "735ecaa2d0d3a9ef4d294f3c41fdbdf3761b3c36", + "target_commit": "fd3eb7b29f78a4e45b10f2ae453e511ed4e5611c", + "source_parents": [ + "99e68e3d54ebd748f8f3fdae220bee63458a55b3" + ], + "target_parents": [ + "1b5c210a0d9189c5a632448bd3374e4ff871095f" + ], + "source_subject": "release: enforce front-door package metadata", + "target_subject": "release: enforce front-door package metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T22:13:03Z", + "target_author_time": "2026-08-03T22:13:03Z", + "source_committer_time": "2026-08-03T22:13:03Z", + "target_committer_time": "2026-08-03T22:13:03Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/LICENSE-APACHE", + "crates/radroots/LICENSE-MIT", + "crates/sdk/Cargo.toml", + "crates/sdk/LICENSE-APACHE", + "crates/sdk/LICENSE-MIT", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "cddeea67025cfcbfbe448bd8bdb1322f41d10ad4c396d774ec0b0f30ef45e7ae", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "748ca41cb2ad3462a5aab3310c6bd6e0901dbb68", + "target_commit": "c47ca80391953813005cb658dd6617640154c99b", + "source_parents": [ + "c178b9495109c530abea18e0d489ae2e57d3da7a" + ], + "target_parents": [ + "d581dedb72e8293bd34929c119590f1081327328" + ], + "source_subject": "sdk: clean default guard warnings", + "target_subject": "sdk: clean default guard warnings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T22:57:51Z", + "target_author_time": "2026-06-30T22:57:51Z", + "source_committer_time": "2026-06-30T22:57:51Z", + "target_committer_time": "2026-06-30T22:57:51Z", + "source_paths": [ + "crates/sdk/src/farm.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/order.rs" + ], + "normalized_patch_sha256": "288f671f645b6c2902745c1f010619c04ba52121d1fe5695c4c36547ff375240", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "755fab472df86ad5f45e398fe0dd590ec23dd0e4", + "target_commit": "2d243420eeb295ecd770446ec472c58d9df62929", + "source_parents": [ + "208e02d55f433fabe4f46320a577104929c20db2" + ], + "target_parents": [ + "9656841cfa620eca297778e5414475ab50643edc" + ], + "source_subject": "feat: expose sdk identity module", + "target_subject": "feat: expose sdk identity module", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T11:07:49Z", + "target_author_time": "2026-06-30T11:07:49Z", + "source_committer_time": "2026-06-30T11:07:49Z", + "target_committer_time": "2026-06-30T11:07:49Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/tests/identity_public_api.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "247842933095a2ee5ff77c48a6309b260df47c2111468347d5b66f4bf43b3624", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "75c719bc0f79ae12a3e4a13a56d5a8943985c8d5", + "target_commit": "52095e03b168c694223be519fa76417590ca7ac5", + "source_parents": [ + "235f088795e2849f1a99574f5ca68c22d75b8d0b" + ], + "target_parents": [ + "646e3c75b633b03aac6d1828632dd1719ed3b8fc" + ], + "source_subject": "build(package): retain sdk crate tests", + "target_subject": "build(package): retain sdk crate tests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T11:42:08Z", + "target_author_time": "2026-08-04T11:42:08Z", + "source_committer_time": "2026-08-04T11:42:08Z", + "target_committer_time": "2026-08-04T11:42:08Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/sdk/Cargo.toml", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "086f0f3b16b7e9c15f753606a6d6438320adf24a65b2db04fd1ad0637bcc0e3d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "762ca4d6270bda5855adf5d22e00abb5411ee456", + "target_commit": "bface7702c4c08216bcca1dadd689dfb94a2dca3", + "source_parents": [ + "cd32f1cb5160e54d77bd296966e4c1050a24cf11" + ], + "target_parents": [ + "6803dac83bfeb6b528ef07f7d156643ab0084b95" + ], + "source_subject": "tests: guard daemon transport publish surface", + "target_subject": "tests: guard daemon transport publish surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T04:29:10Z", + "target_author_time": "2026-07-07T04:29:10Z", + "source_committer_time": "2026-07-07T04:29:10Z", + "target_committer_time": "2026-07-07T04:29:10Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "89d9aff89b01a1e72c1c480cd7a71e573626e492700235889979c0f554957683", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "762f56fd0a11769ed50cdc86c8f79f66f9bd5b09", + "target_commit": "0fb42cc9b6d618a4ac1e6b563501d10c4d4a76f7", + "source_parents": [ + "355a7807b9b38afcff7a47750ab09db222310df4" + ], + "target_parents": [ + "2e602c9c268729a5c82fcaaf85fee5112b46efdb" + ], + "source_subject": "facade: write primary getting-started documentation", + "target_subject": "facade: write primary getting-started documentation", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:49:45Z", + "target_author_time": "2026-08-03T12:49:45Z", + "source_committer_time": "2026-08-03T12:49:45Z", + "target_committer_time": "2026-08-03T12:49:45Z", + "source_paths": [ + "crates/radroots/README.md", + "crates/radroots/src/lib.rs" + ], + "normalized_patch_sha256": "5729a0837866a4d3a3e4350968c7436886ccdf0fdc8efa00fe5cf7247be38eb2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "76fe02a613a024605e2a89e9c3240d271d2d1394", + "target_commit": "c774e8e1ed68e7232177e464efc65770e477ba46", + "source_parents": [ + "0d21ded103d6bccf8087fc27b824d2cc758c4372" + ], + "target_parents": [ + "dc41a3b3cd84b5da3b1ba8c2f94550b51e7c51b4" + ], + "source_subject": "sdk: guard proxy required target semantics", + "target_subject": "sdk: guard proxy required target semantics", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T10:13:30Z", + "target_author_time": "2026-07-10T10:13:30Z", + "source_committer_time": "2026-07-10T10:13:30Z", + "target_committer_time": "2026-07-10T10:13:30Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "e43e93ce53b8a7e233fff864d94971c17e26d80d5fdff4576768cdd0b60e6e5e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7933e55ce77d9207fb4199047ae9ec736160dacd", + "target_commit": "053d1ddaea2f4d90c484593ccb0952e4025314dd", + "source_parents": [ + "c2df5e5aea253771cf9b00e2f29d55e22099f94b" + ], + "target_parents": [ + "ff49f82f8fbbffe7d1ec456693fe1f2320bdd628" + ], + "source_subject": "dto: guard indexed generated bindings", + "target_subject": "dto: guard indexed generated bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T08:21:15Z", + "target_author_time": "2026-06-24T08:21:15Z", + "source_committer_time": "2026-06-24T08:21:15Z", + "target_committer_time": "2026-06-24T08:21:15Z", + "source_paths": [ + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "1343189c9859f4705a3c1f74306b3be511fc0dbe521a754948b8b3078d2ef93f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7934096d1848692a0485e2c08ceb9dc567f004f0", + "target_commit": "02ecef9424453f4aa274ec33ac8d7ee3856be26e", + "source_parents": [ + "97fdfe0efa84fb2cb5a9bd0984c978550b133be7" + ], + "target_parents": [ + "165f130dd079d59d9c749275c32328e44ec14829" + ], + "source_subject": "core: migrate workspace consumers", + "target_subject": "core: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T16:38:42Z", + "target_author_time": "2026-07-27T16:38:42Z", + "source_committer_time": "2026-07-27T16:38:42Z", + "target_committer_time": "2026-07-27T16:38:42Z", + "source_paths": [ + "crates/core_bindings/Cargo.toml", + "crates/core_bindings/src/dto.rs", + "crates/core_bindings/src/lib.rs", + "crates/event_codec_wasm/src/lib.rs", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "crates/trade_bindings/src/dto.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "4fc1ae4da034a1c923c4b47a1b488f813b14147cbb3a6cbe8afaa0d06da7d3c8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "79f1018994c344f59a9b342cd43493b62d3c771f", + "target_commit": "7b48b68af0eaa118340c4e1ef128e4bc2972f06b", + "source_parents": [ + "862c78d5d63b2282dac0e8eb75b8971e5b11d585" + ], + "target_parents": [ + "ffe16540eed12831ee3b62b8a8f1abd6ff2f8682" + ], + "source_subject": "sdk: refactor listing operations", + "target_subject": "sdk: refactor listing operations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:31:43Z", + "target_author_time": "2026-08-03T11:31:43Z", + "source_committer_time": "2026-08-03T11:31:43Z", + "target_committer_time": "2026-08-03T11:31:43Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs" + ], + "normalized_patch_sha256": "1b133275c6d04f028dc196a784bab136a9b2c98f220f402a42ebdd7653aed961", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7a2a8d817d8c7ac0038aa46957c1d4973ed5e973", + "target_commit": "68aa381171e6184587537c13c6959127cd139e3d", + "source_parents": [ + "84699dbf840a054dd4cc19e291b4b28bffa7d7e7" + ], + "target_parents": [ + "dc4f3bf20089bf49d86bab378fdd31a22f3f8484" + ], + "source_subject": "bindings: use protocol as dto authority", + "target_subject": "bindings: use protocol as dto authority", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:13:47Z", + "target_author_time": "2026-08-03T13:13:47Z", + "source_committer_time": "2026-08-03T13:13:47Z", + "target_committer_time": "2026-08-03T13:13:47Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/cli_host.rs", + "tools/xtask/src/contracts.rs" + ], + "normalized_patch_sha256": "f1c0cb982cf5c22b421517639affb4b29d1caa9e8e9835a2c1603b53694275f6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7ad62f8c3952f2a330e74234306d446ec0a9cc0d", + "target_commit": "9650fb7e4c03986ed44e6002779157244bb1f952", + "source_parents": [ + "30a23c4a1e0c95ffb507f57aac6a5387ac97e5d1" + ], + "target_parents": [ + "1558ea5a5c916729ce59fcdb0a709ce397172fc8" + ], + "source_subject": "sdk: extract private workflow enqueue helper", + "target_subject": "sdk: extract private workflow enqueue helper", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T20:16:23-07:00", + "target_author_time": "2026-06-18T20:16:23-07:00", + "source_committer_time": "2026-06-18T20:16:23-07:00", + "target_committer_time": "2026-06-18T20:16:23-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/workflow_runtime.rs" + ], + "normalized_patch_sha256": "32cce1afe89d6370d1a5770c07c9539850232c1834aeb0aaf0bcd4a6b6ea22dd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7b21fd126a677eeabe3dc7b2d5901d9946465101", + "target_commit": "e44f66b6fd9bf04b93d0269b669ba634e0ea8d1f", + "source_parents": [ + "2338e109ef2ffa527f3ac4219817071acbdfbf14" + ], + "target_parents": [ + "93ca97a75b53d83a4c4fcc8d23e7b5437d5dbab5" + ], + "source_subject": "sdk: align runtime contract trade operations", + "target_subject": "sdk: align runtime contract trade operations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T13:52:31Z", + "target_author_time": "2026-07-16T13:52:31Z", + "source_committer_time": "2026-07-16T13:52:31Z", + "target_committer_time": "2026-07-16T13:52:31Z", + "source_paths": [ + "tools/xtask/src/cli_host.rs" + ], + "normalized_patch_sha256": "ccb8a6026179a19b64a083972a2d55802df6d777bcf6d47ff23258745e7a9f38", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7b3e3ba1595879766f8037a689f20b552a9c999a", + "target_commit": "05530a10ed58910f9309e1560820c9091edf300e", + "source_parents": [ + "cfa30b931ff01d8da17d429e215c4151c4224ab1" + ], + "target_parents": [ + "a4c9f542d50475f1d0f290214e29f68d3aa75e1f" + ], + "source_subject": "sdk: expose validation trust receipt fields", + "target_subject": "sdk: expose validation trust receipt fields", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T07:33:27Z", + "target_author_time": "2026-07-01T07:33:27Z", + "source_committer_time": "2026-07-01T07:33:27Z", + "target_committer_time": "2026-07-01T07:33:27Z", + "source_paths": [ + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/dvm_runtime.rs" + ], + "normalized_patch_sha256": "aa83ee6b08f889ab9bcfc3e94ddc2960e54e216c75ea6093e900a7d4b7a89135", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7bd866821394510e912384d08dbaeed4d465d9a8", + "target_commit": "506879327866c6428f8da8babbad228240896346", + "source_parents": [ + "407cfcca4b418d251d0e6c12f19a69cb1e9a41bc" + ], + "target_parents": [ + "4f95496a65a1a258b26faf827e15920c023826eb" + ], + "source_subject": "sdk: split listing publish runtime requests", + "target_subject": "sdk: split listing publish runtime requests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T16:48:45-07:00", + "target_author_time": "2026-06-15T16:48:45-07:00", + "source_committer_time": "2026-06-15T16:48:45-07:00", + "target_committer_time": "2026-06-15T16:48:45-07:00", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/runtime_targets.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "c85d64ecdec28d597a71caa2e1b9c226d479dd29cc9d761ce4efff01ac8e8dbc", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7c0177cd5b2261e2e5bea054907aa6147d52aae7", + "target_commit": "f85a961d2ddd0338a6fff0a1e65fa2e86d978f5d", + "source_parents": [ + "2452accade2358dc65a38fb22f654d2eab60fd2e" + ], + "target_parents": [ + "b092782e95485fccb5a9d4c0b07dfa832e019ded" + ], + "source_subject": "facade: align package manifest and module root", + "target_subject": "facade: align package manifest and module root", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T08:05:57Z", + "target_author_time": "2026-07-27T08:05:57Z", + "source_committer_time": "2026-07-27T08:05:57Z", + "target_committer_time": "2026-07-27T08:05:57Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/README.md", + "crates/radroots/src/client.rs", + "crates/radroots/src/event.rs", + "crates/radroots/src/farm.rs", + "crates/radroots/src/identity.rs", + "crates/radroots/src/knowledge.rs", + "crates/radroots/src/lib.rs", + "crates/radroots/src/listing.rs", + "crates/radroots/src/signing.rs", + "crates/radroots/src/storage.rs", + "crates/radroots/src/sync.rs", + "crates/radroots/src/trade.rs", + "crates/radroots/src/transport.rs", + "crates/radroots/tests/public_surface.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "8b6325d4f334827036c9e4d9a02e52a18cf93919ac16c9937b1539602de7a980", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7cabf6f3f13679e81e036495cef75ef0d411cd92", + "target_commit": "fa6802ef6800a3e6cddda266b71fa48598ca3e16", + "source_parents": [ + "7d4b0d701167691ee8ea04d436acb0d2565ae347" + ], + "target_parents": [ + "ded5fc00f3b24b232682e290c026fb0837360cd4" + ], + "source_subject": "sdk: promote GeoNames client surface", + "target_subject": "sdk: promote GeoNames client surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T08:37:20Z", + "target_author_time": "2026-06-26T08:37:20Z", + "source_committer_time": "2026-06-26T08:37:20Z", + "target_committer_time": "2026-06-26T08:37:20Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/geonames.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/geonames.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "6a9b81564b6acf466f9253aaf3b0a1cc545e8623c235974d2f5d23240c5eb47d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7cd3ebacf5cb7a90b69f2606e547dea4b839277e", + "target_commit": "8138843e85b8231a2034449cc230c4e2d0b69004", + "source_parents": [ + "e0bb1be02dbc7c3016a453a3a45aeb8c9ed7f3ab" + ], + "target_parents": [ + "8d18e6a29ad8050183a3f46b5184f3ce07f35c34" + ], + "source_subject": "sdk: add order status runtime", + "target_subject": "sdk: add order status runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T14:20:49-07:00", + "target_author_time": "2026-06-15T14:20:49-07:00", + "source_committer_time": "2026-06-15T14:20:49-07:00", + "target_committer_time": "2026-06-15T14:20:49-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "4946005501a766e2a42cd7d280f5716ec893da997a8c4f956da1443e58fa9ad4", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7d4b0d701167691ee8ea04d436acb0d2565ae347", + "target_commit": "ded5fc00f3b24b232682e290c026fb0837360cd4", + "source_parents": [ + "0bcdc82efff93198de3fd8f53c9a2acd75a2148c" + ], + "target_parents": [ + "c2e68973a4e2027d891cff605cf08095186f66fb" + ], + "source_subject": "sdk: expose RadrootsClient trade facade", + "target_subject": "sdk: expose RadrootsClient trade facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T08:23:13Z", + "target_author_time": "2026-06-26T08:23:13Z", + "source_committer_time": "2026-06-26T08:23:13Z", + "target_committer_time": "2026-06-26T08:23:13Z", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "48fd315d29fc9ba8deb80f346d91f14ff8568616aa41778836e229538bbdc0c6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7d94a98261a1507a581c5ed6da1920f5b45e6dd2", + "target_commit": "7faa57a39a16459e6bcd598d63ce900a414aa1bb", + "source_parents": [ + "edf40a100590de728fe9ee5677b37d29f61bc40c" + ], + "target_parents": [ + "db352eac57658b672c6f2920693eaa92c7436ed2" + ], + "source_subject": "crate-surface: rename SDK binding packages", + "target_subject": "crate-surface: rename SDK binding packages", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-11T23:56:26Z", + "target_author_time": "2026-07-11T23:56:26Z", + "source_committer_time": "2026-07-11T23:56:26Z", + "target_committer_time": "2026-07-11T23:56:26Z", + "source_paths": [ + "crates/event_bindings/Cargo.toml", + "crates/event_bindings/src/lib.rs", + "crates/event_bindings/src/model.rs", + "crates/event_codec_wasm/Cargo.toml", + "crates/event_codec_wasm/README", + "crates/event_codec_wasm/src/lib.rs", + "crates/replica_schema_bindings/Cargo.toml", + "crates/replica_schema_bindings/src/lib.rs", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_store_wasm/README", + "crates/replica_store_wasm/src/lib.rs", + "crates/replica_store_wasm/src/snapshot.rs", + "crates/replica_store_wasm/src/utils.rs", + "crates/replica_store_wasm/src/wasm_impl.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs", + "tools/xtask/src/wasm.rs", + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "bcb33b16011dcd88e7fd2c5ba3495d18a2e5b922059fc8af0f644c813320c4cd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7e40c2fadd54c1e0edbe2523980b34b07f4ef10a", + "target_commit": "26a3cb8e3af725fe5953a62882efecfc4d0d0e21", + "source_parents": [ + "0ac6929bf2882374f75a8eb737f3a9252a2fe9f8" + ], + "target_parents": [ + "21ddb9b338d217a868ff4ca64624506b740e4b1f" + ], + "source_subject": "tests: guard passive payment SDK surface", + "target_subject": "tests: guard passive payment SDK surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T16:22:56-07:00", + "target_author_time": "2026-06-19T16:22:56-07:00", + "source_committer_time": "2026-06-19T16:22:56-07:00", + "target_committer_time": "2026-06-19T16:22:56-07:00", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "24dc50ec12dae425c7ab8aa81071a07036f234f71b24b5bf8f06002c9547eabe", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7ed8d3580bd7756de3145c384d5790bf6645cceb", + "target_commit": "b014dbfc4a96149d5e027deefe3655f780e401d0", + "source_parents": [ + "4ea65f211b0e3246f2563d3af1683b461ed4ce6b" + ], + "target_parents": [ + "958b9f6c1c056fe4a8a73ad80b3da69d0f50fda8" + ], + "source_subject": "workflow: preserve no-wait delivery semantics", + "target_subject": "workflow: preserve no-wait delivery semantics", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T04:31:50Z", + "target_author_time": "2026-07-09T04:31:50Z", + "source_committer_time": "2026-07-09T04:31:50Z", + "target_committer_time": "2026-07-09T04:31:50Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "537bc3dceeae0f01a48bbbdcdb6a6c350ed977ea5dcd7d6be7f9bb1b653366ee", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7faefa3a24709ee9662aaa66d5c10d6409cb109e", + "target_commit": "04e95e941187000765ae01ced4eda1c048bbca33", + "source_parents": [ + "da79c7bbec2df72ecb255c525f32d131b0370231" + ], + "target_parents": [ + "071a3cdf1f1a2d2903c965c8adfd7f848ccc217b" + ], + "source_subject": "sync: scope proxy idempotency to attempts", + "target_subject": "sync: scope proxy idempotency to attempts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T21:06:02Z", + "target_author_time": "2026-06-23T21:06:02Z", + "source_committer_time": "2026-06-23T21:06:02Z", + "target_committer_time": "2026-06-23T21:06:02Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "022c0525c1a0e45526a1f0ba3635e2eaaaeafeca28cf7cc4d3a6b83634683c8b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "7ffe6708630f37f8cfc7ece959b4c468e524daeb", + "target_commit": "a05eefdd45d08fcf15564813b8d734d80575cc87", + "source_parents": [ + "3beba951bcc7dae327e4367ca1468448c4787696" + ], + "target_parents": [ + "eefc32cc7f76cd761e58b06fe3147c6fd9e298da" + ], + "source_subject": "packages: align npm publish payloads", + "target_subject": "packages: align npm publish payloads", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T02:26:24Z", + "target_author_time": "2026-06-28T02:26:24Z", + "source_committer_time": "2026-06-28T02:26:24Z", + "target_committer_time": "2026-06-28T02:26:24Z", + "source_paths": [ + "tools/xtask/src/check.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/manifest.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "6f37d716ffc053a2e79d0fe7165af0c06e36c8f5bd52f6f4df1fbe6407afaa78", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "805bda41e9075de343943e280b9233ee7462e45c", + "target_commit": "59b737cea14c03552f20a3cd49873906249f5c5d", + "source_parents": [ + "ea2cba9e56423390f4d3cf7bfa3a1195963f322f" + ], + "target_parents": [ + "6195da3c4952f8a2476ee8e358f4469ca245a15b" + ], + "source_subject": "wasm: enforce validated event envelopes", + "target_subject": "wasm: enforce validated event envelopes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-18T12:02:31Z", + "target_author_time": "2026-07-18T12:02:31Z", + "source_committer_time": "2026-07-18T12:02:31Z", + "target_committer_time": "2026-07-18T12:02:31Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs" + ], + "normalized_patch_sha256": "fa8d00729d47d5f5cd4f743f91e66aae27fcec4ef5e57f1c5f153c364db399a3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8084b17eb3ca6113ca8e20121031684e6fbf3248", + "target_commit": "3bc687d567220d4ee133504ff3ebb008860f8c45", + "source_parents": [ + "553c30f2dcc4f9e8dd5e751260705a3b6f4c4620" + ], + "target_parents": [ + "00b6b5e3859930989deb29c3c092f3aa544e73e0" + ], + "source_subject": "build: close SDK warning hygiene", + "target_subject": "build: close SDK warning hygiene", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T22:38:02Z", + "target_author_time": "2026-07-15T22:38:02Z", + "source_committer_time": "2026-07-15T22:38:02Z", + "target_committer_time": "2026-07-15T22:38:02Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/sql_wasm_runtime/src/lib.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/coverage.rs", + "tools/xtask/src/coverage_policy_tests.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/fs.rs" + ], + "normalized_patch_sha256": "3532204de8a5c13384493ae0385f113451a291151fcf057312ed7d18d4e56c72", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "80af9cd1f3559dc8a71ec92518eed69337c68d32", + "target_commit": "de9ab46d7bed496b473ccd136d34b6f705def3a7", + "source_parents": [ + "3318ff5613a8d865fa73878d3c98570354daec27" + ], + "target_parents": [ + "69ffeecec1a44a4a2c08bed407fa36c9a9bfc516" + ], + "source_subject": "sdk: add runtime contract DTOs", + "target_subject": "sdk: add runtime contract DTOs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T13:47:39-07:00", + "target_author_time": "2026-06-17T13:47:39-07:00", + "source_committer_time": "2026-06-17T13:47:39-07:00", + "target_committer_time": "2026-06-17T13:47:39-07:00", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "6a4400fe74fe10c636757dac378492bea0edf93c12f8603aaa689f46a027eb2a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "80b30f33daab6e32a07edcd4bbc856d1778cff38", + "target_commit": "e07290ddb2458f047f4e86041ee01fddc7a62e79", + "source_parents": [ + "bd39b52795651ade71acb7e71f9740cb02c32627" + ], + "target_parents": [ + "8adbade4c805ecd326aa5e2ebcd9a171eddf8b19" + ], + "source_subject": "workflow: add explicit trade mutation evidence modes", + "target_subject": "workflow: add explicit trade mutation evidence modes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-04T00:10:43Z", + "target_author_time": "2026-07-04T00:10:43Z", + "source_committer_time": "2026-07-04T00:10:43Z", + "target_committer_time": "2026-07-04T00:10:43Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "feb64b859ebc4a6292743df465cc38684b28e8f5fd51d14e0c5f4e2ecf3521f8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8274e3f41691db7a35678d20c02ba45878bf140b", + "target_commit": null, + "source_parents": [ + "9f07080df63726521953d0456c3e5fb05ccc3242" + ], + "target_parents": [], + "source_subject": "test: align integration coverage", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T14:19:26-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T14:19:26-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "82a6137848fa527a4c8504a35801cefa52063bb7", + "target_commit": null, + "source_parents": [ + "c2e9bcc528248d541365978c3668ed2167062d8f" + ], + "target_parents": [], + "source_subject": "sdk: refresh qualified trade dependency", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-30T08:52:04Z", + "target_author_time": null, + "source_committer_time": "2026-07-30T08:52:04Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "82ad5c39345238c1e259ac0b8c8a76c349eea3e8", + "target_commit": "4c33070bfc26e31db05cd0ae0bd058ba02755dbe", + "source_parents": [ + "e87eb80d9f256d2afd58f5105c29f4b3f6cd11a3" + ], + "target_parents": [ + "b46e2a378ae90c25a0a6525d2c10ee0a6ab225ca" + ], + "source_subject": "transport: specialize Reticulum preview push errors", + "target_subject": "transport: specialize Reticulum preview push errors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T06:11:12Z", + "target_author_time": "2026-07-07T06:11:12Z", + "source_committer_time": "2026-07-07T06:11:12Z", + "target_committer_time": "2026-07-07T06:11:12Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "43cdd8260c51984ed0323d018ca3496c1fa16bfcfa56b765fcb75dc564c79bdb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "831f1eaf21ddbfd08c90e82fd4a4d4b9e0b46ba6", + "target_commit": "977c73f1054249ee28eb6cf4fe961ce2f1c05313", + "source_parents": [ + "f8a33b695a9234ffd0c6bd57b5ba0102f3d3819f" + ], + "target_parents": [ + "74bb6888161dc69919d1d58d4c86e5a0fa324b5e" + ], + "source_subject": "release-product: harden backup restore manifests", + "target_subject": "release-product: harden backup restore manifests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T21:39:02Z", + "target_author_time": "2026-07-16T21:39:02Z", + "source_committer_time": "2026-07-16T21:39:02Z", + "target_committer_time": "2026-07-16T21:39:02Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "9f2f023172c8b9af2e9a3b8aa09137316c1f685c31171bcae8ccf1cbc96e7d94", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8351f09962ab657d95b04f4d2e3ad3fe963a10d2", + "target_commit": "bba373365eadd4cbc4ef1e509f84200b00f5393d", + "source_parents": [ + "de1642d7f6025307b426dd3f8d9c8cc23f4f5f98" + ], + "target_parents": [ + "1d113e4a184bb0a3ea69cf8d1f874709c1b2816f" + ], + "source_subject": "trade: render bindings from dto registry", + "target_subject": "trade: render bindings from dto registry", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T08:42:43Z", + "target_author_time": "2026-06-24T08:42:43Z", + "source_committer_time": "2026-06-24T08:42:43Z", + "target_committer_time": "2026-06-24T08:42:43Z", + "source_paths": [ + "crates/trade_bindings/Cargo.toml", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "crates/trade_bindings/src/model.rs", + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "6b2f42c64a7afe92959221464edc7b6838bc41e6f7a537b14b4324c675aaf400", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "83d18a76dccdbf4f66536fa5c1eb051bac2b0391", + "target_commit": "5d623f6498b57923b7e4ee64c8b7e7e4e4abbe10", + "source_parents": [ + "3c4398f0fa519dd5611859f67a42c2acd0e6bdad" + ], + "target_parents": [ + "c9ea54fa108d5f86db3e02973286a78945dc58bb" + ], + "source_subject": "sync: recover expired outbox claims before push", + "target_subject": "sync: recover expired outbox claims before push", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T23:21:20Z", + "target_author_time": "2026-07-07T23:21:20Z", + "source_committer_time": "2026-07-07T23:21:20Z", + "target_committer_time": "2026-07-07T23:21:20Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "5144b484084c37468f4da4c9e3aec7855d2f05b5526cc155d5161e1b8387729c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "84699dbf840a054dd4cc19e291b4b28bffa7d7e7", + "target_commit": null, + "source_parents": [ + "f4478a4fb2a758bf0862dde64aea529de3e98471" + ], + "target_parents": [], + "source_subject": "facade: quarantine superseded package surface", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-08-03T13:05:48Z", + "target_author_time": null, + "source_committer_time": "2026-08-03T13:05:48Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "84ec9114bc42376f0fef5b2907b86440a9323ca5", + "target_commit": "3a10e0bbfccf5853deafb49e240d93b04f578920", + "source_parents": [ + "510269bfb468fab87117686ae23ec1ad8d14732a" + ], + "target_parents": [ + "c13137f987420fa4944a08b40663271fd2d602c3" + ], + "source_subject": "facade: scaffold the curated façade", + "target_subject": "facade: scaffold the curated façade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:33:54Z", + "target_author_time": "2026-08-03T12:33:54Z", + "source_committer_time": "2026-08-03T12:33:54Z", + "target_committer_time": "2026-08-03T12:33:54Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/src/lib.rs", + "crates/radroots/tests/package_boundary.rs", + "crates/radroots/tests/public_surface.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "941831cd21d3a9d7c5d895aee7cc6f6a01270f611676cd819398de7be574b726", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "84f21199b51c16103d9898ceda3fd2267cafa6d2", + "target_commit": "a1762bdc8fbf0cf303ce0b840d4c3719f8996c49", + "source_parents": [ + "e00cb1c71f15bab2dbb782599fd2b38c60e6714c" + ], + "target_parents": [ + "841f81812e84790e7fcd6c84e089f86224c16217" + ], + "source_subject": "sdk: add order request evidence ingest", + "target_subject": "sdk: add order request evidence ingest", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T22:58:11-07:00", + "target_author_time": "2026-06-18T22:58:11-07:00", + "source_committer_time": "2026-06-18T22:58:11-07:00", + "target_committer_time": "2026-06-18T22:58:11-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "51273a2035475340c86658fa8df1072885f69b6eb2175dc49cfb9f392ab373f3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "862c78d5d63b2282dac0e8eb75b8971e5b11d585", + "target_commit": "ffe16540eed12831ee3b62b8a8f1abd6ff2f8682", + "source_parents": [ + "27c621961ecfd7fcd0c1aa3513d285596378ccd6" + ], + "target_parents": [ + "db9845f621613817827051d3d3d863597879d567" + ], + "source_subject": "sdk: refactor farm prepare and enqueue operations", + "target_subject": "sdk: refactor farm prepare and enqueue operations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:23:41Z", + "target_author_time": "2026-08-03T11:23:41Z", + "source_committer_time": "2026-08-03T11:23:41Z", + "target_committer_time": "2026-08-03T11:23:41Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/client.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs" + ], + "normalized_patch_sha256": "63a47d30208cb4826b6231db18a839fb687af341671f52f5507acc013b95c8e1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8696f09e52aad7fd7594c553c165dfa4d40c221f", + "target_commit": "62008c4e8ddfd188d00601637025f2a8e42f973c", + "source_parents": [ + "5df4470a539cd0438d791780e747e53af22c0ff8" + ], + "target_parents": [ + "ebf590c826aeb1d22276410f6662603c55f836ab" + ], + "source_subject": "sdk: add product api guard tests", + "target_subject": "sdk: add product api guard tests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T22:39:51-07:00", + "target_author_time": "2026-06-15T22:39:51-07:00", + "source_committer_time": "2026-06-15T22:39:51-07:00", + "target_committer_time": "2026-06-15T22:39:51-07:00", + "source_paths": [ + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "ec0f8ca40ecd7c4019572fe2b22f9e851bd3096e616d3c1345859ca8332996e5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "87cf4ff726d7eee0f11a5eef5a8206f4960b6956", + "target_commit": "70ebbfbfb612fd57dc99456f816c0c7d45dae969", + "source_parents": [ + "25d556cc18d69229df5d688b4062d9e30d0e9d2f" + ], + "target_parents": [ + "967c0bafd608036397f5919e2591db82f9cc0ed6" + ], + "source_subject": "trade: remove stale workflow bindings", + "target_subject": "trade: remove stale workflow bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-27T20:55:04Z", + "target_author_time": "2026-06-27T20:55:04Z", + "source_committer_time": "2026-06-27T20:55:04Z", + "target_committer_time": "2026-06-27T20:55:04Z", + "source_paths": [ + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "c1d175e2adeb79321f80392e7b1e45ae728cbfa2b10af69a3bfe33d858ef67bc", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8916816e395cd70db648e48536be4f8f53628fc6", + "target_commit": "2126dfb990d314dc8662bb48f548e2527d4a6d6d", + "source_parents": [ + "4f3dc3dff6d07980e0ad07970991137e833acf12" + ], + "target_parents": [ + "b5fe8b822840439f71c697ce1b57672777c711e0" + ], + "source_subject": "sdk: quiet no-default imports", + "target_subject": "sdk: quiet no-default imports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T13:20:35-07:00", + "target_author_time": "2026-06-16T13:20:35-07:00", + "source_committer_time": "2026-06-16T13:20:35-07:00", + "target_committer_time": "2026-06-16T13:20:35-07:00", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/profile.rs" + ], + "normalized_patch_sha256": "52cc978fa92d49782a21a856650fa9a6e66dc0ccc92308352a061be88dbcf1b8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8983bedf79f8b558dee05e643eff61fa54746271", + "target_commit": "439ad010eb3aec5f9193f5ab58c4eab2dc5f6261", + "source_parents": [ + "c9fde521b1640fe7ecbbdd6449a192d8f861f33d" + ], + "target_parents": [ + "ba9863edb2381b7971477ba3a8bd85cb41d2cb3b" + ], + "source_subject": "wasm: migrate deterministic event codec", + "target_subject": "wasm: migrate deterministic event codec", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:31:44Z", + "target_author_time": "2026-08-03T13:31:44Z", + "source_committer_time": "2026-08-03T13:31:44Z", + "target_committer_time": "2026-08-03T13:31:44Z", + "source_paths": [ + "tools/xtask/src/check.rs", + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "739fca39aa197c3c3eeabc94411e2f3e9719b8d99b42cf9f23955428aafffdb9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "89b28cff0055c0bd7e92a021d077edff9214db38", + "target_commit": "cebd246ea7b45b615200b47fb72a23a3821dfe96", + "source_parents": [ + "da9525d37943bb014084885d3259de473e79e7f3" + ], + "target_parents": [ + "5b49fe455e9e87ba6e7ddac140a860e12b3e4a7d" + ], + "source_subject": "sdk: align SQLx runtime error handling", + "target_subject": "sdk: align SQLx runtime error handling", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T05:26:51Z", + "target_author_time": "2026-07-15T05:26:51Z", + "source_committer_time": "2026-07-15T05:26:51Z", + "target_committer_time": "2026-07-15T05:26:51Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/unit/error_tests.rs" + ], + "normalized_patch_sha256": "f3fd6279e6edb2a945afbd81bf87ec340fd85a93df6e9457ac4f203a83c32e26", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8ac37b1bc223c2c6a7c84435cb97fc96f4614b68", + "target_commit": "c6e7c084bbd4ad279c7232ba3fc37b04bf6a6e3b", + "source_parents": [ + "3c0094e3b756de3ff67c82153fbfdd04e9977dc7" + ], + "target_parents": [ + "81fce43cda89b3a2089d1bdd45edcb43fb1c9553" + ], + "source_subject": "sdk: remove binding model crate", + "target_subject": "sdk: remove binding model crate", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T09:08:33Z", + "target_author_time": "2026-06-24T09:08:33Z", + "source_committer_time": "2026-06-24T09:08:33Z", + "target_committer_time": "2026-06-24T09:08:33Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "7cf41a0a9b717f3458e6c6ad10e0d4d36c21ff9fc98b55f16e470d3b47f56882", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8c402daa4c6d4dd34b0f634fc59280b7818613fa", + "target_commit": "3b98208727b404d9c3c4654d2b263f0ee6a038d0", + "source_parents": [ + "755fab472df86ad5f45e398fe0dd590ec23dd0e4" + ], + "target_parents": [ + "2d243420eeb295ecd770446ec472c58d9df62929" + ], + "source_subject": "test: ingest status noise events", + "target_subject": "test: ingest status noise events", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T11:10:06Z", + "target_author_time": "2026-06-30T11:10:06Z", + "source_committer_time": "2026-06-30T11:10:06Z", + "target_committer_time": "2026-06-30T11:10:06Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "9ed64b83d3ab900dd9c1e44b15c259ce1bb562ec4c6820012d4888d4414408f2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8c509998e9e2b6350363b9b9998b01fe3e39c1a9", + "target_commit": "d32368cbbfea5a7245010a40d351f31e8909a73a", + "source_parents": [ + "d5ed93c9f35b8e26f03fcbec320a52c9a8eec1a2" + ], + "target_parents": [ + "7d9a8dbef8ec82ca894ab9bcd2414ef4cbb5ce36" + ], + "source_subject": "release: enforce explicit sdk cohort", + "target_subject": "release: enforce explicit sdk cohort", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T19:39:31Z", + "target_author_time": "2026-07-28T19:39:31Z", + "source_committer_time": "2026-07-28T19:39:31Z", + "target_committer_time": "2026-07-28T19:39:31Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "crates/sql_wasm_runtime/Cargo.toml", + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "b1bc8846d13a48d25be7fc8b94a81caf3156503de23b508f605f8ca59478bc38", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8cea39ce833e5136a2a166a51b2670ec4bf8a20f", + "target_commit": "b704b8030c8d6dbc82cc43bfdbfced327c0c6f09", + "source_parents": [ + "f3008c7bf40e5e116dba6bc7dc4b02a5509ee388" + ], + "target_parents": [ + "c66740d5851ebff78dbe8d8d9e6fb5f03ee91d8e" + ], + "source_subject": "sdk: isolate radrootsd execution adapter", + "target_subject": "sdk: isolate radrootsd execution adapter", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:49:45Z", + "target_author_time": "2026-08-03T11:49:45Z", + "source_committer_time": "2026-08-03T11:49:45Z", + "target_committer_time": "2026-08-03T11:49:45Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs" + ], + "normalized_patch_sha256": "78e6d1582310fd3cf4dd94a55f1f955d01f75e2275cfe22ce95ba1cd2c2389f9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8d1846eb57633e0923cef6902851e27c2bd6e703", + "target_commit": "08f1c8b3229f71fdfa37f95450837abf8f28f34f", + "source_parents": [ + "d819a9f25c67ecae4d3cd437c26c96a8103cbf64" + ], + "target_parents": [ + "f0d412234b339d76e26970f9798d87784a133583" + ], + "source_subject": "sdk: enforce relay evidence filter boundaries", + "target_subject": "sdk: enforce relay evidence filter boundaries", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T10:49:18Z", + "target_author_time": "2026-07-01T10:49:18Z", + "source_committer_time": "2026-07-01T10:49:18Z", + "target_committer_time": "2026-07-01T10:49:18Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "d8b48f77814041bfdf32a99397ad2286bcc49236dcbfdfd3e397bab00d2b1e41", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8ebaa2445f1d6db1d691775eef6be454116524f1", + "target_commit": "0da30c9e42d3ac45365bb4c306129a9543f11bd4", + "source_parents": [ + "add9743dae423065a7e84328993eafa78669090c" + ], + "target_parents": [ + "a528f0fee4606c62ebaa15698dcbc6b5c9ca41a3" + ], + "source_subject": "sdk: align workflow sync transport surfaces", + "target_subject": "sdk: align workflow sync transport surfaces", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T22:48:42Z", + "target_author_time": "2026-07-06T22:48:42Z", + "source_committer_time": "2026-07-06T22:48:42Z", + "target_committer_time": "2026-07-06T22:48:42Z", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "e4fce971702d1d60ab7fde2f9948abf14305061df42bb8717e6d3f7d46e6152e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "8ed1e0500f232489c23620f2ceb445bcafbb079e", + "target_commit": "bd217bd3577b83906d8c88a42d6dbc69b960845f", + "source_parents": [ + "21543db0546b0768e8b3d114d7af4ae36c7e5607" + ], + "target_parents": [ + "586de76e625aeea89b9670470657960884f1ff3c" + ], + "source_subject": "workspace: normalize public package metadata", + "target_subject": "workspace: normalize public package metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T09:09:59Z", + "target_author_time": "2026-07-27T09:09:59Z", + "source_committer_time": "2026-07-27T09:09:59Z", + "target_committer_time": "2026-07-27T09:09:59Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "91b669892b4908449acf57b65d0a1fce6b89c9aa5272d813fe453ffa04820112", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "915bc14f3951f99ba853667690776068f991a6af", + "target_commit": "9a43937b5c956970c07155ed845a499c942574e0", + "source_parents": [ + "58c42a7a77cb5f60dd10bb50695d92586232adfd" + ], + "target_parents": [ + "650aac1745fbadb4aa342677229a4193c6e15153" + ], + "source_subject": "sdk: migrate signer providers to radroots_signing", + "target_subject": "sdk: migrate signer providers to radroots_signing", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:01:20Z", + "target_author_time": "2026-08-03T11:01:20Z", + "source_committer_time": "2026-08-03T11:01:20Z", + "target_committer_time": "2026-08-03T11:01:20Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "568f552006f7567b4f226d5d39f52baf8a07031b389c5c1c99e71a7000642fcd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9285c57f4c8ff90363bd37940361ed85de1a1a70", + "target_commit": "a5a19bd919e68bf4115d97da5eb8efc6efaf8f64", + "source_parents": [ + "fd8384aee348034e0c8ea17a868fe7f094770050" + ], + "target_parents": [ + "5ed07d69ae5739867e40aa706b5bce99d6f6cbb0" + ], + "source_subject": "transport: use governed target accessors", + "target_subject": "transport: use governed target accessors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T20:40:24Z", + "target_author_time": "2026-07-27T20:40:24Z", + "source_committer_time": "2026-07-27T20:40:24Z", + "target_committer_time": "2026-07-27T20:40:24Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs" + ], + "normalized_patch_sha256": "0e1e7ca3c4347dc3c83c8bc15d518f82ea1ad0f7f53dddc97903e6f989ced6e0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "93027d0e0885a2234fbc244559f82b70b5f93010", + "target_commit": "13e1d274f04fc464124bf9c7b8d02c51f224c6a2", + "source_parents": [ + "0455d7211ac01f0e8a6d15908285a90202bebc17" + ], + "target_parents": [ + "67e5089b435d89f763b73a60ed3b23e5a9f6a374" + ], + "source_subject": "transport: consume shared Reticulum endpoint in target sets", + "target_subject": "transport: consume shared Reticulum endpoint in target sets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T20:34:34Z", + "target_author_time": "2026-07-07T20:34:34Z", + "source_committer_time": "2026-07-07T20:34:34Z", + "target_committer_time": "2026-07-07T20:34:34Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "04bdd3d9f97c5d3b164d28163b8d858ea1e8b770cd9d0b46abb5e73bdebc0277", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9385ccaf0aee19357a45dc666886e49d029b83c7", + "target_commit": "b55f9190d9381e1810f27d9a4eb9fdd963e36a34", + "source_parents": [ + "fb92badb3815ffeea38a3f233c78fc5153a249f8" + ], + "target_parents": [ + "ceef2a99d1418dbc18082b6e43f4b7535fb94f00" + ], + "source_subject": "release: align SDK development contracts", + "target_subject": "release: align SDK development contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-29T23:42:51Z", + "target_author_time": "2026-07-29T23:42:51Z", + "source_committer_time": "2026-07-29T23:42:51Z", + "target_committer_time": "2026-07-29T23:42:51Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/sdk/Cargo.toml" + ], + "normalized_patch_sha256": "c37f563007e4a5037845ad477321d3f10e41fccee41f8ec79b9c2755b773c1ca", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "956c5114bbccd03955060c274201bf54f54a2b11", + "target_commit": "965979b2252ee3090dd6d46e05bc1021ef4676f5", + "source_parents": [ + "9eb9e9770e1ccf06559c612a1822cde49ebff705" + ], + "target_parents": [ + "52095e03b168c694223be519fa76417590ca7ac5" + ], + "source_subject": "fix(release): qualify SDK package archives", + "target_subject": "fix(release): qualify SDK package archives", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T12:42:05Z", + "target_author_time": "2026-08-04T12:42:05Z", + "source_committer_time": "2026-08-04T12:42:05Z", + "target_committer_time": "2026-08-04T12:42:05Z", + "source_paths": [ + "crates/radroots/Cargo.toml", + "crates/radroots/tests/package_boundary.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "9638c8c3ec95fb5ea1297ddb3fb0d3ca027cf77bfcf788a68c761a4fbe340d6c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "956fa20f4f050816f0a798b39882a59772b6bd8c", + "target_commit": "66c26bcf0793e3d0de933396b60646801fdab081", + "source_parents": [ + "36509e62546b07c2aa2f62cd6ad07b4f39d2abff" + ], + "target_parents": [ + "fe1ab75ec3fdbbd2aa4470b13e7d1dd07ca5f593" + ], + "source_subject": "xtask: resolve wasm cflags deterministically", + "target_subject": "xtask: resolve wasm cflags deterministically", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T06:55:48Z", + "target_author_time": "2026-07-06T06:55:48Z", + "source_committer_time": "2026-07-06T06:55:48Z", + "target_committer_time": "2026-07-06T06:55:48Z", + "source_paths": [ + "tools/xtask/src/wasm.rs" + ], + "normalized_patch_sha256": "381efe56a6ac36c3727c81174fa4b07392a0457f02b42840208dc50247bb52c5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "96708a659006018b626974264f134bbfda5a948a", + "target_commit": "1a648434b025b04885b100e28d2d9ffe690ae618", + "source_parents": [ + "402732b862f40142bdd6445a4d5b8187c2f0a45d" + ], + "target_parents": [ + "59a46b604d1ad90ce1a1a2976930e4238a27e0fe" + ], + "source_subject": "tests: add sdk adoption acceptance coverage", + "target_subject": "tests: add sdk adoption acceptance coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T07:40:43Z", + "target_author_time": "2026-06-30T07:40:43Z", + "source_committer_time": "2026-06-30T07:40:43Z", + "target_committer_time": "2026-06-30T07:40:43Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "c30b0804ba5520b164df0703d76f598c8d543d7c33f0ccef0f855a548383121d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "97610594ed1f3dc4390feafc07b5a061044b5e3f", + "target_commit": null, + "source_parents": [ + "bc2a71c3a1c52c268ffd404b967c53253936ef65" + ], + "target_parents": [], + "source_subject": "refactor: align event bindings", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T14:12:07-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T14:12:07-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "97fdfe0efa84fb2cb5a9bd0984c978550b133be7", + "target_commit": "165f130dd079d59d9c749275c32328e44ec14829", + "source_parents": [ + "4ac891a625014e3044ab343ac3a704c20882335d" + ], + "target_parents": [ + "11e59ce9e6b56d2d248c6be21368f091396bae2c" + ], + "source_subject": "ci: enforce crates release architecture", + "target_subject": "ci: enforce crates release architecture", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T12:48:29Z", + "target_author_time": "2026-07-27T12:48:29Z", + "source_committer_time": "2026-07-27T12:48:29Z", + "target_committer_time": "2026-07-27T12:48:29Z", + "source_paths": [ + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "376b79799b38bc6a4deb3115339af8fdc44afe818cacc6a239854bde7f4d096d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9883c1c0e7139d2552f21457f8819f14ca3467a3", + "target_commit": "87a54fd93c7c4d62b008acba73c51ee0d78f41ff", + "source_parents": [ + "dd50171d8225615ca9368a2c2ff987344118fe9b" + ], + "target_parents": [ + "5af1611cfc896a1ad9890e08b60dad36c5a5ab80" + ], + "source_subject": "docs: align grouped trade SDK surface", + "target_subject": "docs: align grouped trade SDK surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T20:05:50Z", + "target_author_time": "2026-06-30T20:05:50Z", + "source_committer_time": "2026-06-30T20:05:50Z", + "target_committer_time": "2026-06-30T20:05:50Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "f0e5241766adefc7b81df68075294f45598cbf78d6c9b76f0b86af6ee42da081", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "99e68e3d54ebd748f8f3fdae220bee63458a55b3", + "target_commit": "1b5c210a0d9189c5a632448bd3374e4ff871095f", + "source_parents": [ + "e743afc0154003897dd1e318e543f1a80ccf9dc4" + ], + "target_parents": [ + "b9a1f1e38c5c9ea0154eaa1a4741bb0b49bd37d4" + ], + "source_subject": "sdk: restore shared mobile social engine", + "target_subject": "sdk: restore shared mobile social engine", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T17:05:07Z", + "target_author_time": "2026-08-03T17:05:07Z", + "source_committer_time": "2026-08-03T17:05:07Z", + "target_committer_time": "2026-08-03T17:05:07Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README.md", + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/src/sync.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/public_api.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "1ea4480f1015fcde1727fd2ca13c31f365a14af4bfbc1287f9f996175017e807", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9aa7d727eff8cc342fe698f53903e1339772ebf0", + "target_commit": "70cfc205200e56f45dbfa76b983b50b7db13bbb2", + "source_parents": [ + "267e76547d08db145df6133dc2e8ffbf25b48e50" + ], + "target_parents": [ + "105dea109a2356188dce16ecdebb3864e798f3f8" + ], + "source_subject": "secrets: migrate workspace consumers", + "target_subject": "secrets: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-01T11:57:54Z", + "target_author_time": "2026-08-01T11:57:54Z", + "source_committer_time": "2026-08-01T11:57:54Z", + "target_committer_time": "2026-08-01T11:57:54Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/private_store.rs", + "crates/sdk/tests/secrets_migration_boundary.rs" + ], + "normalized_patch_sha256": "606c4cba8d33ae7f63be4327641226d194e40c9b08fdfa68c53e1fab84abe490", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9abf9f2b8751e9cb79da519e59e96db2fd3f4ca1", + "target_commit": "2d157f928e7cccad7a60d060718db448ec5905e8", + "source_parents": [ + "25afa54bbc5c2f35aa522f25021532a84b878e5e" + ], + "target_parents": [ + "c0ba705ff19c5951ab5ed84dc0e2d4cf5af7c577" + ], + "source_subject": "sdk: rename Nostr transport features", + "target_subject": "sdk: rename Nostr transport features", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T01:37:32Z", + "target_author_time": "2026-07-09T01:37:32Z", + "source_committer_time": "2026-07-09T01:37:32Z", + "target_committer_time": "2026-07-09T01:37:32Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/relay.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_relay_tests.rs", + "crates/sdk/tests/unit/error_tests.rs" + ], + "normalized_patch_sha256": "425e11f29ca6eb8ab83ea068b338855765edfdca5aff2e7b1d084c09c2587152", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9b073c1fec2c98393673ecff93d4424da1e71413", + "target_commit": "fc4d0188420ff694485e4065a44e6539255ea247", + "source_parents": [ + "b2a71062a33c1aad550e92c755f8e56233423e34" + ], + "target_parents": [ + "37db737c454043f69bdc14d4b5325b98698df2ae" + ], + "source_subject": "sdk: add source boundary guard", + "target_subject": "sdk: add source boundary guard", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T13:45:31-07:00", + "target_author_time": "2026-06-18T13:45:31-07:00", + "source_committer_time": "2026-06-18T13:45:31-07:00", + "target_committer_time": "2026-06-18T13:45:31-07:00", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "063ad1084f38631d0b06e5e74270d4a26d5944f5f550022440d45a35d54f66d8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9b45e557252f9db93d454a9f39bf3dd01b4f9a93", + "target_commit": "c3b2de702fd1a4893ea5e85175e2e0b78c4d10fe", + "source_parents": [ + "93027d0e0885a2234fbc244559f82b70b5f93010" + ], + "target_parents": [ + "13e1d274f04fc464124bf9c7b8d02c51f224c6a2" + ], + "source_subject": "transport: clear proxy claims on local validation errors", + "target_subject": "transport: clear proxy claims on local validation errors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T20:57:17Z", + "target_author_time": "2026-07-07T20:57:17Z", + "source_committer_time": "2026-07-07T20:57:17Z", + "target_committer_time": "2026-07-07T20:57:17Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "70a1e1ce1261d1ce89bcbd7b7ff539a482f0864f9ba366d21e8f7e634fd9a419", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9be9842a3d55b43d68e01b3966b710e98211220c", + "target_commit": "c258a0553e4b6c30cdddef86eacadaf05ad29004", + "source_parents": [ + "b852798e0436e6dadfb5b9c7f0fc44e732046b95" + ], + "target_parents": [ + "3d335907b7a54e8129b74e7032b80553aea852cf" + ], + "source_subject": "sdk: harden order status receipts", + "target_subject": "sdk: harden order status receipts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T15:36:23-07:00", + "target_author_time": "2026-06-19T15:36:23-07:00", + "source_committer_time": "2026-06-19T15:36:23-07:00", + "target_committer_time": "2026-06-19T15:36:23-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "6847d3aedab190155545bf91bf4fc74cb3e67c008aa35a267a1af1df79c15fa6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9d473e576af62208a3e35c54cd05da263196049c", + "target_commit": "854e2423de13c95be43a8abdbc5b7a9f3182e904", + "source_parents": [ + "0bd6b0a7fae7b4676b22bb721f5d6452863d0e89" + ], + "target_parents": [ + "0242454c74964a329ff2a2c304d6364f1d0a1675" + ], + "source_subject": "tests: use verified signed event fixtures", + "target_subject": "tests: use verified signed event fixtures", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-14T21:14:22Z", + "target_author_time": "2026-07-14T21:14:22Z", + "source_committer_time": "2026-07-14T21:14:22Z", + "target_committer_time": "2026-07-14T21:14:22Z", + "source_paths": [ + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs" + ], + "normalized_patch_sha256": "ae63621908234517df7d9fc912c6cd832c56d2e4d7e8a09c4e58b69a02d8e442", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9ddfcef621187379bbb91b9e3d519532330b3dd0", + "target_commit": "c96c0063a00b507f1382d61af9b099e0aab1db87", + "source_parents": [ + "7faefa3a24709ee9662aaa66d5c10d6409cb109e" + ], + "target_parents": [ + "04e95e941187000765ae01ced4eda1c048bbca33" + ], + "source_subject": "sdk: add async signer provider core", + "target_subject": "sdk: add async signer provider core", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T23:30:56Z", + "target_author_time": "2026-06-23T23:30:56Z", + "source_committer_time": "2026-06-23T23:30:56Z", + "target_committer_time": "2026-06-23T23:30:56Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "b68d62774ba6382a70cfebb6566eea34b2952774d3027662f08d59fad26eb7db", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9de79d220d5a165b18d3687478004ca501a1d145", + "target_commit": "c7d030b68d1dbca0282fb637027eec161d56ae32", + "source_parents": [ + "cfbc2001f696b457fbc7f19b5f4c8ab6a2c157c4" + ], + "target_parents": [ + "867f3e4f403e778ae4f6b3363c2bca7656b0a46c" + ], + "source_subject": "sdk: preserve relay fetch limit evidence", + "target_subject": "sdk: preserve relay fetch limit evidence", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T20:16:22Z", + "target_author_time": "2026-07-01T20:16:22Z", + "source_committer_time": "2026-07-01T20:16:22Z", + "target_committer_time": "2026-07-01T20:16:22Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "797d44b914ffb6aa6ea3e1f37fd7752b33c3228b835ec150c6983841dc6fb820", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9e1462ae7e9acd785ca09acf0cc81049a68d262d", + "target_commit": "31e0f07fa55a06289eb4b7f7e0d7b5262f1cbafb", + "source_parents": [ + "4638baeffcb0be18c0ee32b799d88422f6edb7d8" + ], + "target_parents": [ + "597ebf9950e39c1ac775471c3e91d2368375537f" + ], + "source_subject": "wasm: add knowledge event exports", + "target_subject": "wasm: add knowledge event exports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T06:55:18Z", + "target_author_time": "2026-07-05T06:55:18Z", + "source_committer_time": "2026-07-05T06:55:18Z", + "target_committer_time": "2026-07-05T06:55:18Z", + "source_paths": [ + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "fbb00f235f7ba2c6c862733bbbecaf2bb6edd6496ffbe7d2830a08bfb437b407", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9e511cc931c7b6c51cf168ec37d1b62a43944a12", + "target_commit": "6f21438079eb92228cde2157ee5358e90478c2a4", + "source_parents": [ + "1ce1da677c6992ce87129a59cd6db2a9a720e401" + ], + "target_parents": [ + "79f93f6cb9ba4251dcf54308271071f1fc453bd1" + ], + "source_subject": "sdk: implement the unified capability model", + "target_subject": "sdk: implement the unified capability model", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:37:23Z", + "target_author_time": "2026-08-03T10:37:23Z", + "source_committer_time": "2026-08-03T10:37:23Z", + "target_committer_time": "2026-08-03T10:37:23Z", + "source_paths": [ + "crates/sdk/src/capability.rs", + "crates/sdk/src/client.rs" + ], + "normalized_patch_sha256": "c6060de091f65ddc092d8e5de9a059442a4153f14f471b5d0d459223d6e2bde2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "9eb9e9770e1ccf06559c612a1822cde49ebff705", + "target_commit": null, + "source_parents": [ + "75c719bc0f79ae12a3e4a13a56d5a8943985c8d5" + ], + "target_parents": [], + "source_subject": "build(package): bootstrap facade verification", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-08-04T11:46:04Z", + "target_author_time": null, + "source_committer_time": "2026-08-04T11:46:04Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "9f07080df63726521953d0456c3e5fb05ccc3242", + "target_commit": "9f2a3205ba3aa4b27275bef9842659ae17589dcc", + "source_parents": [ + "97610594ed1f3dc4390feafc07b5a061044b5e3f" + ], + "target_parents": [ + "b789991c7292870652bef42d15d54172d29dd691" + ], + "source_subject": "refactor: align schema bindings", + "target_subject": "refactor: align schema bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T14:16:45-07:00", + "target_author_time": "2026-06-11T14:16:45-07:00", + "source_committer_time": "2026-06-11T14:16:45-07:00", + "target_committer_time": "2026-06-11T14:16:45-07:00", + "source_paths": [ + "crates/trade_bindings/Cargo.toml", + "crates/trade_bindings/src/lib.rs", + "crates/trade_bindings/src/model.rs", + "crates/trade_bindings/src/typescript/types.ts" + ], + "normalized_patch_sha256": "b7643d293cb8926adf16cb959d698e9cfdb4cd742018b589de389b0fec83ddea", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a00c36a1b2d5ed269878b954041331e328b92d55", + "target_commit": "4476586cc10a6f1b1ee58f271991a6be765c791d", + "source_parents": [ + "a7e63f6ccea6fdc60047d583c54e816a95576366" + ], + "target_parents": [ + "c4d7ebfd91d4725fa440578df7d59de400c0065b" + ], + "source_subject": "workspace: enforce snake case crate identities", + "target_subject": "workspace: enforce snake case crate identities", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T20:49:23Z", + "target_author_time": "2026-07-27T20:49:23Z", + "source_committer_time": "2026-07-27T20:49:23Z", + "target_committer_time": "2026-07-27T20:49:23Z", + "source_paths": [ + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "9f5d74494c943e0ca8bcf33f1e9fe89b5c365488e21ee5893de50b46d893e657", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a10f2f320c3b54effbe8c48f3cf5129093ab3dda", + "target_commit": "ceef2a99d1418dbc18082b6e43f4b7535fb94f00", + "source_parents": [ + "39b12eaa8b2b9c6246246200b888e7ce70f12f08" + ], + "target_parents": [ + "4d03e7cff98fe994dc698ed56fb23ec7250e8cea" + ], + "source_subject": "event: migrate bindings beyond superseded surface", + "target_subject": "event: migrate bindings beyond superseded surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-29T08:19:58Z", + "target_author_time": "2026-07-29T08:19:58Z", + "source_committer_time": "2026-07-29T09:42:56Z", + "target_committer_time": "2026-07-29T09:42:56Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "c2cd18c5b637537e02da0e0a3ea50d7a10712947a79a745dc4b7cba853ad2dde", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a307d2d00aed4d37b846e6668cf482e44b3a60e4", + "target_commit": "fc2f1429f372153731a594808a4abb001a07c4d6", + "source_parents": [ + "4b653ca15108b173617adc7842684e01190dfdea" + ], + "target_parents": [ + "4896a04a77d2df368c801f359fcc4637c17055bf" + ], + "source_subject": "release: define approved public package allowlist", + "target_subject": "release: define approved public package allowlist", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T07:49:17Z", + "target_author_time": "2026-07-27T07:49:17Z", + "source_committer_time": "2026-07-27T07:49:17Z", + "target_committer_time": "2026-07-27T07:49:17Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "364fce0e3df50c682b4e4dd5e090f66be0429f998b5c11a8cb69c269a024ad57", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a30e83b0a97abfc06518e0aa92479ba8f8fa4719", + "target_commit": "2709a7ae5e8a2f19e0d1b7c5f4bcdb4711e5bdd0", + "source_parents": [ + "ce8cbacafefb2bffad02c87735c9f8b62e5e1650" + ], + "target_parents": [ + "643f12734ca3d9bea675b8bf15760f9bb38c1ef0" + ], + "source_subject": "dto: add explicit source root sets", + "target_subject": "dto: add explicit source root sets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T06:25:52Z", + "target_author_time": "2026-06-24T06:25:52Z", + "source_committer_time": "2026-06-24T06:25:52Z", + "target_committer_time": "2026-06-24T06:25:52Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "efbea0e7d56f1762e4cf09f65118176d99e07727db02391955211f2bc074890d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a314092543bd2eeb6fe0fef033790651083969b1", + "target_commit": "47938c2d6e7702401014c2cad72b33acf2c077d5", + "source_parents": [ + "516fb4231e6227193ca79385797d339f1a8f3dad" + ], + "target_parents": [ + "993a633408a9db3f2a82ee6da33f9f5767db39c9" + ], + "source_subject": "transport: align SDK satisfaction and preview workflow contracts", + "target_subject": "transport: align SDK satisfaction and preview workflow contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T21:03:11Z", + "target_author_time": "2026-07-09T21:03:11Z", + "source_committer_time": "2026-07-09T21:03:11Z", + "target_committer_time": "2026-07-09T21:03:11Z", + "source_paths": [ + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "c8309862e214f623e38bd3ef06f34cb392e595e4cb71613a6cc5a546ccc048a6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a3781823586e4a61c49afa4fb0a8396dd309afe7", + "target_commit": "63f396706f8e32f8ae15e2f342d3fca3054e40c4", + "source_parents": [ + "d43f72542d6296194944d95328bf172b887d3f89" + ], + "target_parents": [ + "a56654a61c257328ea4251b988cb922f593f3694" + ], + "source_subject": "release: classify front doors as std-only", + "target_subject": "release: classify front doors as std-only", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T23:01:35Z", + "target_author_time": "2026-08-03T23:01:35Z", + "source_committer_time": "2026-08-03T23:01:35Z", + "target_committer_time": "2026-08-03T23:01:35Z", + "source_paths": [ + "tools/xtask/src/main.rs", + "tools/xtask/src/portable_qualification.rs" + ], + "normalized_patch_sha256": "0aeb1361ec7f47c3df83cf5a207d63fa4d3f2b1343e10f6da9d2a1203957158a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a4c2ee6b98e3ffbe70d50e221193699c8aff0ae7", + "target_commit": "a1fd7c79f51357c783f3af5dbb78aa73ebdbc641", + "source_parents": [ + "618d199c2893831d5a208080bb38fda7e750f0b5" + ], + "target_parents": [ + "2e0ee90383bc42fc5d7b17e0c427b77db455f18d" + ], + "source_subject": "sdk: add runtime foundation", + "target_subject": "sdk: add runtime foundation", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T13:55:33-07:00", + "target_author_time": "2026-06-15T13:55:33-07:00", + "source_committer_time": "2026-06-15T13:55:33-07:00", + "target_committer_time": "2026-06-15T13:55:33-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/receipt.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "b0814bde030c6e1bfebefe6f5bf17fbd0fdb9b0e6d1f0df66322a9703ffc31a7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a54ec0b1c92fbd24d142c8d32b79b0de7ba36813", + "target_commit": null, + "source_parents": [ + "29c10ac3f65668a38468f98ad361699de29aaafe" + ], + "target_parents": [], + "source_subject": "extbuild: add SDK project policy", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-17T09:03:03Z", + "target_author_time": null, + "source_committer_time": "2026-07-17T09:03:03Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "a5f2722e28bfc2e8969c7a368605f1fbbf1502ba", + "target_commit": "3d40e54f887c2916b84463f914ac8f65d30da79d", + "source_parents": [ + "6bf67b0b2b2ddbc56b07a77572ae7ea369784c92" + ], + "target_parents": [ + "7b7425fa32a47d0f959b5ae21b0e23f566a6ad13" + ], + "source_subject": "sdk: harden foundation source guard", + "target_subject": "sdk: harden foundation source guard", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-14T18:48:44Z", + "target_author_time": "2026-07-14T18:48:44Z", + "source_committer_time": "2026-07-14T18:48:44Z", + "target_committer_time": "2026-07-14T18:48:44Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "1d950d557eedd7ac344a79d02d6bf775e8b1f92ec8a742fc4dea655d65dcb351", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a699b7f2674f4fe2d2ac8ec3ad47bd6484b1bdb4", + "target_commit": "94589c847a864f5e2cab578473a210c32659f03c", + "source_parents": [ + "011f929fc8575170293e15f38556c124c0cf3934" + ], + "target_parents": [ + "465c9faa8197ae16e7d6c32099d173b3ca2469a7" + ], + "source_subject": "feat(bindings): generate core and types packages", + "target_subject": "feat(bindings): generate core and types packages", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T06:17:51-07:00", + "target_author_time": "2026-06-11T06:17:51-07:00", + "source_committer_time": "2026-06-11T06:17:51-07:00", + "target_committer_time": "2026-06-11T06:17:51-07:00", + "source_paths": [ + "crates/core_bindings/src/lib.rs" + ], + "normalized_patch_sha256": "26b391876de93664f7263cf04278861c3e04cf0415d16c9c99490bb52d5eb556", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a752e21babfaf382383b5e1f3b7005d7890d64a0", + "target_commit": "235a396133ca0c5adde648396c8ed983b0d1185f", + "source_parents": [ + "8983bedf79f8b558dee05e643eff61fa54746271" + ], + "target_parents": [ + "439ad010eb3aec5f9193f5ab58c4eab2dc5f6261" + ], + "source_subject": "ffi: wrap shared sdk engine", + "target_subject": "ffi: wrap shared sdk engine", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:36:36Z", + "target_author_time": "2026-08-03T13:36:36Z", + "source_committer_time": "2026-08-03T13:36:36Z", + "target_committer_time": "2026-08-03T13:36:36Z", + "source_paths": [ + "crates/ffi/Cargo.toml", + "crates/ffi/README.md", + "crates/ffi/src/lib.rs" + ], + "normalized_patch_sha256": "6ab1051b737f97fbf2a0ef493852b58d4d4b0ca66ae82ad1a63b14373f6d0ccb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a778f2d58f4f05c09d281e43d25eaf64eb774421", + "target_commit": "683b7e5d5359fa39348f79496326ac14b2d68170", + "source_parents": [ + "956fa20f4f050816f0a798b39882a59772b6bd8c" + ], + "target_parents": [ + "66c26bcf0793e3d0de933396b60646801fdab081" + ], + "source_subject": "runtime: align SDK drafts with strict contracts", + "target_subject": "runtime: align SDK drafts with strict contracts", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T08:40:33Z", + "target_author_time": "2026-07-06T08:40:33Z", + "source_committer_time": "2026-07-06T08:40:33Z", + "target_committer_time": "2026-07-06T08:40:33Z", + "source_paths": [ + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs" + ], + "normalized_patch_sha256": "c4f34fd3d1b0cbe9871767bde92897a5de28835b6c12b34fdd72de352a4fd69a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a7e497dc748b64bb3831b05205081f56aa1dd7b6", + "target_commit": "893ea49ddb73a156b2a95562a0e548c7b54ff337", + "source_parents": [ + "632465a739f8fdd4b2c6911df3ed65846690694c" + ], + "target_parents": [ + "a454e95a7955b858351004a7d65cebe9c72e8dd5" + ], + "source_subject": "blossom: normalize Blossom type names and module ownership", + "target_subject": "blossom: normalize Blossom type names and module ownership", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T08:21:01Z", + "target_author_time": "2026-07-28T08:21:01Z", + "source_committer_time": "2026-07-28T08:21:01Z", + "target_committer_time": "2026-07-28T08:21:01Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs" + ], + "normalized_patch_sha256": "c4847385640697ca16acf653cd66c0e067375d296238786872e2664010d48b57", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a7e63f6ccea6fdc60047d583c54e816a95576366", + "target_commit": "c4d7ebfd91d4725fa440578df7d59de400c0065b", + "source_parents": [ + "b6ce7f66f838ecb89e3a1d40c376d1017b858512" + ], + "target_parents": [ + "02ecef9424453f4aa274ec33ac8d7ee3856be26e" + ], + "source_subject": "workspace: correct crate names to snake case", + "target_subject": "workspace: correct crate names to snake case", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T18:52:09Z", + "target_author_time": "2026-07-27T18:52:09Z", + "source_committer_time": "2026-07-27T18:52:09Z", + "target_committer_time": "2026-07-27T18:52:09Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README.md", + "tools/xtask/src/architecture.rs", + "tools/xtask/src/architecture/api_leakage.rs", + "tools/xtask/src/architecture/dependency_boundary.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/smoke.rs", + "tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json", + "tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json", + "tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json", + "tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json" + ], + "normalized_patch_sha256": "6c68edcb51f8018eddecfd8786867f9d5c9645a58b85d1be1eff16b833a06938", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a85410e02913454ff7e4bfcabf66b3828228f694", + "target_commit": "1050e18ed6f7f3aac073274f0048bf03ffe25a64", + "source_parents": [ + "cb3c38e2ad8c8c7d598b00bb966d88c5e7221d53" + ], + "target_parents": [ + "0740da72976e88a58166aa1bcb62a5913c779cc3" + ], + "source_subject": "ts: render constants through dto_bindgen", + "target_subject": "ts: render constants through dto_bindgen", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T00:14:49Z", + "target_author_time": "2026-06-28T00:14:49Z", + "source_committer_time": "2026-06-28T00:14:49Z", + "target_committer_time": "2026-06-28T00:14:49Z", + "source_paths": [ + "crates/identity_bindings/Cargo.toml", + "crates/identity_bindings/src/lib.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "6be75431afbfc1e51b0a858fd8e6d9705a1fbfae8975ba1c54502a361fbdd080", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a86d62821f7fe0eb50cf11dfe0dd5c4b7bc6b9cb", + "target_commit": "039d72813c0dadb0c14b10e04f68eabce6514786", + "source_parents": [ + "8c509998e9e2b6350363b9b9998b01fe3e39c1a9" + ], + "target_parents": [ + "d32368cbbfea5a7245010a40d351f31e8909a73a" + ], + "source_subject": "protocol: quarantine superseded package surface", + "target_subject": "protocol: quarantine superseded package surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T20:15:15Z", + "target_author_time": "2026-07-28T20:15:15Z", + "source_committer_time": "2026-07-28T20:23:37Z", + "target_committer_time": "2026-07-28T20:23:37Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/runtime_contract_public_api.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "8d8ad6b0fc3dc29e128d1fc6b489ea78b66787fa967cd7ca2bfaee73d9240666", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a93c1e74cf94737d25046ac4f2212c05a4717ec6", + "target_commit": "fee409998f8f86e2fc415af74aa13db4b564e3eb", + "source_parents": [ + "c4f70690fd59bcd7497b72690016c3ad6ffbfc76" + ], + "target_parents": [ + "86a2bb50b5fd6087cbbeb19bfb764d6e3b408db0" + ], + "source_subject": "sdk: harden public error contract", + "target_subject": "sdk: harden public error contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T17:42:40-07:00", + "target_author_time": "2026-06-17T17:42:40-07:00", + "source_committer_time": "2026-06-17T17:42:40-07:00", + "target_committer_time": "2026-06-17T17:42:40-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/error.rs", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "48c7a1c220aa2ecf8b8ab70a42bcbf82766947817ff7fbfd9c4d88300202c873", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a95445b181e108a35e2d524798201cd89919c0ce", + "target_commit": "deca91ffb6e04cf810080aa099646bb2b52b356d", + "source_parents": [ + "3b6767ef28eae443cb29a62a88ccd649b2977274" + ], + "target_parents": [ + "17ade41af8b510207a4900a2a3e2ce3661495a03" + ], + "source_subject": "architecture: enforce package dependency direction", + "target_subject": "architecture: enforce package dependency direction", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T11:33:32Z", + "target_author_time": "2026-07-27T11:33:32Z", + "source_committer_time": "2026-07-27T11:33:32Z", + "target_committer_time": "2026-07-27T11:33:32Z", + "source_paths": [ + "tools/xtask/src/architecture.rs", + "tools/xtask/src/architecture/dependency_boundary.rs", + "tools/xtask/src/main.rs", + "tools/xtask/tests/fixtures/dependency-boundaries/domain-to-storage.json", + "tools/xtask/tests/fixtures/dependency-boundaries/service-to-sdk.json", + "tools/xtask/tests/fixtures/dependency-boundaries/spi-to-adapter.json", + "tools/xtask/tests/fixtures/dependency-boundaries/valid-downward.json" + ], + "normalized_patch_sha256": "8515f25efbd5b3cad4a7c245b585c90a2e7ae29229998e9fc31521d346ae24e9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a97af18da6b38d9d68bda6b1d4a9dae2070fe1ef", + "target_commit": "3047f3f95183149697babe97c8711143ed6ed226", + "source_parents": [ + "9de79d220d5a165b18d3687478004ca501a1d145" + ], + "target_parents": [ + "c7d030b68d1dbca0282fb637027eec161d56ae32" + ], + "source_subject": "sdk: classify untrusted worker evidence by default", + "target_subject": "sdk: classify untrusted worker evidence by default", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T20:18:48Z", + "target_author_time": "2026-07-01T20:18:48Z", + "source_committer_time": "2026-07-01T20:18:48Z", + "target_committer_time": "2026-07-01T20:18:48Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "6f2b88549f4e28cc53551906ef80f0aed04ee3e885c6226242bb742b838846a6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "a97cf153fdfd4374945c0aa83aa7026d3be4e425", + "target_commit": "1860ba74d6775b99b5aa5b14826a9f70d737f6f2", + "source_parents": [ + "6f16fa242bf1138d8e1d5210faa0438523cdd62c" + ], + "target_parents": [ + "e64e3db81cb82414fd851aa9a5afb13cbeef4e42" + ], + "source_subject": "sync: surface deferred preview outbox status", + "target_subject": "sync: surface deferred preview outbox status", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T19:21:08Z", + "target_author_time": "2026-07-07T19:21:08Z", + "source_committer_time": "2026-07-07T19:21:08Z", + "target_committer_time": "2026-07-07T19:21:08Z", + "source_paths": [ + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "8dc68b4bcb7d53396861374b3ea0b44e7b127312a85d5414a6bbc82540eed378", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "aabb944db75811f33436084e98778729be8d844f", + "target_commit": "4267774b0f66efc51920512bb6a7671d33f4a82f", + "source_parents": [ + "ec425bca2e9f23577f80056bd9b8230e44a0322a" + ], + "target_parents": [ + "1069d841d785e062d6aef9813e3b8134b1b71723" + ], + "source_subject": "sdk: validate radrootsd proxy responses", + "target_subject": "sdk: validate radrootsd proxy responses", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T00:42:38Z", + "target_author_time": "2026-07-08T00:42:38Z", + "source_committer_time": "2026-07-08T00:42:38Z", + "target_committer_time": "2026-07-08T00:42:38Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs" + ], + "normalized_patch_sha256": "02cf061f5c0d72c2191afa7534d5baefa0e04a60eeab368ba7b3e5f49b49d85a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "aac11ecc400624d8789c1dc99cc9fd5681f8a7e7", + "target_commit": "dd0c154c255346d99abd7baa4ac675a61ea56264", + "source_parents": [ + "2dad39727858c557d0f10d0d3c43c2b0ca581208" + ], + "target_parents": [ + "5cf9993e6ad8b807623170709ec2ddeb95b07574" + ], + "source_subject": "sdk: expose fixture geonames asset feature", + "target_subject": "sdk: expose fixture geonames asset feature", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T23:53:30Z", + "target_author_time": "2026-06-26T23:53:30Z", + "source_committer_time": "2026-06-26T23:53:30Z", + "target_committer_time": "2026-06-26T23:53:30Z", + "source_paths": [ + "crates/sdk/Cargo.toml" + ], + "normalized_patch_sha256": "f10f85d7f3c9b425e23b802ac570a8253e1cbcde0c086008c616e44b44f92eaa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "aad1d4654a0edd0f5ef27603cdb8cc0a683df331", + "target_commit": "ae751835b8dfca06801cc41da4eed05819c5bf14", + "source_parents": [ + "595bf8a14f649422e2d316fa913e3f15a728e231" + ], + "target_parents": [ + "90052ecf06dcaa59faf97a55894846b7e8c418b1" + ], + "source_subject": "wasm: harden support coverage", + "target_subject": "wasm: harden support coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T07:28:26Z", + "target_author_time": "2026-06-23T07:28:26Z", + "source_committer_time": "2026-06-23T07:28:26Z", + "target_committer_time": "2026-06-23T07:28:26Z", + "source_paths": [ + "crates/sql_wasm_runtime/src/lib.rs" + ], + "normalized_patch_sha256": "1436083ff49d1aabdfb567c051affd7920ec61adbce304609a3e797d91d9d0cf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ab124d4a7476f5fd139504c32a77df0cdec51b54", + "target_commit": "f23567944d586dfd03b0831aef88a5cae0052c96", + "source_parents": [ + "b65f714457dd6eff1d6cb00b3828fc0c2aa12760" + ], + "target_parents": [ + "f451d26904e4a4a4d59ea9df244541b67b26f9ff" + ], + "source_subject": "transport: pass Reticulum preview policy to outbox", + "target_subject": "transport: pass Reticulum preview policy to outbox", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T08:50:29Z", + "target_author_time": "2026-07-07T08:50:29Z", + "source_committer_time": "2026-07-07T08:50:29Z", + "target_committer_time": "2026-07-07T08:50:29Z", + "source_paths": [ + "crates/sdk/src/workflow_runtime.rs" + ], + "normalized_patch_sha256": "4b7a6904f6bb8b1bd524669e856f000aad7d547ffcdc08e45c65df11729a3489", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ab61f7abfbba4660818d29f821fd6c522e7144df", + "target_commit": "f60ccb9e3d7be2b69754049152d0e85b40d44873", + "source_parents": [ + "d014f5bb17528ed57be88ba0d5439aee0f2271b3" + ], + "target_parents": [ + "9ece927a101214cf2045a3b4cb680430f0cbc4ec" + ], + "source_subject": "sdk: harden order revision status preflight", + "target_subject": "sdk: harden order revision status preflight", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T01:42:54-07:00", + "target_author_time": "2026-06-19T01:42:54-07:00", + "source_committer_time": "2026-06-19T01:42:54-07:00", + "target_committer_time": "2026-06-19T01:42:54-07:00", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "422166270fbf1a5aa79ba5859f79f9e3ff8ca43088948fc8adcd732f62d9683e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "abccef4bc45a47e357dd2841b2111db638055cee", + "target_commit": "cecaf705e888815821007710503389c2fe357883", + "source_parents": [ + "010b90e6a19b30b257ffe7ebb91f284f387ec185" + ], + "target_parents": [ + "2b2e8ee930ae5ec78f2bc960e2b099926ccac6b4" + ], + "source_subject": "bindings: align dto source-root generation", + "target_subject": "bindings: align dto source-root generation", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-27T10:25:27Z", + "target_author_time": "2026-06-27T10:25:27Z", + "source_committer_time": "2026-06-27T10:25:27Z", + "target_committer_time": "2026-06-27T10:25:27Z", + "source_paths": [ + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "b0b33bd732a7446496a3c65d42125aee9158788b104d18e9eb8c699c617a7c12", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "aca99c1fbf80dfa923ccaac4c353bf4d24de4419", + "target_commit": "a1f0387c02a864495b0f9cce4a0e585df03ba3ea", + "source_parents": [ + "6ba234971f8f70f37fd165101f6d0fc9c0b232f1" + ], + "target_parents": [ + "3789cc607e3f2ad84f9cd1d03f4b1332f31152cd" + ], + "source_subject": "sdk: add product outbox push API", + "target_subject": "sdk: add product outbox push API", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T16:57:03-07:00", + "target_author_time": "2026-06-15T16:57:03-07:00", + "source_committer_time": "2026-06-15T16:57:03-07:00", + "target_committer_time": "2026-06-15T16:57:03-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "be7a47a39641505917eb522a24bf79635b6c820690aa9ca6c404153f7c593622", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ad249fe5b25a4f782e944019b165922363983295", + "target_commit": "b9306511817a8abf8d560c30db4d1e7cac8866cd", + "source_parents": [ + "6d68bdd73aca544d0d4cc88f785b38a22d4b85d6" + ], + "target_parents": [ + "c46b39be5ee7517f238ad8de29853fd3296eae3b" + ], + "source_subject": "release: qualify front-door supply chains", + "target_subject": "release: qualify front-door supply chains", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T00:06:25Z", + "target_author_time": "2026-08-04T00:06:25Z", + "source_committer_time": "2026-08-04T00:06:25Z", + "target_committer_time": "2026-08-04T00:06:25Z", + "source_paths": [ + "tools/xtask/src/main.rs", + "tools/xtask/src/supply_chain_qualification.rs" + ], + "normalized_patch_sha256": "d0a44509023857d48884e8ac03dd7c23694784a8c31e1fd7da4b0b24fa67ae35", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ad2543821b705477956180e7600d05958069c2ee", + "target_commit": "6624524c834a20482e5aed7153c9f727f40a0c00", + "source_parents": [ + "aabb944db75811f33436084e98778729be8d844f" + ], + "target_parents": [ + "4267774b0f66efc51920512bb6a7671d33f4a82f" + ], + "source_subject": "sdk: remove proxy relay bridge", + "target_subject": "sdk: remove proxy relay bridge", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T01:01:38Z", + "target_author_time": "2026-07-08T01:01:38Z", + "source_committer_time": "2026-07-08T01:01:38Z", + "target_committer_time": "2026-07-08T01:01:38Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "63b39b6f02ce4cd42a16d76ec803ea5e7250e78f72b36d0cbc6728281602a7c9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "add9743dae423065a7e84328993eafa78669090c", + "target_commit": "a528f0fee4606c62ebaa15698dcbc6b5c9ca41a3", + "source_parents": [ + "a778f2d58f4f05c09d281e43d25eaf64eb774421" + ], + "target_parents": [ + "683b7e5d5359fa39348f79496326ac14b2d68170" + ], + "source_subject": "sdk: add transport profile API", + "target_subject": "sdk: add transport profile API", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T22:27:56Z", + "target_author_time": "2026-07-06T22:27:56Z", + "source_committer_time": "2026-07-06T22:27:56Z", + "target_committer_time": "2026-07-06T22:27:56Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/relay_targets_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "5a2590c989dd120ba35fff6979ddbce5dd6ae843e1adab2b980a3194701a8564", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ae53d4a6a319b553e942298d86aee0846e908d11", + "target_commit": "1ad0ee8ee58845c5f538311214934448157467d8", + "source_parents": [ + "9883c1c0e7139d2552f21457f8819f14ca3467a3" + ], + "target_parents": [ + "87a54fd93c7c4d62b008acba73c51ee0d78f41ff" + ], + "source_subject": "sdk: align trade feature surface", + "target_subject": "sdk: align trade feature surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T21:12:28Z", + "target_author_time": "2026-06-30T21:12:28Z", + "source_committer_time": "2026-06-30T21:12:28Z", + "target_committer_time": "2026-06-30T21:12:28Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/relay_targets.rs" + ], + "normalized_patch_sha256": "075afc5295e9d2238782b43b60dec8dfc5eaffe8c487c7ea24a093339585dc85", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ae89b618e4ef6ce1facdc84361f7c788fe05073c", + "target_commit": "793585c96105fa188a06b5009d94244c538db7de", + "source_parents": [ + "42c1d200a8ebdaad0cbb67b2066ac924dc6a612a" + ], + "target_parents": [ + "6e6966629e309273891a98b2f6a134bf03a424c0" + ], + "source_subject": "packages: smoke test packed installs", + "target_subject": "packages: smoke test packed installs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T06:22:55Z", + "target_author_time": "2026-06-28T06:22:55Z", + "source_committer_time": "2026-06-28T06:22:55Z", + "target_committer_time": "2026-06-28T06:22:55Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "9deca69d8d80ca644d69de20de33c8d97955e6584c4c1bb879e6a71b53d78b5a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "af0c8117762914a6009c54677a100948d69f2ce2", + "target_commit": null, + "source_parents": [ + "cad86c7fe70afcae238faf89dc1b4485d53ca6d2" + ], + "target_parents": [], + "source_subject": "feat(replica-db-schema-bindings): generate schema package", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:23:23-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:23:23-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "af1ebd63d35c374c1ee97e7a32fe45063b6dde5e", + "target_commit": null, + "source_parents": [ + "0925f5849cc7af3dc70b8827f24187b57e0b24bc" + ], + "target_parents": [], + "source_subject": "nostr-connect: normalize URI, method, and permission types", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-31T17:23:14Z", + "target_author_time": null, + "source_committer_time": "2026-07-31T17:23:14Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "afaa2f2853bce4fed6c45e9902a2b35a5c04fcac", + "target_commit": "3d534dcd4faf0042c7b538a9bb1d7ec0db3e9507", + "source_parents": [ + "7d94a98261a1507a581c5ed6da1920f5b45e6dd2" + ], + "target_parents": [ + "7faa57a39a16459e6bcd598d63ce900a414aa1bb" + ], + "source_subject": "crate-surface: cover replica store update declarations", + "target_subject": "crate-surface: cover replica store update declarations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-12T01:39:07Z", + "target_author_time": "2026-07-12T01:39:07Z", + "source_committer_time": "2026-07-12T01:39:07Z", + "target_committer_time": "2026-07-12T01:39:07Z", + "source_paths": [ + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "7eca61272682ba45bae5cdc0f3cd5acbb7102876d30c0da8822a83a5904980d0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b04b893574acdd734d4a42c361ae09e627a05798", + "target_commit": "ee970784ddafd6ae6054360df004d1547cbe9409", + "source_parents": [ + "34281da7f4d67aae7c6cc8508ce784c5ad8e04ed" + ], + "target_parents": [ + "d73d4647f35616987581e5bd9181cc5a766ec5c4" + ], + "source_subject": "transport: validate proxy explicit targets locally", + "target_subject": "transport: validate proxy explicit targets locally", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T22:26:42Z", + "target_author_time": "2026-07-07T22:26:42Z", + "source_committer_time": "2026-07-07T22:26:42Z", + "target_committer_time": "2026-07-07T22:26:42Z", + "source_paths": [ + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs" + ], + "normalized_patch_sha256": "6888ce5c782f0a4e434526d35155de227a2be0ae56f9b669970fb8537deb9188", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b0b478041758ff997c99b45f682ffa7e9681adc3", + "target_commit": "a8e30341d98ae4b1e8ee17804fbe44e16afef697", + "source_parents": [ + "9385ccaf0aee19357a45dc666886e49d029b83c7" + ], + "target_parents": [ + "b55f9190d9381e1810f27d9a4eb9fdd963e36a34" + ], + "source_subject": "event-codec: migrate workspace consumers", + "target_subject": "event-codec: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T04:21:14Z", + "target_author_time": "2026-07-30T04:21:14Z", + "source_committer_time": "2026-07-30T04:21:14Z", + "target_committer_time": "2026-07-30T04:21:14Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/tests/replica_ingest.rs" + ], + "normalized_patch_sha256": "f3015634270ac3f14661cf97c4d760016b32f1bfc9ff699c27aa20f82c420cd2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b21032732edde890590cb3b86f2a7aa7cb252a9a", + "target_commit": "94ca83e4764520d260c81e2ebee2512f8747ec30", + "source_parents": [ + "6a4f55616684d0b595ef1e0479abfd8ab2152692" + ], + "target_parents": [ + "81fb655abd071eac6c6ebe2aa085cfa3eab04423" + ], + "source_subject": "sdk: document public api contract", + "target_subject": "sdk: document public api contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:21:45Z", + "target_author_time": "2026-08-03T12:21:45Z", + "source_committer_time": "2026-08-03T12:21:45Z", + "target_committer_time": "2026-08-03T12:21:45Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README.md", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/safe_memory_client.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/examples/transport_profile.rs", + "crates/sdk/src/lib.rs" + ], + "normalized_patch_sha256": "0551602e0860acdfc8a19fded7381826a2cfce04424cc88abb853ca304ad7adb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b243d14ee590daf5a50d52ef9309f80eb7f47f88", + "target_commit": "5843fe3d2f22eff03d62dc7810cd71396a4404cc", + "source_parents": [ + "db11230b73941a38b34295fc0d1496a220e45f87" + ], + "target_parents": [ + "d4e910c321e256c8cc035243eaf312eebc047bfd" + ], + "source_subject": "dto: close core registry binding output", + "target_subject": "dto: close core registry binding output", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T06:58:06Z", + "target_author_time": "2026-06-24T06:58:06Z", + "source_committer_time": "2026-06-24T06:58:06Z", + "target_committer_time": "2026-06-24T06:58:06Z", + "source_paths": [ + "crates/core_bindings/Cargo.toml", + "crates/core_bindings/src/lib.rs", + "tools/xtask/src/dto_render.rs" + ], + "normalized_patch_sha256": "db41ba331fb4cd6a40f60406187ed8a75d998a074c07e99cea7a59aadf002c1d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b282947cc09853534f4b2e6243f9b51cbe1ccd34", + "target_commit": null, + "source_parents": [ + "fd8384aee348034e0c8ea17a868fe7f094770050" + ], + "target_parents": [], + "source_subject": "docs: add crates release v1 specification", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-27T07:06:32Z", + "target_author_time": null, + "source_committer_time": "2026-07-27T07:06:32Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "b2a71062a33c1aad550e92c755f8e56233423e34", + "target_commit": "37db737c454043f69bdc14d4b5325b98698df2ae", + "source_parents": [ + "d21b1983b4419553c43eaf1d4fb2ba56e668b847" + ], + "target_parents": [ + "bd4145ee8ac6b9e810d6b09bc95fd2a1538568fc" + ], + "source_subject": "sdk: add local runtime feature", + "target_subject": "sdk: add local runtime feature", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T12:51:27-07:00", + "target_author_time": "2026-06-18T12:51:27-07:00", + "source_committer_time": "2026-06-18T12:51:27-07:00", + "target_committer_time": "2026-06-18T12:51:27-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/src/lib.rs" + ], + "normalized_patch_sha256": "6557e6e9a6c57e5364697b6633f5c49e11a11235f7a44e2e9de613acf80830c6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b65f714457dd6eff1d6cb00b3828fc0c2aa12760", + "target_commit": "f451d26904e4a4a4d59ea9df244541b67b26f9ff", + "source_parents": [ + "52e1cfff74b8d7720d5bac84bc2367cc8c5edee4" + ], + "target_parents": [ + "e9d27715b40ec1b719d0f2e8531928a12ba3b7d7" + ], + "source_subject": "transport: reject custom Reticulum preview targets", + "target_subject": "transport: reject custom Reticulum preview targets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T08:33:50Z", + "target_author_time": "2026-07-07T08:33:50Z", + "source_committer_time": "2026-07-07T08:33:50Z", + "target_committer_time": "2026-07-07T08:33:50Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "e12f4214a3221adfaf8b81101542bc74c94254841050718f6fb0afb518cdbc9c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b6ce7f66f838ecb89e3a1d40c376d1017b858512", + "target_commit": null, + "source_parents": [ + "7934096d1848692a0485e2c08ceb9dc567f004f0" + ], + "target_parents": [], + "source_subject": "architecture: retire resolved identity API exceptions", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-27T18:35:32Z", + "target_author_time": null, + "source_committer_time": "2026-07-27T18:35:32Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "b795ace3873c95024d11473cb9d8912b8a6b5a20", + "target_commit": "89577853c46e58a4f0d519bb7612b42802a2b279", + "source_parents": [ + "762ca4d6270bda5855adf5d22e00abb5411ee456" + ], + "target_parents": [ + "bface7702c4c08216bcca1dadd689dfb94a2dca3" + ], + "source_subject": "transport: align Reticulum preview SDK defaults", + "target_subject": "transport: align Reticulum preview SDK defaults", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T05:35:40Z", + "target_author_time": "2026-07-07T05:35:40Z", + "source_committer_time": "2026-07-07T05:35:40Z", + "target_committer_time": "2026-07-07T05:35:40Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "08e547c2ce049236eafdf261ecdfd490ebc59229259787d7a89fbd3dbb3f3bff", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b7a74355693c40926bf6a7840fb113d2c77d2529", + "target_commit": "273a24aafa6211b0fa60bc88647d4a57e0edc2b8", + "source_parents": [ + "1d05399c5eb1097620dfb44c87824e803f37fdd6" + ], + "target_parents": [ + "3166eee6d9f4c71916ee6db9bc49cbaac6139538" + ], + "source_subject": "transport: expose neutral sync status", + "target_subject": "transport: expose neutral sync status", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T07:10:23Z", + "target_author_time": "2026-07-08T07:10:23Z", + "source_committer_time": "2026-07-08T07:10:23Z", + "target_committer_time": "2026-07-08T07:10:23Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "b0d58b87e3cf132cc2bc49f48763e2afe1db4701688af622b8559af6dc6ee11e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b7cf74e494e703864fefd4a873b1177341c31721", + "target_commit": "633eaeabdb22538b6f52e8b6130381c7d0ead03c", + "source_parents": [ + "d6f332bfde2eed006a3de72f0105d6816d676737" + ], + "target_parents": [ + "a8b91eaa9a864e7bc78901ff08e4c93ca4491755" + ], + "source_subject": "sdk: complete package conformance coverage", + "target_subject": "sdk: complete package conformance coverage", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:09:28Z", + "target_author_time": "2026-08-03T12:09:28Z", + "source_committer_time": "2026-08-03T12:09:28Z", + "target_committer_time": "2026-08-03T12:09:28Z", + "source_paths": [ + "crates/sdk/tests/package_boundary.rs", + "crates/sdk/tests/public_api.rs", + "tools/xtask/src/check.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "daddc049fd06a588ad6815d4acb48cf27da2c291fdb2499fa428417672f94df9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b7d90cdf61e4dbb31c6ed3f72df9e450827dabef", + "target_commit": "9b92a55a4dcfb91d91aa41d4cd347c27dc98e48d", + "source_parents": [ + "59b26c73bab504495c77496a752351ac9075b516" + ], + "target_parents": [ + "acfee770ccadad142cd6ba183db48740d2e222e8" + ], + "source_subject": "feat(release): derive publication graph", + "target_subject": "feat(release): derive publication graph", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T11:07:47Z", + "target_author_time": "2026-08-04T11:07:47Z", + "source_committer_time": "2026-08-04T11:07:47Z", + "target_committer_time": "2026-08-04T11:07:47Z", + "source_paths": [ + "tools/xtask/src/main.rs", + "tools/xtask/src/release_graph.rs" + ], + "normalized_patch_sha256": "d4d70bcc267521877124227dcbc03ded14320334af9dfb0dec5db94acd2c600a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b81acdfee8c5ac031641b2e0f2a6f5fd801a2d9f", + "target_commit": "86c6fa3219fa158b888a01c50d3f8ecf5541bf0c", + "source_parents": [ + "80b30f33daab6e32a07edcd4bbc856d1778cff38" + ], + "target_parents": [ + "e07290ddb2458f047f4e86041ee01fddc7a62e79" + ], + "source_subject": "trade: make propose request product-shaped", + "target_subject": "trade: make propose request product-shaped", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-04T01:01:24Z", + "target_author_time": "2026-07-04T01:01:24Z", + "source_committer_time": "2026-07-04T01:01:24Z", + "target_committer_time": "2026-07-04T01:01:24Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs" + ], + "normalized_patch_sha256": "be6a2a2c4be779b2912cb0bd895353d8fb1e0c4d411712e09c1c1c150e85c41a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b852798e0436e6dadfb5b9c7f0fc44e732046b95", + "target_commit": "3d335907b7a54e8129b74e7032b80553aea852cf", + "source_parents": [ + "bad27bc74c7302178e5899702887f0cc56a7d73f" + ], + "target_parents": [ + "47123165cb2c4c47a99e7d6afebefb70e7e32689" + ], + "source_subject": "sdk: add order workflow command metadata", + "target_subject": "sdk: add order workflow command metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T15:32:05-07:00", + "target_author_time": "2026-06-19T15:32:05-07:00", + "source_committer_time": "2026-06-19T15:32:05-07:00", + "target_committer_time": "2026-06-19T15:32:05-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "3a0dec868efcbc8bccce39dfa433f2d74305f3e065aa069f8bdd4be541655ad6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b8838cb197532e955d77521a1779f3b659033e6c", + "target_commit": "0ce314697a6e87c6434d44e785ab7fac352348af", + "source_parents": [ + "7cabf6f3f13679e81e036495cef75ef0d411cd92" + ], + "target_parents": [ + "fa6802ef6800a3e6cddda266b71fa48598ca3e16" + ], + "source_subject": "sdk: assert canonical storage tables", + "target_subject": "sdk: assert canonical storage tables", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T08:41:40Z", + "target_author_time": "2026-06-26T08:41:40Z", + "source_committer_time": "2026-06-26T08:41:40Z", + "target_committer_time": "2026-06-26T08:41:40Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "d0ecd51395c8cc7f8de56a4edff96fc167344b9b49ec6777a723c7b277ce81f5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "b89497727278225eb12f243eca82b77affe5d929", + "target_commit": "fedbccb3bdff2d61d721b9db3119182189ecf658", + "source_parents": [ + "c0620692b8646009d3dd1a8c1123e45c8b8b37a0" + ], + "target_parents": [ + "037cf89daabbe3b9314d9f631e5bf84c25b792d9" + ], + "source_subject": "contracts: align TypeScript package matrix", + "target_subject": "contracts: align TypeScript package matrix", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T04:00:36Z", + "target_author_time": "2026-06-28T04:00:36Z", + "source_committer_time": "2026-06-28T04:00:36Z", + "target_committer_time": "2026-06-28T04:00:36Z", + "source_paths": [ + "tools/xtask/src/contracts.rs" + ], + "normalized_patch_sha256": "965f22ecb2d058e22b0f0adea7c7fd7c3bfc5a617944c33ad54483736cb5c257", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bad27bc74c7302178e5899702887f0cc56a7d73f", + "target_commit": "47123165cb2c4c47a99e7d6afebefb70e7e32689", + "source_parents": [ + "df357dc03e99dde2a4fe6a24bcb9471358f52b06" + ], + "target_parents": [ + "b713f197804d7aaebdfb739a1c439c34970a7896" + ], + "source_subject": "tests: guard order SDK runtime surface", + "target_subject": "tests: guard order SDK runtime surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T15:27:34-07:00", + "target_author_time": "2026-06-19T15:27:34-07:00", + "source_committer_time": "2026-06-19T15:27:34-07:00", + "target_committer_time": "2026-06-19T15:27:34-07:00", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "8dacde54606d97b80ca9621ae6eb9353e196e9109e34f0614d11824397085eed", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "baf6f683acd0c16a2f3a53be85f6fa1e67cf83eb", + "target_commit": "731029342394b7253a20818d07806f06a7c68f29", + "source_parents": [ + "71dc0de57ddc96754f291a6111e9e3dcc9e31234" + ], + "target_parents": [ + "c63dae0f1db19f4662f758f4f9bc7cd025af23d3" + ], + "source_subject": "runtime: add trade command runtime", + "target_subject": "runtime: add trade command runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T12:20:41Z", + "target_author_time": "2026-07-16T12:20:41Z", + "source_committer_time": "2026-07-16T12:20:41Z", + "target_committer_time": "2026-07-16T12:20:41Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs" + ], + "normalized_patch_sha256": "23a3ff6850dd4705959fca7ceaf7b4705195b3550f16e33b20adc1c5fe80b6e7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bbcb47bc576dbdb25a9c68c387aebcfd021941f3", + "target_commit": "8cb1c8788c6e28af22e1d10cb31d613340f75842", + "source_parents": [ + "2932428d1fd3aab889b3044ba98e7e0843a58956" + ], + "target_parents": [ + "4232c5e9c6dca190027ed09be88d56f9d3d34468" + ], + "source_subject": "coverage: add sdk coverage command", + "target_subject": "coverage: add sdk coverage command", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T01:41:58Z", + "target_author_time": "2026-06-23T01:41:58Z", + "source_committer_time": "2026-06-23T01:41:58Z", + "target_committer_time": "2026-06-23T01:41:58Z", + "source_paths": [ + "tools/xtask/src/coverage.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "a4fe6438ad640fb02362af31d1d8aeb302f9912ec71d6465538e317f87a1a233", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bc2a71c3a1c52c268ffd404b967c53253936ef65", + "target_commit": "b789991c7292870652bef42d15d54172d29dd691", + "source_parents": [ + "f7ba91188d3bf4266f6db72bb50c51d03ef3c99c" + ], + "target_parents": [ + "dffb7e0f1387d297ae6a41db0a807448308a9f20" + ], + "source_subject": "refactor: align core bindings", + "target_subject": "refactor: align core bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T14:09:03-07:00", + "target_author_time": "2026-06-11T14:09:03-07:00", + "source_committer_time": "2026-06-11T14:09:03-07:00", + "target_committer_time": "2026-06-11T14:09:03-07:00", + "source_paths": [ + "crates/core_bindings/Cargo.toml", + "crates/core_bindings/src/lib.rs", + "crates/core_bindings/src/typescript/types.ts", + "crates/identity_bindings/Cargo.toml", + "crates/identity_bindings/src/lib.rs", + "crates/identity_bindings/src/typescript/constants.ts" + ], + "normalized_patch_sha256": "e8541d167ca5768fc61d86c52f698c9dfd54c400a91d2bf88752cbc6b1dfdbc5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bc9de9e176233b58a46261de2be9b66b1109fda7", + "target_commit": "f5e7b656ce9d3dac3bc8281ee09088cc5c7a1a81", + "source_parents": [ + "011e4137dc62321e76ad03e842c6463ede1071fa" + ], + "target_parents": [ + "e0e4f14d2db5e35027235b7f5c4095884bce420d" + ], + "source_subject": "status: add root-specific trade selectors", + "target_subject": "status: add root-specific trade selectors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T02:54:22Z", + "target_author_time": "2026-07-05T02:54:22Z", + "source_committer_time": "2026-07-05T02:54:22Z", + "target_committer_time": "2026-07-05T02:54:22Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "9457b73815931059b63dcc31700f04c6de00ed5dab16bd71a6ad474bb1a6a78b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bd39b52795651ade71acb7e71f9740cb02c32627", + "target_commit": "8adbade4c805ecd326aa5e2ebcd9a171eddf8b19", + "source_parents": [ + "349dcd489e63463be346566e788c0750a92d5dbe" + ], + "target_parents": [ + "cc8fde0343779e72dba4fde685cb5758edf73a57" + ], + "source_subject": "status: add branch-aware trade evidence sources", + "target_subject": "status: add branch-aware trade evidence sources", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-03T23:00:09Z", + "target_author_time": "2026-07-03T23:00:09Z", + "source_committer_time": "2026-07-03T23:00:09Z", + "target_committer_time": "2026-07-03T23:00:09Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "fab508920c8641c9f39bb1cd8e5e3dd746658cc73a0ead202757bc8b8da7de8c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "be22167ee5a1a3f4b93a8f892acbec76d34cf48e", + "target_commit": "50305905ff42137bc9a56c0cfb8f3a0963c43a09", + "source_parents": [ + "a00c36a1b2d5ed269878b954041331e328b92d55" + ], + "target_parents": [ + "4476586cc10a6f1b1ee58f271991a6be765c791d" + ], + "source_subject": "identity: migrate workspace consumers", + "target_subject": "identity: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T05:50:15Z", + "target_author_time": "2026-07-28T05:50:15Z", + "source_committer_time": "2026-07-28T05:50:15Z", + "target_committer_time": "2026-07-28T05:50:15Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/identity_bindings/src/lib.rs", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/README.md", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/identity.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/identity_public_api.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/identity_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "70e236f05d6ecb52e22fa2615cf77b1d3933539f8b0194bf328638bed9a21bb1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "be6b479c8a62cb25b89791eaa7b0966b11603051", + "target_commit": "682509146a99903c5eccb39e88607223f165de94", + "source_parents": [ + "18346a27d3db11dc82b5020d239e89ff5837f8b0" + ], + "target_parents": [ + "90cc25893d2105fed3ca142958e4b15fb72cc22c" + ], + "source_subject": "facade: forbid a public radroots::sdk namespace", + "target_subject": "facade: forbid a public radroots::sdk namespace", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:56:35Z", + "target_author_time": "2026-08-03T12:56:35Z", + "source_committer_time": "2026-08-03T12:56:35Z", + "target_committer_time": "2026-08-03T12:56:35Z", + "source_paths": [ + "crates/radroots/README.md", + "crates/radroots/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "f443a3b27190f8f4079b8afbdbd7192fe2e6c3b453a6c5e1da4a22b2563ec325", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "bffc3d0fe3ef8c541edacfa42daa3ab81e477641", + "target_commit": "111f143c5007830710b8f895ae62ab778c22c79e", + "source_parents": [ + "89b28cff0055c0bd7e92a021d077edff9214db38" + ], + "target_parents": [ + "cebd246ea7b45b615200b47fb72a23a3821dfe96" + ], + "source_subject": "runtime: consolidate sdk storage transactions", + "target_subject": "runtime: consolidate sdk storage transactions", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T11:52:34Z", + "target_author_time": "2026-07-15T11:52:34Z", + "source_committer_time": "2026-07-15T11:52:34Z", + "target_committer_time": "2026-07-15T11:52:34Z", + "source_paths": [ + "crates/event_bindings/src/model.rs", + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/studio_store.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "e88bd5a3e47e523987e665c5551674ae0b43501fbb171a7d2d2d9e899cef5842", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c0620692b8646009d3dd1a8c1123e45c8b8b37a0", + "target_commit": "037cf89daabbe3b9314d9f631e5bf84c25b792d9", + "source_parents": [ + "7ffe6708630f37f8cfc7ece959b4c468e524daeb" + ], + "target_parents": [ + "a05eefdd45d08fcf15564813b8d734d80575cc87" + ], + "source_subject": "packages: include npm distribution metadata", + "target_subject": "packages: include npm distribution metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T02:32:34Z", + "target_author_time": "2026-06-28T02:32:34Z", + "source_committer_time": "2026-06-28T02:32:34Z", + "target_committer_time": "2026-06-28T02:32:34Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "4ae797c0ee403741980c8afa9f38c79a21e7109d651c0d2831a11f6ec6e080fa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c06fefe03f3bdb7f0e24b1027f65f18b3397e2c4", + "target_commit": "3273f6cd82edd0279ce186e97aa3fd7c76777ea9", + "source_parents": [ + "fafa24eb55ced4742c43128d40fb559364a063dc" + ], + "target_parents": [ + "c79b55935b72922d9ca8790dbda1ea6c2e08f2dc" + ], + "source_subject": "runtime: cover storage edge branches", + "target_subject": "runtime: cover storage edge branches", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T02:49:36Z", + "target_author_time": "2026-06-23T02:49:36Z", + "source_committer_time": "2026-06-23T02:49:36Z", + "target_committer_time": "2026-06-23T02:49:36Z", + "source_paths": [ + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "e7495cf57f3075eb2e365094d5845211481f7487a1b66834732b658dd84479be", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c0b741eb478814dbbeba63fd7c27f61a9f1336b7", + "target_commit": "4bfe8c0db49f3313146ff6f1c923fd2e9faeb0e2", + "source_parents": [ + "55eee4919fe3a7294bcfb2d13044e004f68d915d" + ], + "target_parents": [ + "685e492a97c05709849a6638a3a062a8589f584a" + ], + "source_subject": "Generate CLI host bindings from runtime contract", + "target_subject": "Generate CLI host bindings from runtime contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T19:21:51Z", + "target_author_time": "2026-07-15T19:21:51Z", + "source_committer_time": "2026-07-15T19:21:51Z", + "target_committer_time": "2026-07-15T19:21:51Z", + "source_paths": [ + "tools/xtask/src/check.rs", + "tools/xtask/src/cli_host.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "3622ee6950cda507878e716ae585dec2b5bb39365c263c6c360120365a6c91ce", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c115963d95b5b239ce6ecd4511ee3f67ffb92708", + "target_commit": "575fc38393473a20587457b59689e57824cec057", + "source_parents": [ + "c866c00b0fc3e591d9f32d176c606ba7b9e54fd3" + ], + "target_parents": [ + "7364cd90f8c1760f1a8cafd72528e1bf8b8798ad" + ], + "source_subject": "sdk: consume the opaque local Nostr signer", + "target_subject": "sdk: consume the opaque local Nostr signer", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T10:57:58Z", + "target_author_time": "2026-07-31T10:57:58Z", + "source_committer_time": "2026-07-31T10:57:58Z", + "target_committer_time": "2026-07-31T10:57:58Z", + "source_paths": [ + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs" + ], + "normalized_patch_sha256": "def2c09f1a472d6a6725cbf5cedc03891b492e23b6898cf86cab676c491d1bdf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c178b9495109c530abea18e0d489ae2e57d3da7a", + "target_commit": "d581dedb72e8293bd34929c119590f1081327328", + "source_parents": [ + "0477bfd5b0cd2d6f0e6a54e7325c6dad06cd9ea6" + ], + "target_parents": [ + "bbac9fc9c3985ebcb4bc76b34501003caf582a87" + ], + "source_subject": "docs: clarify trade feature split", + "target_subject": "docs: clarify trade feature split", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T21:17:58Z", + "target_author_time": "2026-06-30T21:17:58Z", + "source_committer_time": "2026-06-30T21:17:58Z", + "target_committer_time": "2026-06-30T21:17:58Z", + "source_paths": [ + "crates/sdk/README" + ], + "normalized_patch_sha256": "46505bd735427cfddbf81f2501f74c99b4f0116f3e3b33743d4918bdbf87ca4b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c21b66f4402b934f52d73d8720675d264672ed07", + "target_commit": null, + "source_parents": [ + "012f64d4180fdc68108d53933ea8eaa1251cec2e" + ], + "target_parents": [], + "source_subject": "docs: normalize coverage amendment", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-28T18:02:05Z", + "target_author_time": null, + "source_committer_time": "2026-07-28T18:02:05Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "c2ae351210bb0e9b499922a9f234b86221125b45", + "target_commit": "c615d7d133a46be3bdfaa0405d1372d4f6a231da", + "source_parents": [ + "1f51e9c575cefc3897e07c53a04d632be1d630bd" + ], + "target_parents": [ + "b7015b3ab3ad470e115d88475b9233c95717d7c4" + ], + "source_subject": "packages: require built dist exports", + "target_subject": "packages: require built dist exports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T04:05:38Z", + "target_author_time": "2026-06-28T04:05:38Z", + "source_committer_time": "2026-06-28T04:05:38Z", + "target_committer_time": "2026-06-28T04:05:38Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "d72b5d237ee9364b3d43c07a045d7824bbfd8a012023acd04b07566d80fe5626", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c2df5e5aea253771cf9b00e2f29d55e22099f94b", + "target_commit": "ff49f82f8fbbffe7d1ec456693fe1f2320bdd628", + "source_parents": [ + "f31cb78b0f3c74f7c85a7613eea380e5d49abea4" + ], + "target_parents": [ + "983667ec44193c8cc952f1e0a3423ae9f33ba74b" + ], + "source_subject": "dto: render events indexed bindings", + "target_subject": "dto: render events indexed bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T07:53:27Z", + "target_author_time": "2026-06-24T07:53:27Z", + "source_committer_time": "2026-06-24T07:53:27Z", + "target_committer_time": "2026-06-24T07:53:27Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "c326134254f234c1764b03d5ee11ecca35e291da71617b8d3f51768ad2076298", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c2e9bcc528248d541365978c3668ed2167062d8f", + "target_commit": "6056fddcd037d07e0130b6b4ad3b03153b186d2a", + "source_parents": [ + "5a8c47f2a43bed9e0d8e41cc9787287b8f9944f9" + ], + "target_parents": [ + "acf36b2f39f4cc44b9b01519d99c31bd7a5d68e6" + ], + "source_subject": "sdk: adopt organized trade surface", + "target_subject": "sdk: adopt organized trade surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-30T07:52:06Z", + "target_author_time": "2026-07-30T07:52:06Z", + "source_committer_time": "2026-07-30T07:52:06Z", + "target_committer_time": "2026-07-30T07:52:06Z", + "source_paths": [ + "crates/sdk/src/private_store.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs" + ], + "normalized_patch_sha256": "486addc667f8ad028915b6b47e8b8f80bce02d485f03872ee9d0d1696e67c346", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c320c4585c270afc31c3c93d4f5606ca9e5565ea", + "target_commit": "0b64195fcb5e3df40dcfc76e2356694b48302992", + "source_parents": [ + "76fe02a613a024605e2a89e9c3240d271d2d1394" + ], + "target_parents": [ + "c774e8e1ed68e7232177e464efc65770e477ba46" + ], + "source_subject": "transport: remove SDK transport aliases", + "target_subject": "transport: remove SDK transport aliases", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T19:15:52Z", + "target_author_time": "2026-07-10T19:15:52Z", + "source_committer_time": "2026-07-10T19:15:52Z", + "target_committer_time": "2026-07-10T19:15:52Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "aec32d9a0c8f6f4e780e2665d4d6b759bfe1c67c3519f513258102149f13bdb2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c340d53ade6ffa40347706d1956f94ed6bc42679", + "target_commit": "3d908d05c33bd949a1a173d644f14bd83973ae74", + "source_parents": [ + "af0c8117762914a6009c54677a100948d69f2ce2" + ], + "target_parents": [ + "00c041790515ce7e9a45ee8d8dd4d984f9b2d7d4" + ], + "source_subject": "feat(trade-bindings): generate trade package", + "target_subject": "feat(trade-bindings): generate trade package", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-11T06:25:01-07:00", + "target_author_time": "2026-06-11T06:25:01-07:00", + "source_committer_time": "2026-06-11T06:25:01-07:00", + "target_committer_time": "2026-06-11T06:25:01-07:00", + "source_paths": [ + "crates/trade_bindings/src/lib.rs" + ], + "normalized_patch_sha256": "27ee15c8c3c9c50ae1a8ee0259699fe58d97da58b97d2333e3e368aa88f5c776", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c359ef395697769fa7806e77454f7f2a7b122aa9", + "target_commit": "82652bba843c071732456cac79c4a73bc50676a2", + "source_parents": [ + "c54c08d1a494d854f002c5abcee3d0e9cb5d1da1" + ], + "target_parents": [ + "7ca15d89a73935522b22ab58ee9817411ac9c313" + ], + "source_subject": "sdk: route Nostr push through transport facade", + "target_subject": "sdk: route Nostr push through transport facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-11T08:01:18Z", + "target_author_time": "2026-07-11T08:01:18Z", + "source_committer_time": "2026-07-11T08:01:18Z", + "target_committer_time": "2026-07-11T08:01:18Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "d5abba6d4af5b3d558b0df35fb84bf2c5423426843b17b357dfbb8ad03171b6a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c473ea2400efba5aa003cff5bbeb1928de1872d1", + "target_commit": "8bc581e6809e7065beda32cb31fbb91e8d6bf68c", + "source_parents": [ + "956c5114bbccd03955060c274201bf54f54a2b11" + ], + "target_parents": [ + "965979b2252ee3090dd6d46e05bc1021ef4676f5" + ], + "source_subject": "refactor: retire predecessor sdk surfaces", + "target_subject": "refactor: retire predecessor sdk surfaces", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-04T15:20:20Z", + "target_author_time": "2026-08-04T15:20:20Z", + "source_committer_time": "2026-08-04T15:20:20Z", + "target_committer_time": "2026-08-04T15:20:20Z", + "source_paths": [ + "crates/radroots/README.md", + "crates/radroots/src/listing.rs", + "crates/sdk/README.md", + "crates/sdk/src/client.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/listing/operational_listing/draft.rs", + "crates/sdk/src/listing/operational_listing/mod.rs", + "crates/sdk/src/listing/operational_listing/model.rs", + "crates/sdk/src/listing/operational_listing/mutation.rs", + "crates/sdk/src/listing/operational_listing/price_ext.rs", + "crates/sdk/src/listing/operational_listing/validation.rs", + "crates/sdk/tests/package_boundary.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs", + "tools/xtask/src/cli_host.rs", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs" + ], + "normalized_patch_sha256": "7c7e59053e98c11a9c3ec33e851efe06979e40353ac4064d6e63ea9d9af4fc58", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c4f70690fd59bcd7497b72690016c3ad6ffbfc76", + "target_commit": "86a2bb50b5fd6087cbbeb19bfb764d6e3b408db0", + "source_parents": [ + "5485b48739a027ed245e5004047de3c8ded3f4ad" + ], + "target_parents": [ + "c781760ba4184409b4bd598721ccbafa47901b8f" + ], + "source_subject": "sdk: gate runtime example by feature", + "target_subject": "sdk: gate runtime example by feature", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T16:01:14-07:00", + "target_author_time": "2026-06-17T16:01:14-07:00", + "source_committer_time": "2026-06-17T16:01:14-07:00", + "target_committer_time": "2026-06-17T16:01:14-07:00", + "source_paths": [ + "crates/sdk/Cargo.toml" + ], + "normalized_patch_sha256": "8fd2fa7a27f79e8861164e2c9211bd48a51e7afbab902074c0a58e8ce262189f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c54c08d1a494d854f002c5abcee3d0e9cb5d1da1", + "target_commit": "7ca15d89a73935522b22ab58ee9817411ac9c313", + "source_parents": [ + "c320c4585c270afc31c3c93d4f5606ca9e5565ea" + ], + "target_parents": [ + "0b64195fcb5e3df40dcfc76e2356694b48302992" + ], + "source_subject": "sdk: align event API names", + "target_subject": "sdk: align event API names", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-11T06:49:03Z", + "target_author_time": "2026-07-11T06:49:03Z", + "source_committer_time": "2026-07-11T06:49:03Z", + "target_committer_time": "2026-07-11T06:49:03Z", + "source_paths": [ + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "ba29e6b4f586c7ac353ad4969cbb0641ab2635aa4b494039c40e56817512f291", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c57ea6bcf846ecccb8e4b8a8696a86e1a5c312ce", + "target_commit": "b9b574713837dcae18841849ec9262cf83f28d37", + "source_parents": [ + "59a6d373ca009c152a8afad482cd7de0e4d5cf1e" + ], + "target_parents": [ + "95b147d7db8ed8461f7d405c403c40b3fb3afc9a" + ], + "source_subject": "release: require path and version for public dependencies", + "target_subject": "release: require path and version for public dependencies", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T10:38:44Z", + "target_author_time": "2026-07-27T10:38:44Z", + "source_committer_time": "2026-07-27T10:38:44Z", + "target_committer_time": "2026-07-27T10:38:44Z", + "source_paths": [ + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "88d224d3bf57b13316085228499392d635c6c01e808b6c697ff6d886a4567086", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c5a99864adedac481944ab6a14a7ee9709acc2dd", + "target_commit": "943aa92dad222ee9f707ddc51dbd19acd6e87f93", + "source_parents": [ + "4a5997a2e48a2b082f277db8d6c135a16667614f" + ], + "target_parents": [ + "74e31d5d30a3e0b115b7657cb4d14bfc58dfc5c7" + ], + "source_subject": "sdk: remove public low-level trade mutation surface", + "target_subject": "sdk: remove public low-level trade mutation surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:57:05Z", + "target_author_time": "2026-06-30T03:57:05Z", + "source_committer_time": "2026-06-30T03:57:05Z", + "target_committer_time": "2026-06-30T03:57:05Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "5d047488d118e3bf3df4401b5e2e3c4c9d8f007836b57febf65da24821020c3f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c61a2289d2fab174168ffeb4192dad388075a5af", + "target_commit": "296fbbfad980580370d2f851b3f07544e50b5a75", + "source_parents": [ + "e668f030fc29584004b8df8215b3f16ffdf6d613" + ], + "target_parents": [ + "9eee1823f01dbc1579643d457804706bd8403264" + ], + "source_subject": "sdk: add trade product workflow clients", + "target_subject": "sdk: add trade product workflow clients", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:18:41Z", + "target_author_time": "2026-06-30T03:18:41Z", + "source_committer_time": "2026-06-30T03:18:41Z", + "target_committer_time": "2026-06-30T03:18:41Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "4bd79cd6bb7a946e421b812ee510f7eb6dee756cd4257f48b97c56c4fc5bb6f6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c7211babab84f138ee5569e313b04c6c8281cf8c", + "target_commit": "741c42a66bf32960176408e1a58af920add9b9c8", + "source_parents": [ + "fdf5e1bcbd9d6173408e949580112240d861bbbc" + ], + "target_parents": [ + "df5ed1acf9e5a2f854d57b420eded2bd2a450dcc" + ], + "source_subject": "knowledge-sdk: enforce claim citation rules", + "target_subject": "knowledge-sdk: enforce claim citation rules", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T01:37:23Z", + "target_author_time": "2026-07-06T01:37:23Z", + "source_committer_time": "2026-07-06T01:37:23Z", + "target_committer_time": "2026-07-06T01:37:23Z", + "source_paths": [ + "crates/sdk/src/knowledge.rs", + "crates/sdk/tests/knowledge_public_api.rs" + ], + "normalized_patch_sha256": "68de9f39f72da17b4fb9b318d3af95c843d6f65aa64849f154424c23ef4477bf", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c7c627d5d167106b876b0ce3ad34fa090143c61d", + "target_commit": "0d87b57798df35ea6f8e066f3bddf007573d4cc1", + "source_parents": [ + "650fc968e4a9ba68002c85470ef5fd33ca605121" + ], + "target_parents": [ + "a2f17aa6fcd58945c022a409f50c2ea27b2fcd3b" + ], + "source_subject": "sdk: document runtime publish posture", + "target_subject": "sdk: document runtime publish posture", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T17:34:59-07:00", + "target_author_time": "2026-06-15T17:34:59-07:00", + "source_committer_time": "2026-06-15T17:34:59-07:00", + "target_committer_time": "2026-06-15T17:34:59-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs" + ], + "normalized_patch_sha256": "11aeb3a74114671f372abf11ef449891354f49f35b1e68e8b613c2c9067e29f9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c866c00b0fc3e591d9f32d176c606ba7b9e54fd3", + "target_commit": "7364cd90f8c1760f1a8cafd72528e1bf8b8798ad", + "source_parents": [ + "c926ccad3662685142029431c48ef7b9b6013ed8" + ], + "target_parents": [ + "7bec72501c3253b75040097c68f478b12714789a" + ], + "source_subject": "nostr: remove live relay-client dependencies", + "target_subject": "nostr: remove live relay-client dependencies", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T09:03:21Z", + "target_author_time": "2026-07-31T09:03:21Z", + "source_committer_time": "2026-07-31T09:03:21Z", + "target_committer_time": "2026-07-31T09:03:21Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/adapters/nostr.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "5ef60b31ccc4c3b0f3441bbb6834577d6b23543b8d94feec25ee87a6427fe8de", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c926ccad3662685142029431c48ef7b9b6013ed8", + "target_commit": "7bec72501c3253b75040097c68f478b12714789a", + "source_parents": [ + "3edb09c0088a5d37d496515ee7cbbe1aadb57904" + ], + "target_parents": [ + "58fd4bb48ef0dd29fa3171d45607ce29f339b8e2" + ], + "source_subject": "transport: quarantine superseded package surface", + "target_subject": "transport: quarantine superseded package surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T06:25:21Z", + "target_author_time": "2026-07-31T06:25:21Z", + "source_committer_time": "2026-07-31T06:28:34Z", + "target_committer_time": "2026-07-31T06:28:34Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "29c131b5b842a60bdf20cfcfa3c1b5c77516eb1265c1899efb6eaeac40638e29", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "c9fde521b1640fe7ecbbdd6449a192d8f861f33d", + "target_commit": "ba9863edb2381b7971477ba3a8bd85cb41d2cb3b", + "source_parents": [ + "05dabaa5aef5b44c7979c33fe53cebea525c8eef" + ], + "target_parents": [ + "3aa5ea50bc6b7e313357fb243a266da70af8cc6b" + ], + "source_subject": "packages: normalize generated artifact metadata", + "target_subject": "packages: normalize generated artifact metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:24:18Z", + "target_author_time": "2026-08-03T13:24:18Z", + "source_committer_time": "2026-08-03T13:24:18Z", + "target_committer_time": "2026-08-03T13:24:18Z", + "source_paths": [ + "tools/xtask/src/manifest.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "a0c8df35ca652b1c8f1df6a04fc9673c16e332b1faac12b054afd6a780d72e4f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cad86c7fe70afcae238faf89dc1b4485d53ca6d2", + "target_commit": null, + "source_parents": [ + "508cd34459fbb4cf68d420600c7c7a0004b67f6d" + ], + "target_parents": [], + "source_subject": "feat(events-bindings): generate events package", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T06:22:05-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T06:22:05-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "cae6213a677329355ff32ebbdf7e6b3cce4c2577", + "target_commit": "878e3730f842d8a40ad35b076476b50eae7abe94", + "source_parents": [ + "7cd3ebacf5cb7a90b69f2606e547dea4b839277e" + ], + "target_parents": [ + "8138843e85b8231a2034449cc230c4e2d0b69004" + ], + "source_subject": "sdk: demote protocol facade", + "target_subject": "sdk: demote protocol facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T14:29:08-07:00", + "target_author_time": "2026-06-15T14:29:08-07:00", + "source_committer_time": "2026-06-15T14:29:08-07:00", + "target_committer_time": "2026-06-15T14:29:08-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/protocol.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/config.rs", + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/radrootsd.rs", + "crates/sdk/tests/relay_direct.rs", + "crates/sdk/tests/replica_ingest.rs" + ], + "normalized_patch_sha256": "9a16f3155a7d9e7330a4ec85e027bf1170b1b4225df5126c02494dcd1448750a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cb3c38e2ad8c8c7d598b00bb966d88c5e7221d53", + "target_commit": "0740da72976e88a58166aa1bcb62a5913c779cc3", + "source_parents": [ + "ebd6fc76629f8cb7688a04c8012bd1103008e94e" + ], + "target_parents": [ + "4f6389d9ae96b7a525ed062576dedc939a92d097" + ], + "source_subject": "ts: use dto_bindgen module rendering", + "target_subject": "ts: use dto_bindgen module rendering", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T00:10:23Z", + "target_author_time": "2026-06-28T00:10:23Z", + "source_committer_time": "2026-06-28T00:10:23Z", + "target_committer_time": "2026-06-28T00:10:23Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "b670da0b186c9607c587fc2718ff7e31da6895e9b5e487302d3cd61340a238dd", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cd32f1cb5160e54d77bd296966e4c1050a24cf11", + "target_commit": "6803dac83bfeb6b528ef07f7d156643ab0084b95", + "source_parents": [ + "8ebaa2445f1d6db1d691775eef6be454116524f1" + ], + "target_parents": [ + "0da30c9e42d3ac45365bb4c306129a9543f11bd4" + ], + "source_subject": "transport: adopt daemon transport publish protocol", + "target_subject": "transport: adopt daemon transport publish protocol", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T03:45:14Z", + "target_author_time": "2026-07-07T03:45:14Z", + "source_committer_time": "2026-07-07T03:45:14Z", + "target_committer_time": "2026-07-07T03:45:14Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "b1939b28bafe1c49d9c3f03381494e707e5a5819c3c00bd26a43f7596dfafda8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ce8cbacafefb2bffad02c87735c9f8b62e5e1650", + "target_commit": "643f12734ca3d9bea675b8bf15760f9bb38c1ef0", + "source_parents": [ + "d98c2922d96d9c19da0d009216fe0583466f1121" + ], + "target_parents": [ + "82f89064abf18969a1f6796e91fd9d04d933124f" + ], + "source_subject": "feat: add dto registry TypeScript renderer", + "target_subject": "feat: add dto registry TypeScript renderer", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T06:13:47Z", + "target_author_time": "2026-06-24T06:13:47Z", + "source_committer_time": "2026-06-24T06:13:47Z", + "target_committer_time": "2026-06-24T06:13:47Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "2eb5e9f2a4c41097ef93fe77827113a87c2a4d89dd36b4d5bb5c377f69bb33b1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cedf052a77a29c30d1880f559aebac3159ea355e", + "target_commit": "587e5e139f79e6efac02454e6708e5a05918b075", + "source_parents": [ + "f0dab0a96e0f3983d934ae019b3bc65f14d59ab8" + ], + "target_parents": [ + "98f3202cffaf90d965fcdf6fd8325e7c298c653b" + ], + "source_subject": "radrootsd: cover adapter response handling", + "target_subject": "radrootsd: cover adapter response handling", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T01:52:13Z", + "target_author_time": "2026-06-23T01:52:13Z", + "source_committer_time": "2026-06-23T01:52:13Z", + "target_committer_time": "2026-06-23T01:52:13Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs" + ], + "normalized_patch_sha256": "f1d5d9dff84896aa3bf09284ac3cc3ba91dff496989aafae1b28eb2623f89baa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cf95b5087e76b3d734971bea54b5269658d0b4f8", + "target_commit": null, + "source_parents": [ + "8c509998e9e2b6350363b9b9998b01fe3e39c1a9" + ], + "target_parents": [], + "source_subject": "repo: remove hosted workflow automation", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-29T23:35:19Z", + "target_author_time": null, + "source_committer_time": "2026-07-29T23:35:19Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "cfa30b931ff01d8da17d429e215c4151c4224ab1", + "target_commit": "a4c9f542d50475f1d0f290214e29f68d3aa75e1f", + "source_parents": [ + "df45d030e9fe5e7a455ddb31a39d540a5c135c4e" + ], + "target_parents": [ + "f4a352183041e288c605fe4d840d19f1131af94a" + ], + "source_subject": "sdk: normalize private helper cfgs", + "target_subject": "sdk: normalize private helper cfgs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T23:58:55Z", + "target_author_time": "2026-06-30T23:58:55Z", + "source_committer_time": "2026-06-30T23:58:55Z", + "target_committer_time": "2026-06-30T23:58:55Z", + "source_paths": [ + "crates/sdk/src/farm.rs", + "crates/sdk/src/order.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "f4010d601678496432f14badf34f2ff41f8eb855474b348a10495c30c5eda568", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cfbc2001f696b457fbc7f19b5f4c8ab6a2c157c4", + "target_commit": "867f3e4f403e778ae4f6b3363c2bca7656b0a46c", + "source_parents": [ + "8d1846eb57633e0923cef6902851e27c2bd6e703" + ], + "target_parents": [ + "08f1c8b3229f71fdfa37f95450837abf8f28f34f" + ], + "source_subject": "sdk: build relay fetch requests with filters", + "target_subject": "sdk: build relay fetch requests with filters", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T20:11:43Z", + "target_author_time": "2026-07-01T20:11:43Z", + "source_committer_time": "2026-07-01T20:11:43Z", + "target_committer_time": "2026-07-01T20:11:43Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs" + ], + "normalized_patch_sha256": "1e1e008e1c81f31670424b47369df7a093f4036641bd6d75518dd0cb0b4155d1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "cfcc7c99d3cbf6a215d34f7a04ebb4ae1bd3590e", + "target_commit": "16bd4f71f9111ac5f7cebc251e35d0529a06c958", + "source_parents": [ + "7ed8d3580bd7756de3145c384d5790bf6645cceb" + ], + "target_parents": [ + "b014dbfc4a96149d5e027deefe3655f780e401d0" + ], + "source_subject": "runtime: gate reticulum preview feature matrix", + "target_subject": "runtime: gate reticulum preview feature matrix", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-09T05:04:27Z", + "target_author_time": "2026-07-09T05:04:27Z", + "source_committer_time": "2026-07-09T05:04:27Z", + "target_committer_time": "2026-07-09T05:04:27Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "671fe1c6de58cff2ad6d6e4338c6753006650992eee71f2303673cc590aae26c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d014f5bb17528ed57be88ba0d5439aee0f2271b3", + "target_commit": "9ece927a101214cf2045a3b4cb680430f0cbc4ec", + "source_parents": [ + "db0871495e96ecfaca8f7132cc9bef3a2fbb8a8c" + ], + "target_parents": [ + "78ecf2f455ee0ccb22a048ffb2ba7bad2bfa4ea8" + ], + "source_subject": "sdk: remove direct order publish facade", + "target_subject": "sdk: remove direct order publish facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T00:19:25-07:00", + "target_author_time": "2026-06-19T00:19:25-07:00", + "source_committer_time": "2026-06-19T00:19:25-07:00", + "target_committer_time": "2026-06-19T00:19:25-07:00", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/relay_direct.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "20e251b3170ee0d9d1da701d62530515a09fa124358dcdcbc7c8609024d757aa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d015b33f05653c786d2cc24fe9b0830bbdae4cda", + "target_commit": "b41566af5eb76943d9bed59469f5cb8ac61a95cc", + "source_parents": [ + "fe1325675b43341e57999df8bd0d0f929cf0e5c4" + ], + "target_parents": [ + "925d3a35f04568501a2c1b349b14adced4471576" + ], + "source_subject": "deps: repin corrected lib history", + "target_subject": "deps: repin corrected lib history", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-05T16:58:43Z", + "target_author_time": "2026-08-05T16:58:43Z", + "source_committer_time": "2026-08-05T16:58:43Z", + "target_committer_time": "2026-08-05T16:58:43Z", + "source_paths": [ + "tools/xtask/src/release_graph.rs" + ], + "normalized_patch_sha256": "ed78019c8c0a65497fea748add8ffbc12828a37f3ee140a394a08baf19440fb6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d19a688128087c9c62b767b9fdd9e571a3e9add9", + "target_commit": "ff48c3eff6888f7e0aa677bd63ac33714c88ddfd", + "source_parents": [ + "4f6a1033b2e7a79ebb136fb752f6a9eda0ff2605" + ], + "target_parents": [ + "5c054aae7fe507850c35792741bc533bf3356a16" + ], + "source_subject": "nostr-connect: migrate workspace consumers", + "target_subject": "nostr-connect: migrate workspace consumers", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-31T20:32:58Z", + "target_author_time": "2026-07-31T20:32:58Z", + "source_committer_time": "2026-07-31T20:32:58Z", + "target_committer_time": "2026-07-31T20:32:58Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/adapters/signer.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "8cdb9bd4b7f7a1bfdfbaffa1320114fecd628c3e6f0c7c73d86a0b6793a28b89", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d21b1983b4419553c43eaf1d4fb2ba56e668b847", + "target_commit": "bd4145ee8ac6b9e810d6b09bc95fd2a1538568fc", + "source_parents": [ + "7096217f5a5028dbfc2c1278955a689412811c9c" + ], + "target_parents": [ + "3774064b70f7559b19b5153ffbd3f26dd9975288" + ], + "source_subject": "sdk: implement staged backup restore", + "target_subject": "sdk: implement staged backup restore", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T17:58:00-07:00", + "target_author_time": "2026-06-17T17:58:00-07:00", + "source_committer_time": "2026-06-17T17:58:00-07:00", + "target_committer_time": "2026-06-17T17:58:00-07:00", + "source_paths": [ + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "d51e768448dbbcb3c6474e661bcce2e27617d85324fa785003731515cd5d1d2e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d2a0e99e3d1d9665952f489a2fdd82caf326d660", + "target_commit": "b44a2acce87b30a4bce191b7c579a2995fa6ef13", + "source_parents": [ + "fec49a2b5119ed770763307a811d71d2fdeeb36f" + ], + "target_parents": [ + "99baec80e5b479e9c7c3a653e309ddfe3bc68914" + ], + "source_subject": "sdk: guard foundation hardening docs", + "target_subject": "sdk: guard foundation hardening docs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T07:46:53Z", + "target_author_time": "2026-07-13T07:46:53Z", + "source_committer_time": "2026-07-13T07:46:53Z", + "target_committer_time": "2026-07-13T07:46:53Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "e9a57f1ab9d619b9cbc5ee82f3e1774b359c097dfdfac19db59f2230cf8a5a94", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d3525918f42bf9c48b572670e956bec2ca3fb002", + "target_commit": "d2d6423e10fecf292873c119a2a696874dce4f6f", + "source_parents": [ + "9ddfcef621187379bbb91b9e3d519532330b3dd0" + ], + "target_parents": [ + "c96c0063a00b507f1382d61af9b099e0aab1db87" + ], + "source_subject": "sdk: wire configured signer workflows", + "target_subject": "sdk: wire configured signer workflows", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T23:52:07Z", + "target_author_time": "2026-06-23T23:52:07Z", + "source_committer_time": "2026-06-23T23:52:07Z", + "target_committer_time": "2026-06-23T23:52:07Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "eac5d8587177b5c40c03477c2a9474661d2eda2bc290130705e7c68aa901d063", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d385e6ce54d9260255e5d93ac82ceb903f34004e", + "target_commit": "5e0dcf40bef08898f267fd6157617e79a02ee6be", + "source_parents": [ + "2e45a4bd5bc70263cf3bfbce73345452b5c292d8" + ], + "target_parents": [ + "4a3cbe30a88fee436f41094acfcd02caaf8eaa1c" + ], + "source_subject": "transport: guard proxy completion matches", + "target_subject": "transport: guard proxy completion matches", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T05:23:45Z", + "target_author_time": "2026-07-08T05:23:45Z", + "source_committer_time": "2026-07-08T05:23:45Z", + "target_committer_time": "2026-07-08T05:23:45Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "cad8b8d794275e37313be3091c69dfedf4d440762d313d09cc827486b0f2f0ed", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d43f72542d6296194944d95328bf172b887d3f89", + "target_commit": "a56654a61c257328ea4251b988cb922f593f3694", + "source_parents": [ + "47656467f15b1d21b06f6b6159a8f70cb40c0d28" + ], + "target_parents": [ + "ddf11e6914d431f7896fedf22e4b70c7060ed185" + ], + "source_subject": "release: qualify front-door native targets", + "target_subject": "release: qualify front-door native targets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T22:50:56Z", + "target_author_time": "2026-08-03T22:50:56Z", + "source_committer_time": "2026-08-03T22:50:56Z", + "target_committer_time": "2026-08-03T22:50:56Z", + "source_paths": [ + "tools/xtask/src/main.rs", + "tools/xtask/src/target_qualification.rs" + ], + "normalized_patch_sha256": "ec694d6b2e623a0d16f00a20a6f1ac614041563422001911ec1c09bfe04d0afa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d4ded20214274879494d435ae2eaa65fb1c7e95a", + "target_commit": "b0075d086090759654a20c046ef62bbaf936c98e", + "source_parents": [ + "4f72eaf44956b54b0f5f76b7560482be9ae09843" + ], + "target_parents": [ + "11fca7cbb6bb8fa96080b064515605ef455d09e5" + ], + "source_subject": "sdk: type push outbox event ids", + "target_subject": "sdk: type push outbox event ids", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:19:00-07:00", + "target_author_time": "2026-06-16T00:19:00-07:00", + "source_committer_time": "2026-06-16T00:19:00-07:00", + "target_committer_time": "2026-06-16T00:19:00-07:00", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs" + ], + "normalized_patch_sha256": "d39867db0f8a282acc3e3d69e572f1bdda5c46db70134e922ca77de6ba9c3053", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d554fef01ba67ccc3926138bf6cd945aa67917e4", + "target_commit": "5f47c85ca3f1ee7b9583b08cfe48454c6dc78797", + "source_parents": [ + "426b7e68987c64bea50ca20ca0653e4a68d7e28f" + ], + "target_parents": [ + "e2533b48d5558e6f4fb477124f4039564fc1f41f" + ], + "source_subject": "sdk: guard product examples and relay policy", + "target_subject": "sdk: guard product examples and relay policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T14:21:57-07:00", + "target_author_time": "2026-06-16T14:21:57-07:00", + "source_committer_time": "2026-06-16T14:21:57-07:00", + "target_committer_time": "2026-06-16T14:21:57-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/tests/runtime_foundation.rs" + ], + "normalized_patch_sha256": "bbec5bdbe1139f59569af5b03500f06595f68c5a0a00c6e7ada3d66f22439836", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d5ed93c9f35b8e26f03fcbec320a52c9a8eec1a2", + "target_commit": "7d9a8dbef8ec82ca894ab9bcd2414ef4cbb5ce36", + "source_parents": [ + "6c0f7c3fe57c6f3ad994848b65008c700d76198a" + ], + "target_parents": [ + "6b8b642e159273c34ada6dba7c46eb01edc21fb3" + ], + "source_subject": "release: enforce lib-only version amendment", + "target_subject": "release: enforce lib-only version amendment", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:50:33Z", + "target_author_time": "2026-07-28T18:50:33Z", + "source_committer_time": "2026-07-28T18:50:33Z", + "target_committer_time": "2026-07-28T18:50:33Z", + "source_paths": [ + "crates/event_codec_wasm/Cargo.toml", + "crates/replica_store_wasm/Cargo.toml", + "crates/replica_sync_wasm/Cargo.toml", + "crates/sdk/Cargo.toml", + "crates/sql_wasm_runtime/Cargo.toml", + "tools/xtask/Cargo.toml", + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "981af2da0a5dd352dd1d327195aea59e30252e6e08f55e6bda504e182ed7583e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d62f884833ebf2d27b818e73e227b6c9a9a49af6", + "target_commit": "ed1e55fb77759dfeb5c2f482e9e927786537a653", + "source_parents": [ + "df4be6f48cefbb09db3f8242afaf40cef6039a2f" + ], + "target_parents": [ + "8188a96d890912c5787f4b674b83db1326b9f4f1" + ], + "source_subject": "sdk: replace SDK storage implementation with storage SPIs", + "target_subject": "sdk: replace SDK storage implementation with storage SPIs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:50:41Z", + "target_author_time": "2026-08-03T10:50:41Z", + "source_committer_time": "2026-08-03T10:50:41Z", + "target_committer_time": "2026-08-03T10:50:41Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/storage.rs" + ], + "normalized_patch_sha256": "7d5127ce2016363f61bd3f3717dd2af76544272a6187b585c08ec93c79d21e88", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d68b9c254e4ac38411cc999286e622a398ac5bbe", + "target_commit": "d341186d0a819c2ed8d1a5e15985ea1eb63914ec", + "source_parents": [ + "62b4062208f74b7359a75a2ca49375642c20f39c" + ], + "target_parents": [ + "fe9cf3259f3b663dc5c583c700d9539967b4c60b" + ], + "source_subject": "sdk: align listing address facade", + "target_subject": "sdk: align listing address facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-14T02:35:47-07:00", + "target_author_time": "2026-06-14T02:35:47-07:00", + "source_committer_time": "2026-06-14T02:35:47-07:00", + "target_committer_time": "2026-06-14T02:35:47-07:00", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/order.rs", + "crates/sdk/tests/client.rs", + "crates/sdk/tests/facade.rs" + ], + "normalized_patch_sha256": "dfdd2ebd07fb0b2df9a2f83f1202f7380284ac31cb272f97384cc547bc7e0c19", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d6f332bfde2eed006a3de72f0105d6816d676737", + "target_commit": "a8b91eaa9a864e7bc78901ff08e4c93ca4491755", + "source_parents": [ + "e717bc7c190674f18d696b9b509d911d33888f07" + ], + "target_parents": [ + "1a6dcde55a0b52a2c36d46b6e34a1d8c456aa3c7" + ], + "source_subject": "sdk: add comprehensive SDK lifecycle and safe-default tests", + "target_subject": "sdk: add comprehensive SDK lifecycle and safe-default tests", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T12:03:12Z", + "target_author_time": "2026-08-03T12:03:12Z", + "source_committer_time": "2026-08-03T12:03:12Z", + "target_committer_time": "2026-08-03T12:03:12Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/tests/lifecycle.rs" + ], + "normalized_patch_sha256": "097ac234e6ecd8b1ef00f47538416ef887c8eceac16a38f98deb4dda2e3ca4b0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d819a9f25c67ecae4d3cd437c26c96a8103cbf64", + "target_commit": "f0d412234b339d76e26970f9798d87784a133583", + "source_parents": [ + "1a52b44a85b52eb65e5056ffc5dc565c887c34e2" + ], + "target_parents": [ + "7af64268eda3a7f9aa062b1a144c65beffbb27bd" + ], + "source_subject": "sdk: add trade validation receipt client", + "target_subject": "sdk: add trade validation receipt client", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T08:52:43Z", + "target_author_time": "2026-07-01T08:52:43Z", + "source_committer_time": "2026-07-01T08:52:43Z", + "target_committer_time": "2026-07-01T08:52:43Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/trade_public_api.rs" + ], + "normalized_patch_sha256": "9d79dd79cad107eece1da213ea49b5e203063450723719d865a691d97cb2ac37", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d98c2922d96d9c19da0d009216fe0583466f1121", + "target_commit": "82f89064abf18969a1f6796e91fd9d04d933124f", + "source_parents": [ + "d3525918f42bf9c48b572670e956bec2ca3fb002" + ], + "target_parents": [ + "d2d6423e10fecf292873c119a2a696874dce4f6f" + ], + "source_subject": "sdk: harden Myc NIP-46 request policy", + "target_subject": "sdk: harden Myc NIP-46 request policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T04:53:35Z", + "target_author_time": "2026-06-24T04:53:35Z", + "source_committer_time": "2026-06-24T04:53:35Z", + "target_committer_time": "2026-06-24T04:53:35Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs" + ], + "normalized_patch_sha256": "b702c0c9d75e3469728961a3764a206aac546e23ebe657097850e60d4b5659ac", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "d994ba4de5d25b207489fc760eebc91c52dacf1a", + "target_commit": "487b85367edab1c8a30bcc19cd4d72e20a01db73", + "source_parents": [ + "71d53d141f8d2dd68bf3c049a0e0d88873ab0fb3" + ], + "target_parents": [ + "0d83d8de960d66234fe3771d520a0924b5ae558d" + ], + "source_subject": "packages: use pnpm pack tarballs", + "target_subject": "packages: use pnpm pack tarballs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T06:12:37Z", + "target_author_time": "2026-06-28T06:12:37Z", + "source_committer_time": "2026-06-28T06:12:37Z", + "target_committer_time": "2026-06-28T06:12:37Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "aa166952ea87437a4416f8f5230825a8fc212a8fe636afa0fb5aca07fe2713d5", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "da79c7bbec2df72ecb255c525f32d131b0370231", + "target_commit": "071a3cdf1f1a2d2903c965c8adfd7f848ccc217b", + "source_parents": [ + "f2da000795f566876a3b8ae99361fd1b01c81e66" + ], + "target_parents": [ + "eab6d04278cb9bbf5b1042e48148d64229382460" + ], + "source_subject": "publish-proxy: add sdk proxy publish transport", + "target_subject": "publish-proxy: add sdk proxy publish transport", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T09:17:38Z", + "target_author_time": "2026-06-23T09:17:38Z", + "source_committer_time": "2026-06-23T09:17:38Z", + "target_committer_time": "2026-06-23T09:17:38Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README", + "crates/sdk/src/adapters/mod.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/adapters_radrootsd_tests.rs", + "crates/sdk/tests/unit/relay_targets_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "6c2711827582029f1853f9211c6ac3f173c67ced38f602834b65ab2985c8389a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "da9525d37943bb014084885d3259de473e79e7f3", + "target_commit": "5b49fe455e9e87ba6e7ddac140a860e12b3e4a7d", + "source_parents": [ + "3491f2beee0f055f24aa0fd7663fa57b2f3f6695" + ], + "target_parents": [ + "84301e0eca8b92cd2a8d6acea447a146db9bde32" + ], + "source_subject": "sqlite: align SDK bundled runtime", + "target_subject": "sqlite: align SDK bundled runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-15T02:11:31Z", + "target_author_time": "2026-07-15T02:11:31Z", + "source_committer_time": "2026-07-15T02:11:31Z", + "target_committer_time": "2026-07-15T02:11:31Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/private_store.rs", + "crates/sdk/tests/unit/private_store_tests.rs" + ], + "normalized_patch_sha256": "dcb58c8272370adf0b168112df51df2de695c07627168b589ea3159b94bb80c2", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "db0871495e96ecfaca8f7132cc9bef3a2fbb8a8c", + "target_commit": "78ecf2f455ee0ccb22a048ffb2ba7bad2bfa4ea8", + "source_parents": [ + "e03195035f770b09d2ea824210b218a935fa809a" + ], + "target_parents": [ + "1f57dba96d9e446acf23e255ffad471ed7013fa3" + ], + "source_subject": "sdk: add order lifecycle evidence ingest", + "target_subject": "sdk: add order lifecycle evidence ingest", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T23:58:19-07:00", + "target_author_time": "2026-06-18T23:58:19-07:00", + "source_committer_time": "2026-06-18T23:58:19-07:00", + "target_committer_time": "2026-06-18T23:58:19-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "efdf17e84e23a22c1ee0a13842b05e2f0ca8b23ce4ce714bf8a7103a26cfc770", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "db11230b73941a38b34295fc0d1496a220e45f87", + "target_commit": "d4e910c321e256c8cc035243eaf312eebc047bfd", + "source_parents": [ + "a30e83b0a97abfc06518e0aa92479ba8f8fa4719" + ], + "target_parents": [ + "2709a7ae5e8a2f19e0d1b7c5f4bcdb4711e5bdd0" + ], + "source_subject": "dto: render core bindings from source roots", + "target_subject": "dto: render core bindings from source roots", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T06:32:26Z", + "target_author_time": "2026-06-24T06:32:26Z", + "source_committer_time": "2026-06-24T06:32:26Z", + "target_committer_time": "2026-06-24T06:32:26Z", + "source_paths": [ + "tools/xtask/Cargo.toml", + "tools/xtask/src/check.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/generate.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "a4dd2ece40df5a8c22489a3f7250a324db7bd72b7c891a2b00117e7ea12796a3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "db2c6ee7b88c8663f31ec0b86d5e0f2579583b24", + "target_commit": "322f91976abfeeded0dfa98808317b046296c998", + "source_parents": [ + "7e40c2fadd54c1e0edbe2523980b34b07f4ef10a" + ], + "target_parents": [ + "26a3cb8e3af725fe5953a62882efecfc4d0d0e21" + ], + "source_subject": "orders: slim SDK order runtime API", + "target_subject": "orders: slim SDK order runtime API", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T18:17:25-07:00", + "target_author_time": "2026-06-19T18:17:25-07:00", + "source_committer_time": "2026-06-19T18:17:25-07:00", + "target_committer_time": "2026-06-19T18:17:25-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "99c64ef10216565b0c381f8497b0267449a79b183778a76b2b9837b020c61df3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "dd50171d8225615ca9368a2c2ff987344118fe9b", + "target_commit": "5af1611cfc896a1ad9890e08b60dad36c5a5ab80", + "source_parents": [ + "8c402daa4c6d4dd34b0f634fc59280b7818613fa" + ], + "target_parents": [ + "3b98208727b404d9c3c4654d2b263f0ee6a038d0" + ], + "source_subject": "sdk: canonicalize trade product surface", + "target_subject": "sdk: canonicalize trade product surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T20:02:15Z", + "target_author_time": "2026-06-30T20:02:15Z", + "source_committer_time": "2026-06-30T20:02:15Z", + "target_committer_time": "2026-06-30T20:02:15Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/trade_public_api.rs" + ], + "normalized_patch_sha256": "118195a1439060bec21cbd5b640c2f796d71ac6865ada42310d5af6641705a66", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "de1642d7f6025307b426dd3f8d9c8cc23f4f5f98", + "target_commit": "1d113e4a184bb0a3ea69cf8d1f874709c1b2816f", + "source_parents": [ + "7933e55ce77d9207fb4199047ae9ec736160dacd" + ], + "target_parents": [ + "053d1ddaea2f4d90c484593ccb0952e4025314dd" + ], + "source_subject": "trade: inventory binding type surface", + "target_subject": "trade: inventory binding type surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T08:30:26Z", + "target_author_time": "2026-06-24T08:30:26Z", + "source_committer_time": "2026-06-24T08:30:26Z", + "target_committer_time": "2026-06-24T08:30:26Z", + "source_paths": [ + "crates/trade_bindings/src/lib.rs" + ], + "normalized_patch_sha256": "ba4b87c81180d14c019d398d910e66071c61935927eb0e89f04cda4f6ab91bb1", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "df357dc03e99dde2a4fe6a24bcb9471358f52b06", + "target_commit": "b713f197804d7aaebdfb739a1c439c34970a7896", + "source_parents": [ + "6e3552ddca05e061bc4519b08e86edf1b069c77a" + ], + "target_parents": [ + "31197938097ed0790b4b551ad2ff430797947243" + ], + "source_subject": "docs: align local runtime API guidance", + "target_subject": "docs: align local runtime API guidance", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-19T04:00:29-07:00", + "target_author_time": "2026-06-19T04:00:29-07:00", + "source_committer_time": "2026-06-19T04:00:29-07:00", + "target_committer_time": "2026-06-19T04:00:29-07:00", + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "topology_support" + }, + { + "source_commit": "df45d030e9fe5e7a455ddb31a39d540a5c135c4e", + "target_commit": "f4a352183041e288c605fe4d840d19f1131af94a", + "source_parents": [ + "748ca41cb2ad3462a5aab3310c6bd6e0901dbb68" + ], + "target_parents": [ + "c47ca80391953813005cb658dd6617640154c99b" + ], + "source_subject": "tests: harden SDK root trade guards", + "target_subject": "tests: harden SDK root trade guards", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T23:52:39Z", + "target_author_time": "2026-06-30T23:52:39Z", + "source_committer_time": "2026-06-30T23:52:39Z", + "target_committer_time": "2026-06-30T23:52:39Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "10c008c45effb687d4daee12942676439c9143ec3011eae199baeb117fd2890d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "df4be6f48cefbb09db3f8242afaf40cef6039a2f", + "target_commit": "8188a96d890912c5787f4b674b83db1326b9f4f1", + "source_parents": [ + "9e511cc931c7b6c51cf168ec37d1b62a43944a12" + ], + "target_parents": [ + "6f21438079eb92228cde2157ee5358e90478c2a4" + ], + "source_subject": "sdk: replace duplicated SDK error metadata", + "target_subject": "sdk: replace duplicated SDK error metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T10:45:14Z", + "target_author_time": "2026-08-03T10:45:14Z", + "source_committer_time": "2026-08-03T10:45:14Z", + "target_committer_time": "2026-08-03T10:45:14Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/error.rs" + ], + "normalized_patch_sha256": "667e7daf028f986f51ce60af340623ab1e8f5b285cac6a3e2938c48a3a467900", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "dfa3bb1dbd5b2a407f1e010adf1665df1fde6d96", + "target_commit": "57394009fea0b6291d6c2f068d60fbfa139362b2", + "source_parents": [ + "c359ef395697769fa7806e77454f7f2a7b122aa9" + ], + "target_parents": [ + "82652bba843c071732456cac79c4a73bc50676a2" + ], + "source_subject": "tests: guard generic transport dispatch", + "target_subject": "tests: guard generic transport dispatch", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-11T08:29:20Z", + "target_author_time": "2026-07-11T08:29:20Z", + "source_committer_time": "2026-07-11T08:29:20Z", + "target_committer_time": "2026-07-11T08:29:20Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "422c0cbb9c6aeca82ad651034ba1605e735a11f427488597b1a67c5da123815f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e00cb1c71f15bab2dbb782599fd2b38c60e6714c", + "target_commit": "841f81812e84790e7fcd6c84e089f86224c16217", + "source_parents": [ + "4e264626a1932b374c1a83975e2c52c6595f800d" + ], + "target_parents": [ + "6a01051d3bc26836464dbdfa51a70f966ca7bbfc" + ], + "source_subject": "sdk: add order decision runtime", + "target_subject": "sdk: add order decision runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T22:25:34-07:00", + "target_author_time": "2026-06-18T22:25:34-07:00", + "source_committer_time": "2026-06-18T22:25:34-07:00", + "target_committer_time": "2026-06-18T22:25:34-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "64956d02216eaf3984aff322ea6142010088d5b4f960cfeaf6b8f6a065bc5692", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e0131fcb84123ce5939f4eec38d99f7b484023f5", + "target_commit": "ed0710cc7011a29b6f8cc6dcfa5b9f5f2b3887c0", + "source_parents": [ + "96708a659006018b626974264f134bbfda5a948a" + ], + "target_parents": [ + "1a648434b025b04885b100e28d2d9ffe690ae618" + ], + "source_subject": "tests: prove isolated trade resync", + "target_subject": "tests: prove isolated trade resync", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T09:26:34Z", + "target_author_time": "2026-06-30T09:26:34Z", + "source_committer_time": "2026-06-30T09:26:34Z", + "target_committer_time": "2026-06-30T09:26:34Z", + "source_paths": [ + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "473bc584ca8cfb3bd6958b8f9b5d8cae4ffa87205b39dc951108313f3c94387e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e03195035f770b09d2ea824210b218a935fa809a", + "target_commit": "1f57dba96d9e446acf23e255ffad471ed7013fa3", + "source_parents": [ + "84f21199b51c16103d9898ceda3fd2267cafa6d2" + ], + "target_parents": [ + "a1762bdc8fbf0cf303ce0b840d4c3719f8996c49" + ], + "source_subject": "sdk: add order lifecycle runtimes", + "target_subject": "sdk: add order lifecycle runtimes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-18T23:22:46-07:00", + "target_author_time": "2026-06-18T23:22:46-07:00", + "source_committer_time": "2026-06-18T23:22:46-07:00", + "target_committer_time": "2026-06-18T23:22:46-07:00", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "1700cc72001fec39df8b1a604efa8abd2c886ea1fd8b81a6393987ccb820bd9e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e0b77999abf22fccd1ea51e576bd6346f1a13d71", + "target_commit": "f758400e8790560105b19f393d1752d1c1b32502", + "source_parents": [ + "b7a74355693c40926bf6a7840fb113d2c77d2529" + ], + "target_parents": [ + "273a24aafa6211b0fa60bc88647d4a57e0edc2b8" + ], + "source_subject": "sdk: guard transport-neutral sync status", + "target_subject": "sdk: guard transport-neutral sync status", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T07:47:45Z", + "target_author_time": "2026-07-08T07:47:45Z", + "source_committer_time": "2026-07-08T07:47:45Z", + "target_committer_time": "2026-07-08T07:47:45Z", + "source_paths": [ + "crates/sdk/tests/source_boundary.rs" + ], + "normalized_patch_sha256": "7f53cca243a5ca32a3e66baffaf40545fc16d7dd6120c1c7e3bb890045a63b1d", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e0bb1be02dbc7c3016a453a3a45aeb8c9ed7f3ab", + "target_commit": "8d18e6a29ad8050183a3f46b5184f3ce07f35c34", + "source_parents": [ + "38eacd2c501b137011aa66bfca1a966ea8ab106d" + ], + "target_parents": [ + "a84948593485eb4cba3e8ae3c0f4a6c112014f98" + ], + "source_subject": "sdk: add sync outbox push runtime", + "target_subject": "sdk: add sync outbox push runtime", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-15T14:12:09-07:00", + "target_author_time": "2026-06-15T14:12:09-07:00", + "source_committer_time": "2026-06-15T14:12:09-07:00", + "target_committer_time": "2026-06-15T14:12:09-07:00", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "e253dafc10d55b409bbe7f32b08d4e3ecfc67fce7ff449099700e0e69e4f3512", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e1e18ae08f3942df560a881d2db669507eb0b2d7", + "target_commit": "6c365becb0591623964537f02d78f2809c103dce", + "source_parents": [ + "b243d14ee590daf5a50d52ef9309f80eb7f47f88" + ], + "target_parents": [ + "5843fe3d2f22eff03d62dc7810cd71396a4404cc" + ], + "source_subject": "dto: guard events registry migration inputs", + "target_subject": "dto: guard events registry migration inputs", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T07:05:52Z", + "target_author_time": "2026-06-24T07:05:52Z", + "source_committer_time": "2026-06-24T07:05:52Z", + "target_committer_time": "2026-06-24T07:05:52Z", + "source_paths": [ + "tools/xtask/src/dto_render.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "1542aad6b2cda900969c52da22cd404986b7ec9112cebfcd2f10cedaecbb2218", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e2a22515e1ff15baca756071ec5a0df082cc9c19", + "target_commit": "836e5e7cd2888bbbc5e5d504fb91e6f4ae5855d9", + "source_parents": [ + "c5a99864adedac481944ab6a14a7ee9709acc2dd" + ], + "target_parents": [ + "943aa92dad222ee9f707ddc51dbd19acd6e87f93" + ], + "source_subject": "sdk: enforce product trade privacy preflight", + "target_subject": "sdk: enforce product trade privacy preflight", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T04:06:02Z", + "target_author_time": "2026-06-30T04:06:02Z", + "source_committer_time": "2026-06-30T04:06:02Z", + "target_committer_time": "2026-06-30T04:06:02Z", + "source_paths": [ + "crates/sdk/src/error.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/privacy.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/error_tests.rs" + ], + "normalized_patch_sha256": "20a63c039ca49d6c73c77c0cdbceb6d33adaeace58de6bdd9adbcae6ffdd2611", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e424cbf3dcb2ba2b336ce5173b62e1b0e79bfb53", + "target_commit": "97fb7277a2510561461d9254beadea365d7c90e1", + "source_parents": [ + "48b88b2f8df577d015a810609b31b655d80a74f3" + ], + "target_parents": [ + "f85a961d2ddd0338a6fff0a1e65fa2e86d978f5d" + ], + "source_subject": "workspace: enforce complete local membership", + "target_subject": "workspace: enforce complete local membership", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-27T08:09:48Z", + "target_author_time": "2026-07-27T08:09:48Z", + "source_committer_time": "2026-07-27T08:09:48Z", + "target_committer_time": "2026-07-27T08:09:48Z", + "source_paths": [ + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "a275ed2785ad29724375c9e8928a0711ce5d82d0f4763943b123bba15de8646a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e48c5227400710f2bf84234aae3827b6384937dd", + "target_commit": "d66458e494e87c1dc3c9ff1424e2ac8da93285e3", + "source_parents": [ + "71be029ec028696f118d8b9bfc0f4f80c39103b3" + ], + "target_parents": [ + "fc488d6410f06ee1166b950d88b062cb67828ccb" + ], + "source_subject": "sdk: push queued outbox targets", + "target_subject": "sdk: push queued outbox targets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:12:03-07:00", + "target_author_time": "2026-06-16T00:12:03-07:00", + "source_committer_time": "2026-06-16T00:12:03-07:00", + "target_committer_time": "2026-06-16T00:12:03-07:00", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "9d69b2474dd7f6e1521249fcdb89ca43f967f44b54a6802110f348657abc1029", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e50c1fa6525ce5a10b3fa0787cc2cb84bb9ad3a8", + "target_commit": null, + "source_parents": [ + "cedf052a77a29c30d1880f559aebac3159ea355e" + ], + "target_parents": [], + "source_subject": "packages: expose sdk validation scripts", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-23T01:54:49Z", + "target_author_time": null, + "source_committer_time": "2026-06-23T01:54:49Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "e543680454009d3bc0b08f3c7f13685ec28d6b63", + "target_commit": "31a6c7d8e9cec3eac4ef1290b6cafa7ba3ec5b22", + "source_parents": [ + "ef39fc6d21e1e9d082132083f2ef853aa6ae0cae" + ], + "target_parents": [ + "653b4519d7eb006d407d83609fb994ab284f17f2" + ], + "source_subject": "style: format repository version policy", + "target_subject": "style: format repository version policy", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:41:46Z", + "target_author_time": "2026-07-28T18:41:46Z", + "source_committer_time": "2026-07-28T18:41:46Z", + "target_committer_time": "2026-07-28T18:41:46Z", + "source_paths": [ + "tools/xtask/src/architecture.rs" + ], + "normalized_patch_sha256": "8f3a28a9d75073085bde7af4c98edd274188a68b449062f42e2c739dcb9d7b71", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e668f030fc29584004b8df8215b3f16ffdf6d613", + "target_commit": "9eee1823f01dbc1579643d457804706bd8403264", + "source_parents": [ + "ef9d0ba73e74d6eb98c18153ac8fc1b3338c9d53" + ], + "target_parents": [ + "4931e0a17b37dac553c0a268d179caa063c36d9b" + ], + "source_subject": "sdk: adopt trade product facade", + "target_subject": "sdk: adopt trade product facade", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T03:04:09Z", + "target_author_time": "2026-06-30T03:04:09Z", + "source_committer_time": "2026-06-30T03:04:09Z", + "target_committer_time": "2026-06-30T03:04:09Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/privacy.rs", + "crates/sdk/src/product_clients.rs", + "crates/sdk/src/relay_targets.rs", + "crates/sdk/src/trade_storage.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/relay_targets_tests.rs" + ], + "normalized_patch_sha256": "334cb02a43601c2e33d070e4133c88db89556944cb1f3f9c1d52de86b03a4fc7", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e717bc7c190674f18d696b9b509d911d33888f07", + "target_commit": "1a6dcde55a0b52a2c36d46b6e34a1d8c456aa3c7", + "source_parents": [ + "f7a7d8bf62211ac090c922ba0a917bf3548c5b2f" + ], + "target_parents": [ + "558a8c97318cc8dbe55bf2b691f294e3ae466e5e" + ], + "source_subject": "sdk: rename public SDK types and remove representation coupling", + "target_subject": "sdk: rename public SDK types and remove representation coupling", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:59:31Z", + "target_author_time": "2026-08-03T11:59:31Z", + "source_committer_time": "2026-08-03T11:59:31Z", + "target_committer_time": "2026-08-03T11:59:31Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/public_api.rs" + ], + "normalized_patch_sha256": "afea274d6b0cb3c6b4898a1e5d8a2337367eb50201bb5adff2d83a918bdf169b", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e743afc0154003897dd1e318e543f1a80ccf9dc4", + "target_commit": "b9a1f1e38c5c9ea0154eaa1a4741bb0b49bd37d4", + "source_parents": [ + "6a196b1ff23563d2b58662ed091f85208a995307" + ], + "target_parents": [ + "396b9ed397d2bd85935f04fc01f2348c931166f3" + ], + "source_subject": "kotlin: align generated sdk contract", + "target_subject": "kotlin: align generated sdk contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:47:55Z", + "target_author_time": "2026-08-03T13:47:55Z", + "source_committer_time": "2026-08-03T13:47:55Z", + "target_committer_time": "2026-08-03T13:47:55Z", + "source_paths": [ + "tools/xtask/src/bindings.rs" + ], + "normalized_patch_sha256": "0a9148f999b1b527a1d568bfb77f84e6b1339d9ca8c38c9726588298fac49edc", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e87eb80d9f256d2afd58f5105c29f4b3f6cd11a3", + "target_commit": "b46e2a378ae90c25a0a6525d2c10ee0a6ab225ca", + "source_parents": [ + "b795ace3873c95024d11473cb9d8912b8a6b5a20" + ], + "target_parents": [ + "89577853c46e58a4f0d519bb7612b42802a2b279" + ], + "source_subject": "transport: align SDK publish preview outcomes", + "target_subject": "transport: align SDK publish preview outcomes", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T05:46:44Z", + "target_author_time": "2026-07-07T05:46:44Z", + "source_committer_time": "2026-07-07T05:46:44Z", + "target_committer_time": "2026-07-07T05:46:44Z", + "source_paths": [ + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/sync_runtime.rs" + ], + "normalized_patch_sha256": "3d05204798e5564582167b7c4f03e5cfb80485c04558c4a59479d647f8e89ae6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "e9d7d77cc105113afc209cc80949bcfb93ef4aa9", + "target_commit": "e3bb612d4410aa0dcdee75d0523923eb54d65a35", + "source_parents": [ + "c7211babab84f138ee5569e313b04c6c8281cf8c" + ], + "target_parents": [ + "741c42a66bf32960176408e1a58af920add9b9c8" + ], + "source_subject": "knowledge-sdk: align wiki title optionality", + "target_subject": "knowledge-sdk: align wiki title optionality", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-06T01:50:09Z", + "target_author_time": "2026-07-06T01:50:09Z", + "source_committer_time": "2026-07-06T01:50:09Z", + "target_committer_time": "2026-07-06T01:50:09Z", + "source_paths": [ + "crates/sdk/src/knowledge.rs", + "crates/sdk/tests/knowledge_public_api.rs" + ], + "normalized_patch_sha256": "35f69fbca382a54e1eeda8cc64ef7bc904f0073930bae083971fc29ba3595406", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ea2cba9e56423390f4d3cf7bfa3a1195963f322f", + "target_commit": "6195da3c4952f8a2476ee8e358f4469ca245a15b", + "source_parents": [ + "a54ec0b1c92fbd24d142c8d32b79b0de7ba36813" + ], + "target_parents": [ + "977c73f1054249ee28eb6cf4fe961ce2f1c05313" + ], + "source_subject": "sdk: align foundational event contract", + "target_subject": "sdk: align foundational event contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-18T10:05:42Z", + "target_author_time": "2026-07-18T10:05:42Z", + "source_committer_time": "2026-07-18T10:05:42Z", + "target_committer_time": "2026-07-18T10:05:42Z", + "source_paths": [ + "crates/event_codec_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "bd7c303b451a2499cce344c21c5e3946a850718f0be8d070f7792f1bcef9c8fb", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ebd6fc76629f8cb7688a04c8012bd1103008e94e", + "target_commit": "4f6389d9ae96b7a525ed062576dedc939a92d097", + "source_parents": [ + "87cf4ff726d7eee0f11a5eef5a8206f4960b6956" + ], + "target_parents": [ + "70ebbfbfb612fd57dc99456f816c0c7d45dae969" + ], + "source_subject": "trade: source order workflow projection bindings", + "target_subject": "trade: source order workflow projection bindings", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-27T21:45:13Z", + "target_author_time": "2026-06-27T21:45:13Z", + "source_committer_time": "2026-06-27T21:45:13Z", + "target_committer_time": "2026-06-27T21:45:13Z", + "source_paths": [ + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "351beec3d55fe8909de29b618375b6f8ead010fc549feca46cfba4a2def8ebe6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ec425bca2e9f23577f80056bd9b8230e44a0322a", + "target_commit": "1069d841d785e062d6aef9813e3b8134b1b71723", + "source_parents": [ + "83d18a76dccdbf4f66536fa5c1eb051bac2b0391" + ], + "target_parents": [ + "5d623f6498b57923b7e4ee64c8b7e7e4e4abbe10" + ], + "source_subject": "sync: keep sibling plans after proxy validation failure", + "target_subject": "sync: keep sibling plans after proxy validation failure", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-07T23:29:35Z", + "target_author_time": "2026-07-07T23:29:35Z", + "source_committer_time": "2026-07-07T23:29:35Z", + "target_committer_time": "2026-07-07T23:29:35Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "3afba45b45896282fff6f6a20f7ee17b95d722eb4ab3350ae717241cff78d1ff", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ede93b05499af38054f92f827207a37a2d190cdc", + "target_commit": "4ec09b6d4d422d04a4762bee02586c45ec2e9006", + "source_parents": [ + "52be745797061ad23fe82f77470dcd58cbb5bcf0" + ], + "target_parents": [ + "eb827e0a4b4dd97e444ab138c1377b3884855fb7" + ], + "source_subject": "status: expose storage checkpoint evidence", + "target_subject": "status: expose storage checkpoint evidence", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T05:13:51Z", + "target_author_time": "2026-07-05T05:13:51Z", + "source_committer_time": "2026-07-05T05:13:51Z", + "target_committer_time": "2026-07-05T05:13:51Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "4beb82c7dcd6da2d07ea0d6fa90dff594cbb11af1401fd4cbe979eb74ba12e29", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "edf40a100590de728fe9ee5677b37d29f61bc40c", + "target_commit": "db352eac57658b672c6f2920693eaa92c7436ed2", + "source_parents": [ + "dfa3bb1dbd5b2a407f1e010adf1665df1fde6d96" + ], + "target_parents": [ + "57394009fea0b6291d6c2f068d60fbfa139362b2" + ], + "source_subject": "crate-surface: delete types bindings package", + "target_subject": "crate-surface: delete types bindings package", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-11T22:22:49Z", + "target_author_time": "2026-07-11T22:22:49Z", + "source_committer_time": "2026-07-11T22:22:49Z", + "target_committer_time": "2026-07-11T22:22:49Z", + "source_paths": [ + "crates/replica_sync_wasm/Cargo.toml", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/Cargo.toml", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/actor_json.rs", + "crates/sdk/src/adapters/radrootsd.rs", + "crates/sdk/src/adapters/signing.rs", + "crates/sdk/src/dvm_runtime.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/idempotency.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/market_runtime.rs", + "crates/sdk/src/order.rs", + "crates/sdk/src/orders_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_storage.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/dvm_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/market_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/replica_ingest.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/trade_product_publish_runtime.rs", + "crates/sdk/tests/trade_public_api.rs", + "crates/sdk/tests/unit/actor_json_tests.rs", + "crates/sdk/tests/unit/adapters_nostr_tests.rs", + "crates/sdk/tests/unit/adapters_signing_tests.rs", + "crates/sdk/tests/unit/dvm_runtime_tests.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/idempotency_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/private_store_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "tools/xtask/Cargo.toml", + "tools/xtask/src/contracts.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/output.rs", + "tools/xtask/src/package_matrix.rs" + ], + "normalized_patch_sha256": "52dbebeb7f38fede38eacaaa229f3b57b9ff9e424709bddf2c91adcced43ace6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ef39fc6d21e1e9d082132083f2ef853aa6ae0cae", + "target_commit": "653b4519d7eb006d407d83609fb994ab284f17f2", + "source_parents": [ + "c21b66f4402b934f52d73d8720675d264672ed07" + ], + "target_parents": [ + "893ea49ddb73a156b2a95562a0e548c7b54ff337" + ], + "source_subject": "deps: consume frozen library version", + "target_subject": "deps: consume frozen library version", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-28T18:14:10Z", + "target_author_time": "2026-07-28T18:14:10Z", + "source_committer_time": "2026-07-28T18:14:10Z", + "target_committer_time": "2026-07-28T18:14:10Z", + "source_paths": [ + "tools/xtask/src/architecture.rs", + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "895a2789ad2f8945ec523cbbb9361c45b2bace3c2c2e65902cf8b8034c2b8c40", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "ef9d0ba73e74d6eb98c18153ac8fc1b3338c9d53", + "target_commit": "4931e0a17b37dac553c0a268d179caa063c36d9b", + "source_parents": [ + "ae89b618e4ef6ce1facdc84361f7c788fe05073c" + ], + "target_parents": [ + "793585c96105fa188a06b5009d94244c538db7de" + ], + "source_subject": "packages: order release gate checks", + "target_subject": "packages: order release gate checks", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-28T06:26:05Z", + "target_author_time": "2026-06-28T06:26:05Z", + "source_committer_time": "2026-06-28T06:26:05Z", + "target_committer_time": "2026-06-28T06:26:05Z", + "source_paths": [ + "tools/xtask/src/check.rs" + ], + "normalized_patch_sha256": "af866b45e0cd24426440175ec714824a27c8277075d7ef99168da34e0ca59447", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f0dab0a96e0f3983d934ae019b3bc65f14d59ab8", + "target_commit": "98f3202cffaf90d965fcdf6fd8325e7c298c653b", + "source_parents": [ + "bbcb47bc576dbdb25a9c68c387aebcfd021941f3" + ], + "target_parents": [ + "8cb1c8788c6e28af22e1d10cb31d613340f75842" + ], + "source_subject": "wasm: cover sdk wrapper logic", + "target_subject": "wasm: cover sdk wrapper logic", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T01:45:56Z", + "target_author_time": "2026-06-23T01:45:56Z", + "source_committer_time": "2026-06-23T01:45:56Z", + "target_committer_time": "2026-06-23T01:45:56Z", + "source_paths": [ + "crates/replica_sync_wasm/src/lib.rs" + ], + "normalized_patch_sha256": "1fb8e06713f735931abd543353ac33394ebdf95a4268b992ead6f87b91903abe", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f0f142265096cb8da7e2ede732e7c599bd243bdb", + "target_commit": "46ec9a40f3981d00654663abe226b8f33cd7eb39", + "source_parents": [ + "3020f5bb3370c0d84e28510761373d4b516491e5" + ], + "target_parents": [ + "6b38872141aa99e7bfe20b42e6473084abadead0" + ], + "source_subject": "bindings: align event dto imports", + "target_subject": "bindings: align event dto imports", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-26T15:00:07Z", + "target_author_time": "2026-06-26T15:00:07Z", + "source_committer_time": "2026-06-26T15:00:07Z", + "target_committer_time": "2026-06-26T15:00:07Z", + "source_paths": [ + "crates/sdk/src/market_runtime.rs", + "crates/sdk/tests/unit/market_runtime_tests.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs" + ], + "normalized_patch_sha256": "1971de80cc119a16622f4474482356e794f1efa152e1e6f4e55d42ed8e0f65c9", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f2da000795f566876a3b8ae99361fd1b01c81e66", + "target_commit": "eab6d04278cb9bbf5b1042e48148d64229382460", + "source_parents": [ + "aad1d4654a0edd0f5ef27603cdb8cc0a683df331" + ], + "target_parents": [ + "ae751835b8dfca06801cc41da4eed05819c5bf14" + ], + "source_subject": "coverage: enforce final sdk policy gates", + "target_subject": "coverage: enforce final sdk policy gates", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T07:39:51Z", + "target_author_time": "2026-06-23T07:39:51Z", + "source_committer_time": "2026-06-23T07:39:51Z", + "target_committer_time": "2026-06-23T07:39:51Z", + "source_paths": [ + "tools/xtask/src/coverage.rs", + "tools/xtask/src/coverage_policy.rs", + "tools/xtask/src/coverage_policy_tests.rs", + "tools/xtask/src/main.rs" + ], + "normalized_patch_sha256": "13f73b4254d44ca29b797f0b5a8a323e62171d6cead50537a4a012fe8b6cf4d8", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f3008c7bf40e5e116dba6bc7dc4b02a5509ee388", + "target_commit": "c66740d5851ebff78dbe8d8d9e6fb5f03ee91d8e", + "source_parents": [ + "16bdb7f65ec8ff902041d5736e6773e1684a5560" + ], + "target_parents": [ + "023faeb5a806a3494ebc08b42b0bda441dac41f7" + ], + "source_subject": "sdk: migrate reliability operations", + "target_subject": "sdk: migrate reliability operations", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:44:55Z", + "target_author_time": "2026-08-03T11:44:55Z", + "source_committer_time": "2026-08-03T11:44:55Z", + "target_committer_time": "2026-08-03T11:44:55Z", + "source_paths": [ + "crates/sdk/src/client.rs", + "crates/sdk/src/diagnostics.rs", + "crates/sdk/src/storage.rs", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "1c18185f9b676c4e62070d460288786f230231c1c6a8901d10230d4ae72c7bad", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f31cb78b0f3c74f7c85a7613eea380e5d49abea4", + "target_commit": "983667ec44193c8cc952f1e0a3423ae9f33ba74b", + "source_parents": [ + "5f51a1af8d85087d48dc57e9e53b3baede2eb2de" + ], + "target_parents": [ + "09f1b3c02b45a7ae01a0886c6ac8ab6f42e53924" + ], + "source_subject": "bindings: source constants and kinds", + "target_subject": "bindings: source constants and kinds", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-24T07:45:01Z", + "target_author_time": "2026-06-24T07:45:01Z", + "source_committer_time": "2026-06-24T07:45:01Z", + "target_committer_time": "2026-06-24T07:45:01Z", + "source_paths": [ + "crates/identity_bindings/Cargo.toml", + "crates/identity_bindings/src/lib.rs", + "tools/xtask/src/output.rs" + ], + "normalized_patch_sha256": "909fe44169068b87f16048eb5ae9b4ef2a76f11630757318d3ffcc3de553b354", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f3e863b1c50384378df18549c0d08e34732b1e1b", + "target_commit": "7132368233259ecb1887654fc163f4ec3a5cc018", + "source_parents": [ + "d4ded20214274879494d435ae2eaa65fb1c7e95a" + ], + "target_parents": [ + "b0075d086090759654a20c046ef62bbaf936c98e" + ], + "source_subject": "sdk: document queued sync targets", + "target_subject": "sdk: document queued sync targets", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-16T00:20:15-07:00", + "target_author_time": "2026-06-16T00:20:15-07:00", + "source_committer_time": "2026-06-16T00:20:15-07:00", + "target_committer_time": "2026-06-16T00:20:15-07:00", + "source_paths": [ + "crates/sdk/README" + ], + "normalized_patch_sha256": "50a9bfb43e3a4eb742505af72be369d1aab00ded907462a476c94913cf9de72c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f4478a4fb2a758bf0862dde64aea529de3e98471", + "target_commit": "dc4f3bf20089bf49d86bab378fdd31a22f3f8484", + "source_parents": [ + "65263961fe1471d6516af1848499bd95d82cf0b0" + ], + "target_parents": [ + "bcb30a70846629bc858c563200ac21e8b9ab79e7" + ], + "source_subject": "facade: document public api contract", + "target_subject": "facade: document public api contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T13:03:50Z", + "target_author_time": "2026-08-03T13:03:50Z", + "source_committer_time": "2026-08-03T13:03:50Z", + "target_committer_time": "2026-08-03T13:03:50Z", + "source_paths": [ + "crates/radroots/README.md" + ], + "normalized_patch_sha256": "9f2fe2c84be946aafa3318b74c2babafc28f06f5c2bc6491454d4f3c1abed0af", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f49393558a17c226bee0234138f0187258991601", + "target_commit": "caf57a3332451c3f1cf8e0ed360e51e86cada81e", + "source_parents": [ + "70014334c649b1b07bd7740be857daab6566e55d" + ], + "target_parents": [ + "2e911103ac9453c9735013228e158a633d6806f6" + ], + "source_subject": "sdk: add restore archive contract", + "target_subject": "sdk: add restore archive contract", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-17T17:51:05-07:00", + "target_author_time": "2026-06-17T17:51:05-07:00", + "source_committer_time": "2026-06-17T17:51:05-07:00", + "target_committer_time": "2026-06-17T17:51:05-07:00", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "269aa46fd83a2b7df25fcea1d95ff00c45bca8a20d8e3ae9a042d343673308f6", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f5038dbc581eb48f4b238b7a5e2732948281abfb", + "target_commit": "06b5d28cd6a4c4388117db0757e4544c5fef95a0", + "source_parents": [ + "e0131fcb84123ce5939f4eec38d99f7b484023f5" + ], + "target_parents": [ + "ed0710cc7011a29b6f8cc6dcfa5b9f5f2b3887c0" + ], + "source_subject": "sdk: preflight workflow idempotency", + "target_subject": "sdk: preflight workflow idempotency", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-30T09:58:29Z", + "target_author_time": "2026-06-30T09:58:29Z", + "source_committer_time": "2026-06-30T09:58:29Z", + "target_committer_time": "2026-06-30T09:58:29Z", + "source_paths": [ + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/orders_runtime.rs", + "crates/sdk/tests/unit/orders_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs" + ], + "normalized_patch_sha256": "8a5602e2abfccb790ace884ac05ded9a0406dde44bb76b9636ce19ce3549f3d0", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f7a7d8bf62211ac090c922ba0a917bf3548c5b2f", + "target_commit": "558a8c97318cc8dbe55bf2b691f294e3ae466e5e", + "source_parents": [ + "8cea39ce833e5136a2a166a51b2670ec4bf8a20f" + ], + "target_parents": [ + "b704b8030c8d6dbc82cc43bfdbfced327c0c6f09" + ], + "source_subject": "sdk: implement the approved SDK feature vocabulary", + "target_subject": "sdk: implement the approved SDK feature vocabulary", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-03T11:54:12Z", + "target_author_time": "2026-08-03T11:54:12Z", + "source_committer_time": "2026-08-03T11:54:12Z", + "target_committer_time": "2026-08-03T11:54:12Z", + "source_paths": [ + "crates/sdk/Cargo.toml", + "crates/sdk/README.md", + "crates/sdk/tests/package_boundary.rs" + ], + "normalized_patch_sha256": "1c665ef8109c2a25f2efa83560d38b636a1cee677eaaf110fb2f980167dd89aa", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f7ba91188d3bf4266f6db72bb50c51d03ef3c99c", + "target_commit": null, + "source_parents": [ + "2f172e208550f7ef0177c138bf174d38ca675784" + ], + "target_parents": [], + "source_subject": "chore: align binding model", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-06-11T14:05:50-07:00", + "target_author_time": null, + "source_committer_time": "2026-06-11T14:05:50-07:00", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "f8a33b695a9234ffd0c6bd57b5ba0102f3d3819f", + "target_commit": "74bb6888161dc69919d1d58d4c86e5a0fa324b5e", + "source_parents": [ + "7b21fd126a677eeabe3dc7b2d5901d9946465101" + ], + "target_parents": [ + "e44f66b6fd9bf04b93d0269b669ba634e0ea8d1f" + ], + "source_subject": "release-product: remove validation receipt SDK surface", + "target_subject": "release-product: remove validation receipt SDK surface", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-16T21:06:47Z", + "target_author_time": "2026-07-16T21:06:47Z", + "source_committer_time": "2026-07-16T21:06:47Z", + "target_committer_time": "2026-07-16T21:06:47Z", + "source_paths": [ + "crates/sdk/README", + "crates/sdk/src/lib.rs", + "crates/sdk/src/sync_runtime.rs", + "tools/xtask/src/cli_host.rs" + ], + "normalized_patch_sha256": "50bbe160dc23e8e028cc8c88d3991ddb93ce2eb38e2e3ed92a81c39baaa9415c", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f96206c243f1d3ea3db4e60133c27d6388b238ac", + "target_commit": null, + "source_parents": [ + "e424cbf3dcb2ba2b336ce5173b62e1b0e79bfb53" + ], + "target_parents": [], + "source_subject": "workspace: repair independent cargo lock", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-27T08:12:18Z", + "target_author_time": null, + "source_committer_time": "2026-07-27T08:12:18Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "f987cde525c7752152579bbed03a818d4658e171", + "target_commit": "b9bf25b4972dc36688fb823feafb0046c86602ce", + "source_parents": [ + "01e989020cdb3632400b4a22e4702494146033f1" + ], + "target_parents": [ + "028b84f7f095e2758c2ee2f7a857c03248a6d833" + ], + "source_subject": "sync: preserve direct Nostr target metadata", + "target_subject": "sync: preserve direct Nostr target metadata", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-10T03:40:33Z", + "target_author_time": "2026-07-10T03:40:33Z", + "source_committer_time": "2026-07-10T03:40:33Z", + "target_committer_time": "2026-07-10T03:40:33Z", + "source_paths": [ + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs" + ], + "normalized_patch_sha256": "eb0f46cd76d9181b43b2b68baa379b33eec1c53f8d7b011a40f23768207f22e3", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "f9a6f13f31140f26f9bda8a28aab1bafec00f6fc", + "target_commit": "9600dfa4b30820660154d1e28d95a7e83344d103", + "source_parents": [ + "a97af18da6b38d9d68bda6b1d4a9dae2070fe1ef" + ], + "target_parents": [ + "3047f3f95183149697babe97c8711143ed6ed226" + ], + "source_subject": "sdk: classify local validation worker evidence", + "target_subject": "sdk: classify local validation worker evidence", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-01T20:53:32Z", + "target_author_time": "2026-07-01T20:53:32Z", + "source_committer_time": "2026-07-01T20:53:32Z", + "target_committer_time": "2026-07-01T20:53:32Z", + "source_paths": [ + "crates/sdk/src/orders_runtime.rs" + ], + "normalized_patch_sha256": "2774e8449aca1e41f599ee217cac2eba108f44cdaac8e62bb4226c137c84689e", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fa617f1a61f01b264ec17a059c485fe7cfa99cba", + "target_commit": "82b7095680ad0d1c9cb9de0dfc68d8c6acd71799", + "source_parents": [ + "e50c1fa6525ce5a10b3fa0787cc2cb84bb9ad3a8" + ], + "target_parents": [ + "587e5e139f79e6efac02454e6708e5a05918b075" + ], + "source_subject": "coverage: enforce scoped policy gates", + "target_subject": "coverage: enforce scoped policy gates", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T02:21:39Z", + "target_author_time": "2026-06-23T02:21:39Z", + "source_committer_time": "2026-06-23T02:21:39Z", + "target_committer_time": "2026-06-23T02:21:39Z", + "source_paths": [ + "tools/xtask/src/coverage.rs" + ], + "normalized_patch_sha256": "72377cc178f0f9c998a82dadd5d77fceb3dbb78a3ff80b3687604237440baf78", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "faacecec47f2727159ba4d53b0fe9d53e8786732", + "target_commit": "a4eb86587fdf71fa8080e5a64aa28eeafa113924", + "source_parents": [ + "3aab8706a0d9da8e7b06f4dc8d9a5e0844da4233" + ], + "target_parents": [ + "79ff8c411788096c014b6478585207358274d917" + ], + "source_subject": "tests: prove SDK local import rows", + "target_subject": "tests: prove SDK local import rows", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-08T11:04:19Z", + "target_author_time": "2026-07-08T11:04:19Z", + "source_committer_time": "2026-07-08T11:04:19Z", + "target_committer_time": "2026-07-08T11:04:19Z", + "source_paths": [ + "crates/sdk/tests/sync_runtime.rs" + ], + "normalized_patch_sha256": "0744ecefa20a2b752dd9f79c5662dd40457554840179a0156589ae6ff8a7f708", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fafa24eb55ced4742c43128d40fb559364a063dc", + "target_commit": "c79b55935b72922d9ca8790dbda1ea6c2e08f2dc", + "source_parents": [ + "6ad6e010fcc5323078ddf0d7f04486e402498d81" + ], + "target_parents": [ + "eefd990d961244a742d126feb3b50279c920b18e" + ], + "source_subject": "orders: cover runtime branch surfaces", + "target_subject": "orders: cover runtime branch surfaces", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-06-23T02:38:09Z", + "target_author_time": "2026-06-23T02:38:09Z", + "source_committer_time": "2026-06-23T02:38:09Z", + "target_committer_time": "2026-06-23T02:38:09Z", + "source_paths": [ + "crates/sdk/tests/facade.rs", + "crates/sdk/tests/orders_runtime.rs" + ], + "normalized_patch_sha256": "cb2b316e3f1ab5c92287a64850b154b7da332e66cfe28e0b2e84b6cebbc8a64f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fb92badb3815ffeea38a3f233c78fc5153a249f8", + "target_commit": null, + "source_parents": [ + "a10f2f320c3b54effbe8c48f3cf5129093ab3dda" + ], + "target_parents": [], + "source_subject": "repo: remove hosted workflow automation", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-29T23:28:35Z", + "target_author_time": null, + "source_committer_time": "2026-07-29T23:28:35Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "fc71e707191ca282364f944541a968c14e8c0010", + "target_commit": "5ed07d69ae5739867e40aa706b5bce99d6f6cbb0", + "source_parents": [ + "602a9095572f9f2c02815104b5559b44c5cb326b" + ], + "target_parents": [ + "ab2bda16beed08db8819299d4cda167d89428da2" + ], + "source_subject": "sdk: align foundational event workflows", + "target_subject": "sdk: align foundational event workflows", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-20T00:55:23Z", + "target_author_time": "2026-07-20T00:55:23Z", + "source_committer_time": "2026-07-20T00:55:23Z", + "target_committer_time": "2026-07-20T00:55:23Z", + "source_paths": [ + "crates/event_bindings/src/lib.rs", + "crates/event_bindings/src/model.rs", + "crates/event_codec_wasm/Cargo.toml", + "crates/event_codec_wasm/src/lib.rs", + "crates/replica_sync_wasm/src/lib.rs", + "crates/sdk/examples/runtime_local.rs", + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/examples/sdk_v1_listing_prepare.rs", + "crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs", + "crates/sdk/src/error.rs", + "crates/sdk/src/farms_runtime.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/src/listings_runtime.rs", + "crates/sdk/src/private_store.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/src/signer_provider.rs", + "crates/sdk/src/sync_runtime.rs", + "crates/sdk/src/trade_runtime.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/farms_runtime.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/sdk/tests/listings_runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/source_boundary.rs", + "crates/sdk/tests/support/fixture_signer.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/error_tests.rs", + "crates/sdk/tests/unit/farms_runtime_tests.rs", + "crates/sdk/tests/unit/listings_runtime_tests.rs", + "crates/sdk/tests/unit/runtime_tests.rs", + "crates/sdk/tests/unit/signer_provider_tests.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/trade_runtime_tests.rs", + "crates/sdk/tests/unit/workflow_runtime_tests.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/smoke.rs", + "tools/xtask/src/wasm_declarations.rs" + ], + "normalized_patch_sha256": "b2502145831358592fbc765b324244324a1cd983783a8985f19433ddd8eaeb5f", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fd8384aee348034e0c8ea17a868fe7f094770050", + "target_commit": null, + "source_parents": [ + "fc71e707191ca282364f944541a968c14e8c0010" + ], + "target_parents": [], + "source_subject": "sqlite: use registry bundled runtime", + "target_subject": null, + "source_author": "triesap <tyson@radroots.org>", + "target_author": null, + "source_author_time": "2026-07-22T20:53:16Z", + "target_author_time": null, + "source_committer_time": "2026-07-22T20:53:16Z", + "target_committer_time": null, + "source_paths": [], + "normalized_patch_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "empty_commit_disposition": "out_of_scope" + }, + { + "source_commit": "fde2294678edc117625e94c6735fac1250fe6b5c", + "target_commit": "3bbc182d4c51815e8f9bdedf7f32d5c2950f9546", + "source_parents": [ + "9e1462ae7e9acd785ca09acf0cc81049a68d262d" + ], + "target_parents": [ + "31e0f07fa55a06289eb4b7f7e0d7b5262f1cbafb" + ], + "source_subject": "knowledge-sdk: add fluent knowledge builders", + "target_subject": "knowledge-sdk: add fluent knowledge builders", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T20:39:57Z", + "target_author_time": "2026-07-05T20:39:57Z", + "source_committer_time": "2026-07-05T20:39:57Z", + "target_committer_time": "2026-07-05T20:39:57Z", + "source_paths": [ + "crates/sdk/examples/sdk_v1_knowledge_prepare.rs", + "crates/sdk/src/knowledge.rs", + "crates/sdk/src/lib.rs", + "crates/sdk/tests/knowledge_public_api.rs", + "crates/trade_bindings/src/dto.rs", + "crates/trade_bindings/src/lib.rs", + "tools/xtask/src/dto_roots.rs", + "tools/xtask/src/smoke.rs" + ], + "normalized_patch_sha256": "62cbbbfa6ea5b01919a8e938cf165c014eaf97b0b7bdc7b50c8bf559e24b1378", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fdf5e1bcbd9d6173408e949580112240d861bbbc", + "target_commit": "df5ed1acf9e5a2f854d57b420eded2bd2a450dcc", + "source_parents": [ + "fde2294678edc117625e94c6735fac1250fe6b5c" + ], + "target_parents": [ + "3bbc182d4c51815e8f9bdedf7f32d5c2950f9546" + ], + "source_subject": "runtime: split storage checkpoint evidence", + "target_subject": "runtime: split storage checkpoint evidence", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-05T22:07:07Z", + "target_author_time": "2026-07-05T22:07:07Z", + "source_committer_time": "2026-07-05T22:07:07Z", + "target_committer_time": "2026-07-05T22:07:07Z", + "source_paths": [ + "crates/sdk/src/lib.rs", + "crates/sdk/src/runtime.rs", + "crates/sdk/tests/runtime_foundation.rs", + "crates/sdk/tests/unit/runtime_tests.rs" + ], + "normalized_patch_sha256": "73ca18efa5eb46dd596cb823aa1ef6d95bd9e2c495494f916ee9e4ef89852aad", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fe1325675b43341e57999df8bd0d0f929cf0e5c4", + "target_commit": "925d3a35f04568501a2c1b349b14adced4471576", + "source_parents": [ + "4cd485179a5f56a95fe54bcbd556becd239d1f03" + ], + "target_parents": [ + "172f6a83e8877b3afebfc015000be50845321525" + ], + "source_subject": "refactor: adopt authored operations v2", + "target_subject": "refactor: adopt authored operations v2", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-08-05T14:36:42Z", + "target_author_time": "2026-08-05T14:36:42Z", + "source_committer_time": "2026-08-05T14:36:42Z", + "target_committer_time": "2026-08-05T14:36:42Z", + "source_paths": [ + "crates/radroots/src/client.rs", + "crates/radroots/src/event.rs", + "crates/radroots/tests/public_surface.rs", + "crates/sdk/src/client.rs", + "crates/sdk/src/farm.rs", + "crates/sdk/src/listing.rs", + "crates/sdk/src/listing/operational_listing/mod.rs", + "crates/sdk/src/listing/operational_listing/mutation.rs", + "crates/sdk/src/signing.rs", + "crates/sdk/src/sync.rs", + "crates/sdk/src/trade.rs", + "crates/sdk/src/transport.rs", + "crates/sdk/tests/package_boundary.rs", + "tools/xtask/src/api_qualification.rs", + "tools/xtask/src/release_graph.rs", + "tools/xtask/src/supply_chain_qualification.rs" + ], + "normalized_patch_sha256": "6fa5e3d45895a355cf6707e58fe5fe21a18c275f7faec5473374e70e9c32af0a", + "empty_commit_disposition": "retained" + }, + { + "source_commit": "fec49a2b5119ed770763307a811d71d2fdeeb36f", + "target_commit": "99baec80e5b479e9c7c3a653e309ddfe3bc68914", + "source_parents": [ + "4dce5873dedc247e3dca34352a8ebba5be9cb89f" + ], + "target_parents": [ + "6a9cdfb1bda0181ab824967433cac737e3ceb79a" + ], + "source_subject": "sdk: use typed transport target constructors", + "target_subject": "sdk: use typed transport target constructors", + "source_author": "triesap <tyson@radroots.org>", + "target_author": "triesap <tyson@radroots.org>", + "source_author_time": "2026-07-13T07:11:05Z", + "target_author_time": "2026-07-13T07:11:05Z", + "source_committer_time": "2026-07-13T07:11:05Z", + "target_committer_time": "2026-07-13T07:11:05Z", + "source_paths": [ + "crates/sdk/src/transport.rs", + "crates/sdk/src/workflow_runtime.rs", + "crates/sdk/tests/sync_runtime.rs", + "crates/sdk/tests/unit/sync_runtime_tests.rs", + "crates/sdk/tests/unit/transport_tests.rs" + ], + "normalized_patch_sha256": "214883ad507bbd0a56d9b321474ee1de5f48124b0430fa4712f7f763bd54e096", + "empty_commit_disposition": "retained" + } + ] +} diff --git a/contracts/coverage.toml b/contracts/coverage.toml @@ -22,12 +22,17 @@ reason = "branch coverage is not applicable while the crate has no measured bran [required] crates = [ + "radroots", "radroots_blossom", "radroots_core", + "radroots_core_bindings", "radroots_event", + "radroots_event_bindings", "radroots_event_codec", + "radroots_event_codec_wasm", "radroots_geonames", "radroots_identity", + "radroots_identity_bindings", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", @@ -43,14 +48,21 @@ crates = [ "radroots_transport_nostr", "radroots_transport_reticulum", "radroots_replica_store", + "radroots_replica_store_wasm", "radroots_replica_schema", + "radroots_replica_schema_bindings", "radroots_replica_sync", + "radroots_replica_sync_wasm", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_sql_core", + "radroots_sdk", + "radroots_sdk_ffi", + "radroots_sdk_sql_wasm_runtime", "radroots_test_fixtures", "radroots_trade", + "radroots_trade_bindings", "radroots_transport", "xtask", ] diff --git a/contracts/crates/catalog.v1.toml b/contracts/crates/catalog.v1.toml @@ -769,14 +769,14 @@ replaces = [] [[package]] name = "radroots_sdk" path = "crates/sdk" -state = "reserved_import" +state = "active" tier = "sdk" visibility = "public_release" publish = true version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["any"] -groups = ["portable", "public_native", "sdk"] +groups = ["coverage_required", "portable", "public_native", "sdk"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk"] source_repository = "https://github.com/radrootslabs/sdk" @@ -784,20 +784,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/sdk" source_tree_sha256 = "6cc78f544bd0a0d74783cbba118ea4815ce4b4bd094433193844621352411efb" compatibility = ["rust_api", "features", "package", "product"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots" path = "crates/radroots" -state = "reserved_import" +state = "active" tier = "facade" visibility = "public_release" publish = true version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["any"] -groups = ["portable", "public_native", "sdk"] +groups = ["coverage_required", "portable", "public_native", "sdk"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade"] source_repository = "https://github.com/radrootslabs/sdk" @@ -805,20 +804,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/radroots" source_tree_sha256 = "7c9b4392eb7e8d8e799f4a62c9886f349161bad440659beb0d1a739911ab1425" compatibility = ["rust_api", "features", "package", "product"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_core_bindings" path = "crates/core_bindings" -state = "reserved_import" +state = "active" tier = "codegen" visibility = "private_codegen" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "codegen"] source_repository = "https://github.com/radrootslabs/sdk" @@ -826,20 +824,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/core_bindings" source_tree_sha256 = "f1a72bde7364179687bbdf6c4dacddacd504c52a884af941ce3557bae27ee981" compatibility = ["generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_event_bindings" path = "crates/event_bindings" -state = "reserved_import" +state = "active" tier = "codegen" visibility = "private_codegen" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "codegen"] source_repository = "https://github.com/radrootslabs/sdk" @@ -847,20 +844,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/event_bindings" source_tree_sha256 = "32f23a76a5c135de841cae623a28de3424dace3b6ba46af8c2cccc35b7c62f09" compatibility = ["generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_identity_bindings" path = "crates/identity_bindings" -state = "reserved_import" +state = "active" tier = "codegen" visibility = "private_codegen" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "codegen"] source_repository = "https://github.com/radrootslabs/sdk" @@ -868,20 +864,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/identity_bindings" source_tree_sha256 = "a0cf1539805704195cf82f9ffffa06e443a9c1cabd90a47bfcf6110f20549acd" compatibility = ["generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_trade_bindings" path = "crates/trade_bindings" -state = "reserved_import" +state = "active" tier = "codegen" visibility = "private_codegen" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "codegen"] source_repository = "https://github.com/radrootslabs/sdk" @@ -889,20 +884,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/trade_bindings" source_tree_sha256 = "28a60e08d65a891961189d5e614bbc008fe36574b3823333375339c589236342" compatibility = ["generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_replica_schema_bindings" path = "crates/replica_schema_bindings" -state = "reserved_import" +state = "active" tier = "codegen" visibility = "private_codegen" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "sdk", "facade", "codegen"] source_repository = "https://github.com/radrootslabs/sdk" @@ -910,20 +904,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/replica_schema_bindings" source_tree_sha256 = "41aeb6520962da64f13e09449d03b6df901b788965732eff439b282fe8f8abb1" compatibility = ["generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_event_codec_wasm" path = "crates/event_codec_wasm" -state = "reserved_import" +state = "active" tier = "boundary" visibility = "private_boundary" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["wasm32"] -groups = ["sdk", "wasm", "boundaries"] +groups = ["coverage_required", "sdk", "wasm", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "boundary"] source_repository = "https://github.com/radrootslabs/sdk" @@ -931,20 +924,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/event_codec_wasm" source_tree_sha256 = "698abf3b77aa87b6460432342908b475c55bb55d285dc9aef24e27239e4cfa3c" compatibility = ["wasm", "generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_replica_store_wasm" path = "crates/replica_store_wasm" -state = "reserved_import" +state = "active" tier = "boundary" visibility = "private_boundary" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["wasm32"] -groups = ["sdk", "wasm", "boundaries"] +groups = ["coverage_required", "sdk", "wasm", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "sdk", "facade", "boundary"] source_repository = "https://github.com/radrootslabs/sdk" @@ -952,20 +944,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/replica_store_wasm" source_tree_sha256 = "59cb050d0493047b57aebc4e1bb3adfd5af2fcd464de1f76ffabc1dfabe2baad" compatibility = ["wasm", "generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_replica_sync_wasm" path = "crates/replica_sync_wasm" -state = "reserved_import" +state = "active" tier = "boundary" visibility = "private_boundary" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["wasm32"] -groups = ["sdk", "wasm", "boundaries"] +groups = ["coverage_required", "sdk", "wasm", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "preview", "sdk", "facade", "boundary"] source_repository = "https://github.com/radrootslabs/sdk" @@ -973,20 +964,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/replica_sync_wasm" source_tree_sha256 = "95f2a291066bab18c744f50a910feecc913dd50a4a3b6c9a464247695d8faffc" compatibility = ["wasm", "generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_sdk_ffi" path = "crates/sdk_ffi" -state = "reserved_import" +state = "active" tier = "boundary" visibility = "private_boundary" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["native"] -groups = ["sdk", "boundaries"] +groups = ["coverage_required", "sdk", "boundaries"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter", "orchestration", "sdk", "facade", "boundary"] source_repository = "https://github.com/radrootslabs/sdk" @@ -994,20 +984,19 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/ffi" source_tree_sha256 = "f07e2087f0506b146848476048b32b01584905e2928fe56a36d4f90663590e33" compatibility = ["ffi", "generated", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] name = "radroots_sdk_sql_wasm_runtime" path = "crates/sdk_sql_wasm_runtime" -state = "reserved_import" +state = "active" tier = "adapter" visibility = "private_adapter" publish = false version = "0.1.0-alpha" license = "MIT OR Apache-2.0" platforms = ["wasm32"] -groups = ["sdk", "wasm"] +groups = ["coverage_required", "sdk", "wasm"] owners = ["sdk"] permitted_dependency_tiers = ["foundation", "domain", "spi", "adapter"] source_repository = "https://github.com/radrootslabs/sdk" @@ -1015,7 +1004,6 @@ source_revision = "170ecf2b620107fadca85fcb11fbf3798b4ca1ef" source_path = "crates/sql_wasm_runtime" source_tree_sha256 = "6ca1dca6f248c50b6bcd4c684b4ad31acd17b8bfc36a8b0072872f6db927c303" compatibility = ["wasm", "data", "package_private"] -removal_gate = "sdk_history_import_verified" replaces = [] [[package]] diff --git a/contracts/crates/generated/package_groups.v1.toml b/contracts/crates/generated/package_groups.v1.toml @@ -1,16 +1,16 @@ schema = "radroots.workspace.package-groups.v1" -catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace" +catalog_sha256 = "08a97c61225ff876cad7e00739c421275d17766d602eff0bc0bac72171865c8e" [[group]] id = "boundaries" packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mobile_bindgen", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_ffi", "radroots_studio_ffi", "radroots_studio_uniffi_bindgen", "radroots_trade_bindings"] -active_packages = [] -reserved_packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mobile_bindgen", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_ffi", "radroots_studio_ffi", "radroots_studio_uniffi_bindgen", "radroots_trade_bindings"] +active_packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_ffi", "radroots_trade_bindings"] +reserved_packages = ["radroots_mobile_bindgen", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_studio_ffi", "radroots_studio_uniffi_bindgen"] [[group]] id = "coverage_required" -packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"] -active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_store", "radroots_replica_sync", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"] +packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_core_bindings", "radroots_event", "radroots_event_bindings", "radroots_event_codec", "radroots_event_codec_wasm", "radroots_geonames", "radroots_identity", "radroots_identity_bindings", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_schema_bindings", "radroots_replica_store", "radroots_replica_store_wasm", "radroots_replica_sync", "radroots_replica_sync_wasm", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_trade_bindings", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"] +active_packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_core_bindings", "radroots_event", "radroots_event_bindings", "radroots_event_codec", "radroots_event_codec_wasm", "radroots_geonames", "radroots_identity", "radroots_identity_bindings", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_nostr", "radroots_nostr_connect", "radroots_nostrdb", "radroots_protocol", "radroots_replica_schema", "radroots_replica_schema_bindings", "radroots_replica_store", "radroots_replica_store_wasm", "radroots_replica_sync", "radroots_replica_sync_wasm", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_secrets", "radroots_signing", "radroots_sql_core", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_test_fixtures", "radroots_trade", "radroots_trade_bindings", "radroots_transport", "radroots_transport_nostr", "radroots_transport_reticulum", "xtask"] reserved_packages = [] [[group]] @@ -22,8 +22,8 @@ reserved_packages = ["radroots_mobile_bindgen", "radroots_mobile_core", "radroot [[group]] id = "portable" packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] -active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] -reserved_packages = ["radroots", "radroots_sdk"] +active_packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] +reserved_packages = [] [[group]] id = "preview" @@ -34,14 +34,14 @@ reserved_packages = [] [[group]] id = "public_native" packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] -active_packages = ["radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] -reserved_packages = ["radroots", "radroots_sdk"] +active_packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] +reserved_packages = [] [[group]] id = "sdk" packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_trade_bindings"] -active_packages = [] -reserved_packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_trade_bindings"] +active_packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_trade_bindings"] +reserved_packages = [] [[group]] id = "studio" @@ -58,5 +58,5 @@ reserved_packages = [] [[group]] id = "wasm" packages = ["radroots_event_codec_wasm", "radroots_mobile_wasm", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_sql_wasm_runtime", "radroots_sql_core"] -active_packages = ["radroots_sql_core"] -reserved_packages = ["radroots_event_codec_wasm", "radroots_mobile_wasm", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_sql_wasm_runtime"] +active_packages = ["radroots_event_codec_wasm", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk_sql_wasm_runtime", "radroots_sql_core"] +reserved_packages = ["radroots_mobile_wasm"] diff --git a/contracts/crates/generated/platform_inventory.v1.toml b/contracts/crates/generated/platform_inventory.v1.toml @@ -1,5 +1,5 @@ schema = "radroots.workspace.platform-inventory.v1" -catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace" +catalog_sha256 = "08a97c61225ff876cad7e00739c421275d17766d602eff0bc0bac72171865c8e" [[platform]] id = "android" diff --git a/contracts/crates/generated/release_inventory.v2.toml b/contracts/crates/generated/release_inventory.v2.toml @@ -1,7 +1,7 @@ schema = "radroots.workspace.release-inventory.v2" -catalog_sha256 = "0b1d39e687f499a58c7d3a1ffee6b8c07210b61d3b94c7c2a18681fe3a005ace" +catalog_sha256 = "08a97c61225ff876cad7e00739c421275d17766d602eff0bc0bac72171865c8e" architecture = "radroots.crates.release.v2" version = "0.1.0-alpha" public_packages = ["radroots", "radroots_blossom", "radroots_core", "radroots_event", "radroots_event_codec", "radroots_geonames", "radroots_identity", "radroots_nostr", "radroots_nostr_connect", "radroots_protocol", "radroots_sdk", "radroots_secrets", "radroots_signing", "radroots_storage", "radroots_storage_sqlite", "radroots_sync", "radroots_trade", "radroots_transport", "radroots_transport_nostr"] private_packages = ["radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mesh", "radroots_mesh_agent_client", "radroots_mesh_agent_proto", "radroots_mobile_bindgen", "radroots_mobile_core", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_nostrdb", "radroots_replica_schema", "radroots_replica_schema_bindings", "radroots_replica_store", "radroots_replica_store_wasm", "radroots_replica_sync", "radroots_replica_sync_wasm", "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_simplex_agent_proto", "radroots_simplex_app_store", "radroots_simplex_chat_proto", "radroots_simplex_smp_crypto", "radroots_simplex_smp_proto", "radroots_simplex_smp_transport", "radroots_sql_core", "radroots_studio_application", "radroots_studio_domain", "radroots_studio_ffi", "radroots_studio_nostr", "radroots_studio_runtime", "radroots_studio_storage", "radroots_studio_uniffi_bindgen", "radroots_test_fixtures", "radroots_trade_bindings", "radroots_transport_reticulum", "xtask"] -reserved_packages = ["radroots", "radroots_core_bindings", "radroots_event_bindings", "radroots_event_codec_wasm", "radroots_identity_bindings", "radroots_mobile_bindgen", "radroots_mobile_core", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_replica_schema_bindings", "radroots_replica_store_wasm", "radroots_replica_sync_wasm", "radroots_sdk", "radroots_sdk_ffi", "radroots_sdk_sql_wasm_runtime", "radroots_studio_application", "radroots_studio_domain", "radroots_studio_ffi", "radroots_studio_nostr", "radroots_studio_runtime", "radroots_studio_storage", "radroots_studio_uniffi_bindgen", "radroots_trade_bindings"] +reserved_packages = ["radroots_mobile_bindgen", "radroots_mobile_core", "radroots_mobile_ffi", "radroots_mobile_wasm", "radroots_studio_application", "radroots_studio_domain", "radroots_studio_ffi", "radroots_studio_nostr", "radroots_studio_runtime", "radroots_studio_storage", "radroots_studio_uniffi_bindgen"] diff --git a/contracts/releases/publish_policy.toml b/contracts/releases/publish_policy.toml @@ -45,17 +45,31 @@ local_packages = [ "radroots_transport_nostr", "radroots_sync", "radroots_geonames", + "radroots_sdk", + "radroots", ] -external_packages = ["radroots_sdk", "radroots"] +external_packages = [] [workspace_classification] private = [ "radroots_runtime_distribution", "radroots_runtime_manager", "radroots_runtime_paths", + "radroots_sdk_ffi", "radroots_sql_core", ] -build_codegen = ["xtask"] +build_codegen = [ + "radroots_core_bindings", + "radroots_event_bindings", + "radroots_event_codec_wasm", + "radroots_identity_bindings", + "radroots_replica_schema_bindings", + "radroots_replica_store_wasm", + "radroots_replica_sync_wasm", + "radroots_sdk_sql_wasm_runtime", + "radroots_trade_bindings", + "xtask", +] test_support = ["radroots_test_fixtures"] preview = [ "radroots_mesh", @@ -94,4 +108,6 @@ crates = [ "radroots_transport_nostr", "radroots_sync", "radroots_geonames", + "radroots_sdk", + "radroots", ] diff --git a/crates/core_bindings/Cargo.toml b/crates/core_bindings/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "radroots_core_bindings" +description = "Generated bindings for Radroots core primitives" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[dependencies] +dto_bindgen = { workspace = true } +dto_bindgen_core = { workspace = true } +radroots_core = { workspace = true, features = ["serde", "std"] } diff --git a/crates/core_bindings/src/dto.rs b/crates/core_bindings/src/dto.rs @@ -0,0 +1,139 @@ +#![allow(dead_code)] + +use dto_bindgen_core::RootDescriptor; + +pub fn dto_roots() -> Vec<RootDescriptor> { + vec![ + RootDescriptor::new::<DiscountDescriptor>(), + RootDescriptor::new::<DiscountScopeDescriptor>(), + RootDescriptor::new::<DiscountThresholdDescriptor>(), + RootDescriptor::new::<DiscountValueDescriptor>(), + RootDescriptor::new::<MoneyDescriptor>(), + RootDescriptor::new::<PercentDescriptor>(), + RootDescriptor::new::<QuantityDescriptor>(), + RootDescriptor::new::<QuantityPriceDescriptor>(), + RootDescriptor::new::<UnitDescriptor>(), + RootDescriptor::new::<UnitDimensionDescriptor>(), + ] +} + +#[derive(dto_bindgen::Dto)] +#[dto(as = "string")] +#[dto(ts(name = "Currency"))] +struct CurrencyDescriptor(String); + +#[derive(dto_bindgen::Dto)] +#[dto(as = "string")] +#[dto(ts(name = "Decimal"))] +struct DecimalDescriptor(String); + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "Money"))] +struct MoneyDescriptor { + amount: DecimalDescriptor, + currency: CurrencyDescriptor, +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "Percent"))] +struct PercentDescriptor { + value: DecimalDescriptor, +} + +#[derive(dto_bindgen::Dto)] +#[dto(as = "string_enum")] +#[dto(ts(name = "Unit"))] +enum UnitDescriptor { + #[dto(rename = "each")] + Each, + #[dto(rename = "kg")] + MassKg, + #[dto(rename = "g")] + MassG, + #[dto(rename = "oz")] + MassOz, + #[dto(rename = "lb")] + MassLb, + #[dto(rename = "l")] + VolumeL, + #[dto(rename = "ml")] + VolumeMl, +} + +#[derive(dto_bindgen::Dto)] +#[dto(as = "string_enum")] +#[dto(ts(name = "UnitDimension"))] +enum UnitDimensionDescriptor { + #[dto(rename = "count")] + Count, + #[dto(rename = "mass")] + Mass, + #[dto(rename = "volume")] + Volume, +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "Quantity"))] +struct QuantityDescriptor { + amount: DecimalDescriptor, + unit: UnitDescriptor, + #[serde(skip_serializing_if = "Option::is_none")] + label: Option<String>, +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "QuantityPrice"))] +struct QuantityPriceDescriptor { + amount: MoneyDescriptor, + quantity: QuantityDescriptor, +} + +#[derive(dto_bindgen::Dto)] +#[dto(as = "string_enum")] +#[dto(ts(name = "DiscountScope"))] +enum DiscountScopeDescriptor { + #[dto(rename = "bin")] + Bin, + #[dto(rename = "order_total")] + OrderTotal, +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "DiscountThreshold"))] +#[serde(rename_all = "snake_case", tag = "kind", content = "amount")] +enum DiscountThresholdDescriptor { + BinCount { bin_id: String, min: u32 }, + OrderQuantity { min: QuantityDescriptor }, +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "DiscountValue"))] +#[serde(rename_all = "snake_case", tag = "kind", content = "amount")] +enum DiscountValueDescriptor { + MoneyPerBin(MoneyDescriptor), + Percent(PercentDescriptor), +} + +#[derive(dto_bindgen::Dto)] +#[dto(ts(name = "Discount"))] +struct DiscountDescriptor { + scope: DiscountScopeDescriptor, + threshold: DiscountThresholdDescriptor, + value: DiscountValueDescriptor, +} + +#[cfg(test)] +mod tests { + use dto_bindgen_core::build_registry; + + use super::dto_roots; + + #[test] + fn canonical_core_descriptor_roots_build_without_diagnostics() { + let roots = dto_roots(); + let registry = build_registry(roots.clone()); + + assert!(!registry.has_errors(), "{:?}", registry.diagnostics); + assert_eq!(registry.roots.len(), roots.len()); + } +} diff --git a/crates/core_bindings/src/lib.rs b/crates/core_bindings/src/lib.rs @@ -0,0 +1,19 @@ +mod dto; + +pub use dto::dto_roots; +pub use radroots_core as upstream; + +#[cfg(test)] +mod tests { + const GENERATED_TYPES_TS: &str = include_str!("../tests/fixtures/generated_types.ts"); + + #[test] + fn generated_core_types_are_source_rendered() { + assert!(GENERATED_TYPES_TS.contains("export type Money")); + assert!(GENERATED_TYPES_TS.contains("export type QuantityPrice")); + assert!(GENERATED_TYPES_TS.contains("export type UnitDimension")); + assert!(GENERATED_TYPES_TS.contains("label?: string | null")); + assert!(!GENERATED_TYPES_TS.contains("label: string | null")); + assert!(!GENERATED_TYPES_TS.contains("RadrootsCore")); + } +} diff --git a/crates/core_bindings/tests/fixtures/generated_types.ts b/crates/core_bindings/tests/fixtures/generated_types.ts @@ -0,0 +1,25 @@ +// @generated by cargo xtask generate ts +// Do not edit by hand. +export type Currency = string; + +export type Decimal = string; + +export type Discount = { scope: DiscountScope, threshold: DiscountThreshold, value: DiscountValue, }; + +export type DiscountScope = "bin" | "order_total"; + +export type DiscountThreshold = { kind: "bin_count", amount: { bin_id: string, min: number, }, } | { kind: "order_quantity", amount: { min: Quantity, }, }; + +export type DiscountValue = { kind: "money_per_bin", amount: Money, } | { kind: "percent", amount: Percent, }; + +export type Money = { amount: string, currency: string, }; + +export type Percent = { value: string, }; + +export type Quantity = { amount: string, unit: Unit, label?: string | null, }; + +export type QuantityPrice = { amount: Money, quantity: Quantity, }; + +export type Unit = "each" | "kg" | "g" | "oz" | "lb" | "l" | "ml"; + +export type UnitDimension = "count" | "mass" | "volume"; diff --git a/crates/event_bindings/Cargo.toml b/crates/event_bindings/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "radroots_event_bindings" +description = "Generated bindings for Radroots event contracts" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[dependencies] +dto_bindgen_backend_ts = { workspace = true } +radroots_event = { workspace = true } diff --git a/crates/event_bindings/src/lib.rs b/crates/event_bindings/src/lib.rs @@ -0,0 +1,28 @@ +pub use radroots_event as upstream; + +mod model; + +pub use model::{constants_module, kinds_module}; + +#[cfg(test)] +mod tests { + use radroots_event::{ + envelope::kind as kinds, + listing::operational::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS, + }; + + use super::{constants_module, kinds_module}; + + #[test] + fn preserves_event_constant_exports() { + let constants = constants_module().render_source(); + let kinds_ts = kinds_module().render_source(); + assert!(constants.contains("RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS")); + assert!(constants.contains(RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS[0])); + assert!(kinds_ts.contains("KIND_CLASSIFIED_LISTING")); + assert!(kinds_ts.contains(&kinds::KIND_CLASSIFIED_LISTING.to_string())); + assert!(kinds_ts.contains("KIND_TRADE_PROPOSAL")); + assert!(kinds_ts.contains(&kinds::KIND_TRADE_PROPOSAL.to_string())); + assert!(!kinds_ts.contains("KIND_ORDER_REQUEST")); + } +} diff --git a/crates/event_bindings/src/model.rs b/crates/event_bindings/src/model.rs @@ -0,0 +1,161 @@ +use dto_bindgen_backend_ts::{ + TypeScriptDeclaration, TypeScriptImport, TypeScriptModule, TypeScriptType, TypeScriptValue, +}; +use radroots_event::{ + envelope::kind as kinds, listing::operational::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS, +}; + +pub fn constants_module() -> TypeScriptModule { + TypeScriptModule::new("src/generated/constants.ts") + .with_import(TypeScriptImport::type_only( + ["RadrootsOperationalListingProductTagKeys"], + "./types.js", + )) + .with_declaration(TypeScriptDeclaration::constant( + "RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS", + Some(TypeScriptType::named( + "RadrootsOperationalListingProductTagKeys", + )), + TypeScriptValue::array( + RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS + .iter() + .map(|value| TypeScriptValue::string(*value)) + .collect::<Vec<_>>(), + ), + )) +} + +pub fn kinds_module() -> TypeScriptModule { + EVENT_KIND_EXPORTS.iter().fold( + TypeScriptModule::new("src/generated/kinds.ts"), + |module, (name, value)| { + module.with_declaration(TypeScriptDeclaration::constant( + *name, + None, + TypeScriptValue::number(i64::from(*value)), + )) + }, + ) +} + +const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[ + ("KIND_PROFILE", kinds::KIND_PROFILE), + ("KIND_POST", kinds::KIND_POST), + ("KIND_FOLLOW", kinds::KIND_FOLLOW), + ("KIND_REACTION", kinds::KIND_REACTION), + ("KIND_SEAL", kinds::KIND_SEAL), + ("KIND_MESSAGE", kinds::KIND_MESSAGE), + ("KIND_MESSAGE_FILE", kinds::KIND_MESSAGE_FILE), + ("KIND_GIFT_WRAP", kinds::KIND_GIFT_WRAP), + ("KIND_COMMENT", kinds::KIND_COMMENT), + ("KIND_GEOCHAT", kinds::KIND_GEOCHAT), + ("KIND_WIKI_MERGE_REQUEST", kinds::KIND_WIKI_MERGE_REQUEST), + ("KIND_WIKI_ARTICLE", kinds::KIND_WIKI_ARTICLE), + ("KIND_WIKI_REDIRECT", kinds::KIND_WIKI_REDIRECT), + ("KIND_LIST_MUTE", kinds::KIND_LIST_MUTE), + ("KIND_LIST_PINNED_NOTES", kinds::KIND_LIST_PINNED_NOTES), + ( + "KIND_LIST_READ_WRITE_RELAYS", + kinds::KIND_LIST_READ_WRITE_RELAYS, + ), + ("KIND_LIST_BOOKMARKS", kinds::KIND_LIST_BOOKMARKS), + ("KIND_LIST_COMMUNITIES", kinds::KIND_LIST_COMMUNITIES), + ("KIND_LIST_PUBLIC_CHATS", kinds::KIND_LIST_PUBLIC_CHATS), + ("KIND_LIST_BLOCKED_RELAYS", kinds::KIND_LIST_BLOCKED_RELAYS), + ("KIND_LIST_SEARCH_RELAYS", kinds::KIND_LIST_SEARCH_RELAYS), + ("KIND_LIST_SIMPLE_GROUPS", kinds::KIND_LIST_SIMPLE_GROUPS), + ("KIND_LIST_RELAY_FEEDS", kinds::KIND_LIST_RELAY_FEEDS), + ("KIND_LIST_INTERESTS", kinds::KIND_LIST_INTERESTS), + ("KIND_LIST_MEDIA_FOLLOWS", kinds::KIND_LIST_MEDIA_FOLLOWS), + ("KIND_LIST_EMOJIS", kinds::KIND_LIST_EMOJIS), + ("KIND_LIST_DM_RELAYS", kinds::KIND_LIST_DM_RELAYS), + ( + "KIND_LIST_GOOD_WIKI_AUTHORS", + kinds::KIND_LIST_GOOD_WIKI_AUTHORS, + ), + ( + "KIND_LIST_GOOD_WIKI_RELAYS", + kinds::KIND_LIST_GOOD_WIKI_RELAYS, + ), + ("KIND_LIST_SET_FOLLOW", kinds::KIND_LIST_SET_FOLLOW), + ("KIND_LIST_SET_GENERIC", kinds::KIND_LIST_SET_GENERIC), + ("KIND_LIST_SET_RELAY", kinds::KIND_LIST_SET_RELAY), + ("KIND_LIST_SET_BOOKMARK", kinds::KIND_LIST_SET_BOOKMARK), + ("KIND_LIST_SET_CURATION", kinds::KIND_LIST_SET_CURATION), + ("KIND_LIST_SET_VIDEO", kinds::KIND_LIST_SET_VIDEO), + ("KIND_LIST_SET_PICTURE", kinds::KIND_LIST_SET_PICTURE), + ("KIND_LIST_SET_KIND_MUTE", kinds::KIND_LIST_SET_KIND_MUTE), + ("KIND_LIST_SET_INTEREST", kinds::KIND_LIST_SET_INTEREST), + ("KIND_LIST_SET_EMOJI", kinds::KIND_LIST_SET_EMOJI), + ( + "KIND_LIST_SET_RELEASE_ARTIFACT", + kinds::KIND_LIST_SET_RELEASE_ARTIFACT, + ), + ( + "KIND_LIST_SET_APP_CURATION", + kinds::KIND_LIST_SET_APP_CURATION, + ), + ("KIND_LIST_SET_CALENDAR", kinds::KIND_LIST_SET_CALENDAR), + ( + "KIND_LIST_SET_STARTER_PACK", + kinds::KIND_LIST_SET_STARTER_PACK, + ), + ( + "KIND_LIST_SET_MEDIA_STARTER_PACK", + kinds::KIND_LIST_SET_MEDIA_STARTER_PACK, + ), + ("KIND_FARM", kinds::KIND_FARM), + ("KIND_PLOT", kinds::KIND_PLOT), + ("KIND_COOP", kinds::KIND_COOP), + ("KIND_DOCUMENT", kinds::KIND_DOCUMENT), + ("KIND_RESOURCE_AREA", kinds::KIND_RESOURCE_AREA), + ( + "KIND_RESOURCE_HARVEST_CAP", + kinds::KIND_RESOURCE_HARVEST_CAP, + ), + ("KIND_ACCOUNT_CLAIM", kinds::KIND_ACCOUNT_CLAIM), + ("KIND_APP_DATA", kinds::KIND_APP_DATA), + ("KIND_CLASSIFIED_LISTING", kinds::KIND_CLASSIFIED_LISTING), + ("KIND_APPLICATION_HANDLER", kinds::KIND_APPLICATION_HANDLER), + ("KIND_TRADE_PROPOSAL", kinds::KIND_TRADE_PROPOSAL), + ("KIND_TRADE_DECISION", kinds::KIND_TRADE_DECISION), + ( + "KIND_TRADE_REVISION_PROPOSAL", + kinds::KIND_TRADE_REVISION_PROPOSAL, + ), + ( + "KIND_TRADE_REVISION_DECISION", + kinds::KIND_TRADE_REVISION_DECISION, + ), + ("KIND_TRADE_CANCELLATION", kinds::KIND_TRADE_CANCELLATION), + ( + "KIND_TRADE_SELLER_RESERVATION_ASSERTION", + kinds::KIND_TRADE_SELLER_RESERVATION_ASSERTION, + ), + ( + "KIND_TRADE_VALIDATION_RECEIPT", + kinds::KIND_TRADE_VALIDATION_RECEIPT, + ), + ("KIND_KNOWLEDGE_CLAIM", kinds::KIND_KNOWLEDGE_CLAIM), + ("KIND_KNOWLEDGE_RELATION", kinds::KIND_KNOWLEDGE_RELATION), + ("KIND_KNOWLEDGE_REVIEW", kinds::KIND_KNOWLEDGE_REVIEW), + ( + "KIND_KNOWLEDGE_FIELD_REPORT", + kinds::KIND_KNOWLEDGE_FIELD_REPORT, + ), + ( + "KIND_KNOWLEDGE_CHANGE_PROPOSAL", + kinds::KIND_KNOWLEDGE_CHANGE_PROPOSAL, + ), + ( + "KIND_CONTRIBUTION_ATTESTATION", + kinds::KIND_CONTRIBUTION_ATTESTATION, + ), + ("KIND_KNOWLEDGE_SOURCE", kinds::KIND_KNOWLEDGE_SOURCE), + ("KIND_EVIDENCE_BOUNTY", kinds::KIND_EVIDENCE_BOUNTY), + ("KIND_JOB_REQUEST_MIN", kinds::KIND_JOB_REQUEST_MIN), + ("KIND_JOB_REQUEST_MAX", kinds::KIND_JOB_REQUEST_MAX), + ("KIND_JOB_RESULT_MIN", kinds::KIND_JOB_RESULT_MIN), + ("KIND_JOB_RESULT_MAX", kinds::KIND_JOB_RESULT_MAX), + ("KIND_JOB_FEEDBACK", kinds::KIND_JOB_FEEDBACK), +]; diff --git a/crates/event_codec_wasm/Cargo.toml b/crates/event_codec_wasm/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "radroots_event_codec_wasm" +publish = false +version = "0.1.0-alpha" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "WASM boundary for Radroots event codecs" +repository.workspace = true +homepage.workspace = true +readme = "README" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +radroots_blossom = { workspace = true, default-features = false, features = [ + "std", +] } +radroots_event = { workspace = true, default-features = false, features = [ + "std", + "serde", + "knowledge", +] } +radroots_event_codec = { workspace = true, default-features = false, features = [ + "std", + "manifests", +] } +serde = { workspace = true } +serde_json = { workspace = true } +wasm-bindgen = { workspace = true } + +[dev-dependencies] +nostr = { workspace = true, features = ["std"] } +radroots_core = { workspace = true, default-features = false, features = [ + "std", +] } +radroots_identity = { workspace = true, default-features = false, features = [ + "std", +] } diff --git a/crates/event_codec_wasm/README b/crates/event_codec_wasm/README @@ -0,0 +1,24 @@ +# radroots_event_codec_wasm + +This is the README for `radroots_event_codec_wasm`, which provides WebAssembly +bindings for `radroots_event_codec` in the `radroots` core libraries. + +## Overview + + * wasm-bindgen entry points for event content and tag encoding and decoding; + * specialized tag helpers for farm, list, job, message, plot, and reaction + event families; + * JSON and `JsValue` boundaries built around `serde-wasm-bindgen` and base64 + helpers; + * built as `cdylib` and `rlib` artifacts for wasm consumers. + +## Copyright + +Except as otherwise noted, all files in the `radroots_event_codec_wasm` +distribution are + + Copyright (c) 2026 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_event_codec_wasm` +distribution. diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs @@ -0,0 +1,2184 @@ +#![forbid(unsafe_code)] + +use radroots_blossom::{BlobDescriptor, BlobUrl, Error, MediaType, Sha256}; +use radroots_event::envelope::{EventEnvelope, EventEnvelopeError, EventEnvelopeParts}; +use radroots_event::farm::Farm; +use radroots_event::farm::coop::Coop; +use radroots_event::farm::crdt::FarmCrdtChange; +use radroots_event::farm::file::FarmFileMetadata; +use radroots_event::farm::plot::Plot; +use radroots_event::farm::workspace::FarmWorkspaceManifest; +use radroots_event::knowledge::{ + KnowledgeClaim, KnowledgeFieldReport, KnowledgeRelation, KnowledgeReview, KnowledgeSource, + WikiArticle, WikiMergeRequest, WikiRedirect, +}; +use radroots_event::listing::operational::OperationalListing; +use radroots_event::media::file_metadata::FileMetadata; +use radroots_event::media::{AuthoredImage, AuthoredImageError}; +use radroots_event::post::article::Article; +use radroots_event::post::comment::{ + AuthoredNip22Comment, Nip22AddressRootReference, Nip22CommentError, + Nip22CommentParentReference, Nip22CommentRoot, Nip22EventRootReference, +}; +use radroots_event::post::document::Document; +use radroots_event::post::reaction::Reaction; +use radroots_event::post::report::Report; +use radroots_event::post::repost::{GenericRepost, Repost}; +use radroots_event::post::{ + AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostError, AuthoredPostImage, AuthoredUpdate, + PostImageDimensions, +}; +use radroots_event::social::follow::Follow; +use radroots_event::social::gift_wrap::GiftWrap; +use radroots_event::social::group::{ + GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup, + GroupEditMetadata, GroupJoinRequest, GroupLeaveRequest, GroupMembers, GroupMetadata, + GroupPutUser, GroupRemoveUser, GroupRoles, +}; +use radroots_event::social::http_auth::HttpAuth; +use radroots_event::social::job_feedback::JobFeedback; +use radroots_event::social::job_request::JobRequest; +use radroots_event::social::job_result::JobResult; +use radroots_event::social::list::List; +use radroots_event::social::list_set::ListSet; +use radroots_event::social::message::Message; +use radroots_event::social::message_file::MessageFile; +use radroots_event::social::relay_auth::RelayAuth; +use radroots_event::social::seal::Seal; +use radroots_event::wire::Nip01EventWireParts; +use radroots_event_codec::encode::article::article_build_tags; +use radroots_event_codec::encode::comment::authored_nip22_comment_to_wire_parts; +use radroots_event_codec::encode::coop::coop_build_tags; +use radroots_event_codec::encode::document::document_build_tags; +use radroots_event_codec::encode::farm::farm_build_tags; +use radroots_event_codec::encode::farm_crdt::farm_crdt_change_build_tags_with_author; +use radroots_event_codec::encode::farm_file::farm_file_metadata_build_tags; +use radroots_event_codec::encode::farm_workspace::farm_workspace_build_tags; +use radroots_event_codec::encode::file_metadata::file_metadata_build_tags; +use radroots_event_codec::encode::follow::follow_build_tags; +use radroots_event_codec::encode::gift_wrap::gift_wrap_build_tags; +use radroots_event_codec::encode::group::{ + group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags, + group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags, + group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags, + group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags, + group_roles_build_tags, +}; +use radroots_event_codec::encode::http_auth::http_auth_build_tags; +use radroots_event_codec::encode::job::feedback::job_feedback_build_tags; +use radroots_event_codec::encode::job::request::job_request_build_tags; +use radroots_event_codec::encode::job::result::job_result_build_tags; +use radroots_event_codec::encode::knowledge::{ + knowledge_claim_build_tags, knowledge_field_report_build_tags, knowledge_relation_build_tags, + knowledge_review_build_tags, knowledge_source_build_tags, wiki_article_build_tags, + wiki_merge_request_build_tags, wiki_redirect_build_tags, +}; +use radroots_event_codec::encode::list::list_build_tags; +use radroots_event_codec::encode::list_set::list_set_build_tags; +use radroots_event_codec::encode::message::message_build_tags; +use radroots_event_codec::encode::message_file::message_file_build_tags; +use radroots_event_codec::encode::operational_listing::{ + operational_listing_tags as operational_listing_tags_impl, + operational_listing_tags_full as operational_listing_tags_full_impl, +}; +use radroots_event_codec::encode::plot::plot_build_tags; +use radroots_event_codec::encode::post::{ + authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts, +}; +use radroots_event_codec::encode::reaction::reaction_build_tags; +use radroots_event_codec::encode::relay_auth::relay_auth_build_tags; +use radroots_event_codec::encode::report::report_build_tags; +use radroots_event_codec::encode::repost::{generic_repost_build_tags, repost_build_tags}; +use radroots_event_codec::encode::seal::seal_build_tags; +use radroots_event_codec::verify::{RadrootsDecodeError, RadrootsDecodedEvent}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +type RadrootsJsValue = JsValue; + +#[cfg(not(target_arch = "wasm32"))] +type RadrootsJsValue = String; + +fn err_js<E: ToString>(err: E) -> RadrootsJsValue { + #[cfg(target_arch = "wasm32")] + { + JsValue::from_str(&err.to_string()) + } + #[cfg(not(target_arch = "wasm32"))] + { + err.to_string() + } +} + +fn error_json(code: &str, inner_code: Option<&str>) -> RadrootsJsValue { + let value = serde_json::json!({ + "code": code, + "inner_code": inner_code, + }); + err_js(value) +} + +fn normalized_payload(input: &str) -> &str { + if input.is_empty() { "{}" } else { input } +} + +fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> { + serde_json::from_str(normalized_payload(input)).map_err(err_js) +} + +fn parse_authored_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> { + serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("authored_input"))) +} + +fn authored_error(code: &str) -> RadrootsJsValue { + error_json("encode_error", Some(code)) +} + +fn blossom_authored_error(error: Error) -> RadrootsJsValue { + authored_error(error.code()) +} + +fn post_authored_error(error: AuthoredPostError) -> RadrootsJsValue { + authored_error(error.code()) +} + +fn image_authored_error(error: AuthoredImageError) -> RadrootsJsValue { + authored_error(error.code()) +} + +fn comment_authored_error(error: Nip22CommentError) -> RadrootsJsValue { + authored_error(error.code()) +} + +fn wire_parts_to_json(parts: &Nip01EventWireParts) -> Result<String, RadrootsJsValue> { + serde_json::to_string(parts) + .map_err(|_| error_json("internal_error", Some("wire_parts_serialization"))) +} + +fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> { + serde_json::to_string(&tags).map_err(err_js) +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct EventEnvelopeInput { + id: String, + author: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, +} + +fn envelope_error_code(error: &EventEnvelopeError) -> &'static str { + match error { + EventEnvelopeError::InvalidId(_) => "id_invalid", + EventEnvelopeError::InvalidAuthor(_) => "author_invalid", + EventEnvelopeError::InvalidSignature(_) => "signature_invalid", + EventEnvelopeError::NonCanonicalId => "id_non_canonical", + EventEnvelopeError::NonCanonicalAuthor => "author_non_canonical", + EventEnvelopeError::NonCanonicalSignature => "signature_non_canonical", + EventEnvelopeError::EmptyTag { .. } => "tag_empty", + EventEnvelopeError::EmptyTagKey { .. } => "tag_key_empty", + EventEnvelopeError::ControlCharacterTagKey { .. } => "tag_key_control_character", + EventEnvelopeError::ContentTooLarge { .. } => "content_too_large", + EventEnvelopeError::TooManyTags { .. } => "too_many_tags", + EventEnvelopeError::TooManyTagElements { .. } => "too_many_tag_elements", + EventEnvelopeError::TagElementTooLarge { .. } => "tag_element_too_large", + EventEnvelopeError::TagsTooLarge { .. } => "tags_too_large", + } +} + +fn parse_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> { + let envelope: EventEnvelopeInput = + serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("event_json")))?; + EventEnvelope::new(EventEnvelopeParts { + id: envelope.id, + author: envelope.author, + created_at: envelope.created_at, + kind: envelope.kind, + tags: envelope.tags, + content: envelope.content, + sig: envelope.sig, + }) + .map_err(|error| error_json("invalid_event", Some(envelope_error_code(&error)))) +} + +fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue> +where + T: DeserializeOwned, + E: ToString, + F: FnOnce(&T) -> Result<Vec<Vec<String>>, E>, +{ + let value = parse_json::<T>(input)?; + let tags = build(&value).map_err(err_js)?; + tags_to_json(tags) +} + +fn build_tags_json_infallible<T, F>(input: &str, build: F) -> Result<String, RadrootsJsValue> +where + T: DeserializeOwned, + F: FnOnce(&T) -> Vec<Vec<String>>, +{ + let value = parse_json::<T>(input)?; + let tags = build(&value); + tags_to_json(tags) +} + +#[derive(Serialize)] +struct DecodedEventJson<'a, T> +where + T: Serialize, +{ + event_type: &'static str, + contract_id: &'static str, + event: &'a radroots_event::envelope::EventEnvelope, + payload: &'a T, +} + +fn decoded_event_to_json(decoded: RadrootsDecodedEvent) -> Result<String, RadrootsJsValue> { + match decoded { + RadrootsDecodedEvent::WikiArticle(parsed) => { + decoded_payload_to_json("wiki_article", "radroots.wiki.article.v1", &parsed) + } + RadrootsDecodedEvent::WikiRedirect(parsed) => { + decoded_payload_to_json("wiki_redirect", "radroots.wiki.redirect.v1", &parsed) + } + RadrootsDecodedEvent::WikiMergeRequest(parsed) => decoded_payload_to_json( + "wiki_merge_request", + "radroots.wiki.merge_request.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeSource(parsed) => { + decoded_payload_to_json("knowledge_source", "radroots.knowledge.source.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeClaim(parsed) => { + decoded_payload_to_json("knowledge_claim", "radroots.knowledge.claim.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeRelation(parsed) => decoded_payload_to_json( + "knowledge_relation", + "radroots.knowledge.relation.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeReview(parsed) => { + decoded_payload_to_json("knowledge_review", "radroots.knowledge.review.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeFieldReport(parsed) => decoded_payload_to_json( + "knowledge_field_report", + "radroots.knowledge.field_report.v1", + &parsed, + ), + RadrootsDecodedEvent::EvidenceBounty(parsed) => decoded_payload_to_json( + "evidence_bounty", + "radroots.knowledge.evidence_bounty.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeChangeProposal(parsed) => decoded_payload_to_json( + "knowledge_change_proposal", + "radroots.knowledge.change_proposal.v1", + &parsed, + ), + RadrootsDecodedEvent::ContributionAttestation(parsed) => decoded_payload_to_json( + "contribution_attestation", + "radroots.knowledge.contribution_attestation.v1", + &parsed, + ), + } +} + +fn decoded_payload_to_json<T>( + event_type: &'static str, + contract_id: &'static str, + parsed: &radroots_event_codec::decode::RadrootsParsedEvent<T>, +) -> Result<String, RadrootsJsValue> +where + T: Serialize, +{ + serde_json::to_string(&DecodedEventJson { + event_type, + contract_id, + event: &parsed.event, + payload: &parsed.data.data, + }) + .map_err(err_js) +} + +fn decode_error_json(error: RadrootsDecodeError) -> RadrootsJsValue { + let inner = match &error { + RadrootsDecodeError::Nip01Verification(error) => Some(error.code()), + RadrootsDecodeError::ContractValidation(error) => Some(error.code()), + RadrootsDecodeError::EventParse(error) => Some(error.code()), + RadrootsDecodeError::UnsupportedContract { .. } => None, + }; + error_json(error.code(), inner) +} + +#[derive(serde::Deserialize)] +struct FarmCrdtTagsInput { + change: FarmCrdtChange, + author_pubkey: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AuthoredUpdateInput { + content: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AuthoredMediaPostInput { + content: String, + #[serde(default)] + images: Vec<AuthoredImageInput>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AuthoredImageInput { + bytes: Vec<u8>, + url: String, + media_type: String, + uploaded: u64, + width: u32, + height: u32, + alt: String, + #[serde(default)] + fallbacks: Vec<String>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AuthoredCommentInput { + content: String, + root: AuthoredCommentRootInput, + position: AuthoredCommentPositionInput, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +enum AuthoredCommentRootInput { + Event { + event_id: String, + author: String, + kind: u32, + #[serde(default)] + relay: Option<String>, + }, + Address { + coordinate: String, + author: String, + kind: u32, + #[serde(default)] + relay: Option<String>, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +enum AuthoredCommentPositionInput { + TopEvent, + TopAddress { current_revision: String }, + Nested { parent: AuthoredCommentParentInput }, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct AuthoredCommentParentInput { + event_id: String, + author: String, + #[serde(default)] + relay: Option<String>, +} + +fn authored_post_images( + inputs: Vec<AuthoredImageInput>, +) -> Result<Vec<AuthoredPostImage>, RadrootsJsValue> { + inputs.into_iter().map(authored_post_image).collect() +} + +fn authored_post_image(input: AuthoredImageInput) -> Result<AuthoredPostImage, RadrootsJsValue> { + let media_type = MediaType::parse(&input.media_type).map_err(blossom_authored_error)?; + let sha256 = Sha256::digest(&input.bytes); + let size = u64::try_from(input.bytes.len()) + .map_err(|_| authored_error("blob_size_platform_overflow"))?; + let descriptor = BlobDescriptor::new( + BlobUrl::parse(&input.url).map_err(blossom_authored_error)?, + sha256, + size, + media_type.clone(), + input.uploaded, + ) + .map_err(blossom_authored_error)? + .approve_reference() + .map_err(blossom_authored_error)? + .verify_bytes(&input.bytes, &media_type) + .map_err(blossom_authored_error)?; + let image = + AuthoredImage::try_from_verified_descriptor(descriptor).map_err(image_authored_error)?; + let dimensions = + PostImageDimensions::new(input.width, input.height).map_err(post_authored_error)?; + let mut authored = + AuthoredPostImage::new(image, dimensions, input.alt).map_err(post_authored_error)?; + for fallback in input.fallbacks { + let fallback = BlobUrl::parse(&fallback) + .map_err(blossom_authored_error)? + .approve() + .map_err(blossom_authored_error)?; + authored = authored + .try_with_fallback(fallback) + .map_err(post_authored_error)?; + } + Ok(authored) +} + +fn authored_comment_from_input( + input: AuthoredCommentInput, +) -> Result<AuthoredNip22Comment, RadrootsJsValue> { + let root = match input.root { + AuthoredCommentRootInput::Event { + event_id, + author, + kind, + relay, + } => Nip22CommentRoot::Event( + Nip22EventRootReference::parse(event_id, author, kind, relay.as_deref()) + .map_err(comment_authored_error)?, + ), + AuthoredCommentRootInput::Address { + coordinate, + author, + kind, + relay, + } => { + let root = Nip22AddressRootReference::parse(coordinate, relay.as_deref()) + .map_err(comment_authored_error)?; + if root.author().to_hex() != author { + return Err(authored_error("comment_root_author_mismatch")); + } + if root.kind().as_u32() != kind { + return Err(authored_error("comment_root_kind_mismatch")); + } + Nip22CommentRoot::Address(root) + } + }; + + match (root, input.position) { + (Nip22CommentRoot::Event(root), AuthoredCommentPositionInput::TopEvent) => { + AuthoredNip22Comment::top_level_event(input.content, root) + .map_err(comment_authored_error) + } + ( + Nip22CommentRoot::Address(root), + AuthoredCommentPositionInput::TopAddress { current_revision }, + ) => AuthoredNip22Comment::parse_top_level_address(input.content, root, current_revision) + .map_err(comment_authored_error), + (root, AuthoredCommentPositionInput::Nested { parent }) => { + let parent = Nip22CommentParentReference::parse( + parent.event_id, + parent.author, + parent.relay.as_deref(), + ) + .map_err(comment_authored_error)?; + AuthoredNip22Comment::nested(input.content, root, parent) + .map_err(comment_authored_error) + } + _ => Err(authored_error("comment_root_position_incompatible")), + } +} + +/// Builds deterministic unsigned kind-1 wire parts for a strict Update. +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = social_update_build_authored_draft) +)] +pub fn social_update_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> { + let input = parse_authored_json::<AuthoredUpdateInput>(input_json)?; + let update = AuthoredUpdate::new(input.content).map_err(post_authored_error)?; + wire_parts_to_json(&authored_update_to_wire_parts(&update)) +} + +/// Builds deterministic unsigned kind-1 wire parts for a strict PhotoUpdate. +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = social_photo_update_build_authored_draft) +)] +pub fn social_photo_update_build_authored_draft( + input_json: &str, +) -> Result<String, RadrootsJsValue> { + let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?; + let images = authored_post_images(input.images)?; + let photo = AuthoredPhotoUpdate::new(input.content, images).map_err(post_authored_error)?; + wire_parts_to_json(&authored_photo_update_to_wire_parts(&photo)) +} + +/// Builds deterministic unsigned kind-1 wire parts for a strict Ask. +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = social_ask_build_authored_draft) +)] +pub fn social_ask_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> { + let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?; + let images = authored_post_images(input.images)?; + let ask = AuthoredAsk::new(input.content, images).map_err(post_authored_error)?; + wire_parts_to_json(&authored_ask_to_wire_parts(&ask)) +} + +/// Builds deterministic unsigned kind-1111 wire parts for a strict NIP-22 Comment. +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = social_comment_build_authored_draft) +)] +pub fn social_comment_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> { + let input = parse_authored_json::<AuthoredCommentInput>(input_json)?; + let comment = authored_comment_from_input(input)?; + wire_parts_to_json(&authored_nip22_comment_to_wire_parts(&comment)) +} + +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = operational_listing_tags) +)] +pub fn operational_listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_impl) +} + +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = operational_listing_tags_full) +)] +pub fn operational_listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_full_impl) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))] +pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Article, _, _>(article_json, article_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_article_tags))] +pub fn wiki_article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<WikiArticle, _, _>(article_json, wiki_article_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_redirect_tags))] +pub fn wiki_redirect_tags(redirect_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<WikiRedirect, _, _>(redirect_json, wiki_redirect_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_merge_request_tags))] +pub fn wiki_merge_request_tags(request_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<WikiMergeRequest, _, _>(request_json, wiki_merge_request_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_source_tags))] +pub fn knowledge_source_tags(source_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<KnowledgeSource, _, _>(source_json, knowledge_source_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_claim_tags))] +pub fn knowledge_claim_tags(claim_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<KnowledgeClaim, _, _>(claim_json, knowledge_claim_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_relation_tags))] +pub fn knowledge_relation_tags(relation_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<KnowledgeRelation, _, _>(relation_json, knowledge_relation_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_review_tags))] +pub fn knowledge_review_tags(review_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<KnowledgeReview, _, _>(review_json, knowledge_review_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_field_report_tags))] +pub fn knowledge_field_report_tags(report_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<KnowledgeFieldReport, _, _>(report_json, knowledge_field_report_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = verify_and_decode_event_json))] +pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, RadrootsJsValue> { + let event = parse_event_json(event_json)?; + let decoded = radroots_event_codec::verify::verify_and_decode_radroots_event(event) + .map_err(decode_error_json)?; + decoded_event_to_json(decoded) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))] +pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> { + radroots_event_codec::manifest::contract_manifest_json().map_err(err_js) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))] +pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<FileMetadata, _, _>(metadata_json, file_metadata_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))] +pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Repost, _, _>(repost_json, repost_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))] +pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GenericRepost, _, _>(repost_json, generic_repost_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))] +pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Report, _, _>(report_json, report_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))] +pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Follow, _, _>(follow_json, follow_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = document_tags))] +pub fn document_tags(document_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Document, _, _>(document_json, document_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = coop_tags))] +pub fn coop_tags(coop_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Coop, _, _>(coop_json, coop_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_tags))] +pub fn farm_tags(farm_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Farm, _, _>(farm_json, farm_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_tags))] +pub fn list_tags(list_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<List, _, _>(list_json, list_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_set_tags))] +pub fn list_set_tags(list_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<ListSet, _, _>(list_json, list_set_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = plot_tags))] +pub fn plot_tags(plot_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Plot, _, _>(plot_json, plot_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_request_tags))] +pub fn job_request_tags(job_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json_infallible::<JobRequest, _>(job_json, job_request_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_result_tags))] +pub fn job_result_tags(job_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json_infallible::<JobResult, _>(job_json, job_result_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_feedback_tags))] +pub fn job_feedback_tags(job_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json_infallible::<JobFeedback, _>(job_json, job_feedback_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = reaction_tags))] +pub fn reaction_tags(reaction_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Reaction, _, _>(reaction_json, reaction_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_tags))] +pub fn message_tags(message_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Message, _, _>(message_json, message_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_file_tags))] +pub fn message_file_tags(message_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<MessageFile, _, _>(message_json, message_file_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = seal_tags))] +pub fn seal_tags(seal_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<Seal, _, _>(seal_json, seal_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = gift_wrap_tags))] +pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags) +} + +#[cfg_attr( + target_arch = "wasm32", + wasm_bindgen(js_name = farm_workspace_manifest_tags) +)] +pub fn farm_workspace_manifest_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<FarmWorkspaceManifest, _, _>(workspace_json, farm_workspace_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_change_tags))] +pub fn farm_crdt_change_tags(input_json: &str) -> Result<String, RadrootsJsValue> { + let input = parse_json::<FarmCrdtTagsInput>(input_json)?; + let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey)) + .map_err(err_js)?; + tags_to_json(tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_metadata_tags))] +pub fn farm_file_metadata_tags(file_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<FarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))] +pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RelayAuth, _, _>(auth_json, relay_auth_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))] +pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<HttpAuth, _, _>(auth_json, http_auth_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))] +pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupPutUser, _, _>(group_json, group_put_user_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))] +pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupRemoveUser, _, _>(group_json, group_remove_user_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))] +pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupCreateGroup, _, _>(group_json, group_create_group_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))] +pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))] +pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))] +pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))] +pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupCreateInvite, _, _>(group_json, group_create_invite_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))] +pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupJoinRequest, _, _>(group_json, group_join_request_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))] +pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))] +pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupMetadata, _, _>(group_json, group_metadata_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))] +pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupAdmins, _, _>(group_json, group_admins_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))] +pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupMembers, _, _>(group_json, group_members_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))] +pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<GroupRoles, _, _>(group_json, group_roles_build_tags) +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::envelope::kind::{ + KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE, + KIND_WIKI_ARTICLE, + }; + use radroots_event::envelope::{EventEnvelope, EventEnvelopeLimits, EventEnvelopeParts}; + use radroots_event::farm::FarmRef; + use radroots_event::farm::crdt::{ + CrdtBackend, FarmCrdtDocumentKind, FarmSemanticKind, RADROOTS_FARM_CRDT_CHANGE_SCHEMA, + }; + use radroots_event::farm::file::{FarmFileDimensions, FarmFileMetadata, FarmFileSource}; + use radroots_event::farm::workspace::{ + FarmWorkspaceManifest, FarmWorkspaceMediaServer, FarmWorkspaceRef, FarmWorkspaceRelay, + FarmWorkspaceRelayMode, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, + RADROOTS_FARM_WORKSPACE_SCHEMA, + }; + use radroots_event::knowledge::{ + AddressableRef, KnowledgeCitationSpan, KnowledgeFieldContext, KnowledgeLocation, + KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation, + KnowledgeObservationValue, KnowledgeReviewScope, KnowledgeReviewScore, + KnowledgeReviewTarget, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, + RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA, + RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, WikiArticleVersionRef, + }; + use radroots_event::listing::operational::{OperationalListingBin, OperationalListingProduct}; + use radroots_event::social::group::{ + GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup, + GroupEditMetadata, GroupEditableMetadata, GroupJoinRequest, GroupLeaveRequest, + GroupMembers, GroupMetadata, GroupPutUser, GroupRemoveUser, GroupRole, GroupRoles, + GroupUserRef, + }; + use radroots_event::social::http_auth::HttpAuth; + use radroots_event::social::job::JobInputType; + use radroots_event::social::job_request::{JobInput, JobParam}; + use radroots_event::social::relay_auth::RelayAuth; + use radroots_event::social::{ + ReportFileTarget, ReportType, SocialFarmAnchor, SocialLocation, SocialMediaDimensions, + SocialTarget, + }; + + fn sample_listing() -> OperationalListing { + let quantity = + Quantity::try_new(Decimal::from(1u32), Unit::Each).expect("positive fixture quantity"); + let price = QuantityPrice::try_new( + Money::try_new(Decimal::from(10u32), Currency::USD) + .expect("non-negative fixture money"), + quantity.clone(), + ) + .expect("non-zero fixture pricing quantity"); + + OperationalListing { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), + published_at: None, + farm: FarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + product: OperationalListingProduct { + key: "sku".to_string(), + title: "widget".to_string(), + category: "tools".to_string(), + summary: None, + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: "bin-1".parse().expect("primary bin id"), + bins: vec![OperationalListingBin { + bin_id: "bin-1".parse().expect("bin id"), + quantity, + price_per_canonical_unit: price, + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: None, + availability: None, + delivery_method: None, + location: None, + images: None, + } + } + + fn synthetic_pubkey(seed: char) -> String { + seed.to_string().repeat(64) + } + + fn synthetic_event_id(seed: char) -> String { + seed.to_string().repeat(64) + } + + fn social_farm_anchor() -> SocialFarmAnchor { + SocialFarmAnchor { + farm: FarmRef { + pubkey: synthetic_pubkey('a'), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn event_target(kind: u32, seed: char) -> SocialTarget { + SocialTarget::Event { + id: synthetic_event_id(seed), + author: Some(synthetic_pubkey('b')), + event_kind: Some(kind), + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn address_target(kind: u32, d_tag: &str) -> SocialTarget { + let author = synthetic_pubkey('c'); + SocialTarget::Address { + address: format!("{kind}:{author}:{d_tag}"), + author: Some(author), + event_kind: Some(kind), + relays: Some(vec!["wss://relay2.example.test".to_string()]), + } + } + + fn knowledge_event_ref(seed: char, kind: u32) -> radroots_event::tag::EventRef { + radroots_event::tag::EventRef { + id: synthetic_event_id(seed), + author: radroots_identity::PublicKey::from_hex(&synthetic_pubkey('a')) + .expect("fixture public key"), + kind, + d_tag: None, + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn knowledge_address_ref() -> AddressableRef { + AddressableRef { + kind: KIND_WIKI_ARTICLE, + pubkey: synthetic_pubkey('a'), + d_tag: "soil-health".to_string(), + relays: vec!["wss://relay.example.test".to_string()], + } + } + + fn sample_wiki_article() -> WikiArticle { + WikiArticle { + d_tag: "soil-health".to_string(), + title: Some("Soil health".to_string()), + content_djot: "# Soil health".to_string(), + summary: Some("Living soil basics".to_string()), + topics: vec!["soil".to_string()], + references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)], + forked_from: vec![WikiArticleVersionRef { + event_id: synthetic_event_id('b'), + address_ref: knowledge_address_ref(), + }], + deferred_to: None, + } + } + + fn sample_wiki_redirect() -> WikiRedirect { + WikiRedirect { + d_tag: "soil".to_string(), + target: knowledge_address_ref(), + } + } + + fn sample_wiki_merge_request() -> WikiMergeRequest { + WikiMergeRequest { + target_article: knowledge_address_ref(), + destination_pubkey: synthetic_pubkey('a'), + base_version_event_id: Some(synthetic_event_id('e')), + source_version_event_id: synthetic_event_id('f'), + explanation: Some("Merge synthetic soil article updates".to_string()), + } + } + + fn sample_knowledge_source() -> KnowledgeSource { + KnowledgeSource { + schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + d_tag: "soil-source".to_string(), + title: "Soil Source".to_string(), + source_type: "book".to_string(), + authors: vec!["A. Example".to_string()], + publisher: Some("Radroots Synthetic Press".to_string()), + publication_year: Some(2026), + edition: None, + canonical_url: Some("https://source.example.test/soil-source".to_string()), + artifact_refs: vec![knowledge_event_ref('3', KIND_FILE_METADATA)], + author_asserted_rights: None, + topics: vec!["soil".to_string()], + summary: Some("Synthetic source for wasm coverage".to_string()), + } + } + + fn sample_knowledge_claim() -> KnowledgeClaim { + KnowledgeClaim { + schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + claim_type: "practice_effect".to_string(), + text: "Cover crops improve soil structure.".to_string(), + citation_spans: vec![KnowledgeCitationSpan { + source_ref: knowledge_event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_string()], + quote_hash: Some(synthetic_event_id('5')), + chunk_id: Some("chunk-1".to_string()), + }], + topics: vec!["cover-crops".to_string()], + applies_to: vec!["local-food".to_string()], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } + } + + fn sample_knowledge_node_ref(label: &str) -> KnowledgeNodeRef { + KnowledgeNodeRef { + node_type: "event".to_string(), + event_ref: Some(knowledge_event_ref('6', KIND_KNOWLEDGE_CLAIM)), + address_ref: None, + external_id: None, + label: Some(label.to_string()), + } + } + + fn sample_knowledge_relation() -> KnowledgeRelation { + KnowledgeRelation { + schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + subject: sample_knowledge_node_ref("cover crops"), + predicate: "supports".to_string(), + object: sample_knowledge_node_ref("soil structure"), + support_refs: vec![knowledge_event_ref('7', KIND_KNOWLEDGE_CLAIM)], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } + } + + fn sample_knowledge_review() -> KnowledgeReview { + KnowledgeReview { + schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + target: KnowledgeReviewTarget { + event_id: synthetic_event_id('8'), + author_pubkey: synthetic_pubkey('a'), + kind: KIND_KNOWLEDGE_CLAIM, + address: None, + relays: vec!["wss://relay.example.test".to_string()], + review_scope: KnowledgeReviewScope::SpecificVersion, + }, + reviewer_role: "peer".to_string(), + verdict: "needs_more_evidence".to_string(), + scores: vec![KnowledgeReviewScore { + dimension: "evidence".to_string(), + value: "partial".to_string(), + note: None, + }], + notes: Some("Synthetic review".to_string()), + evidence_refs: vec![knowledge_event_ref('9', KIND_KNOWLEDGE_SOURCE)], + } + } + + fn sample_knowledge_field_report() -> KnowledgeFieldReport { + KnowledgeFieldReport { + schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + report_type: "observation".to_string(), + title: "Field observation".to_string(), + summary: Some("Observed cover crop residue.".to_string()), + context: KnowledgeFieldContext { + location_precision: KnowledgeLocationPrecision::CoarseGeohash, + public_location: Some(KnowledgeLocation { + label: Some("watershed".to_string()), + region: Some("synthetic-region".to_string()), + locality: None, + geohash: Some("c23".to_string()), + }), + private_location_ref: None, + topics: vec!["field".to_string()], + context_tags: vec!["observation".to_string()], + }, + observations: vec![KnowledgeObservation { + observation_type: "residue".to_string(), + text: "Residue was visible across beds.".to_string(), + observed_at: Some("2026-07-05".to_string()), + values: vec![KnowledgeObservationValue { + key: "coverage".to_string(), + value: "medium".to_string(), + unit: None, + }], + }], + artifact_refs: vec![knowledge_event_ref('c', KIND_FILE_METADATA)], + related_refs: vec![knowledge_event_ref('d', KIND_KNOWLEDGE_CLAIM)], + limitations: vec!["single observer".to_string()], + } + } + + fn signed_claim_event_json() -> String { + let claim_json = serde_json::to_string(&sample_knowledge_claim()).expect("claim json"); + let tags = serde_json::from_str::<Vec<Vec<String>>>( + &knowledge_claim_tags(&claim_json).expect("claim tags"), + ) + .expect("tags"); + let tags = tags + .into_iter() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("parsed tags"); + let keys = nostr::Keys::generate(); + let event = + nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json) + .tags(tags) + .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) + .sign_with_keys(&keys) + .expect("signed event"); + let envelope = EventEnvelope::new(EventEnvelopeParts { + id: event.id.to_hex(), + author: event.pubkey.to_hex(), + created_at: event.created_at.as_secs(), + kind: u32::from(event.kind.as_u16()), + tags: event + .tags + .as_slice() + .iter() + .map(|tag| tag.as_slice().to_vec()) + .collect(), + content: event.content, + sig: event.sig.to_string(), + }) + .expect("event envelope"); + serde_json::to_string(&envelope).expect("event json") + } + + fn social_location() -> SocialLocation { + SocialLocation { + name: Some("field edge".to_string()), + geohash: Some("c23nb62w20st".to_string()), + } + } + + fn sample_article() -> Article { + Article { + d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + title: "soil notes".to_string(), + content: "# soil notes".to_string(), + summary: Some("cover crop observations".to_string()), + image: Some("https://media.example.test/article.jpg".to_string()), + published_at: Some(1_780_000_000), + farm: Some(social_farm_anchor()), + location: Some(social_location()), + topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]), + } + } + + fn sample_public_file_metadata() -> FileMetadata { + FileMetadata { + url: "https://media.example.test/public.jpg".to_string(), + mime_type: "image/jpeg".to_string(), + sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + original_sha256: Some( + "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(), + ), + size: Some(4096), + dimensions: Some(SocialMediaDimensions { + width: 1200, + height: 800, + }), + blurhash: None, + thumbnails: None, + summary: Some("public field photo".to_string()), + alt: Some("rows after harvest".to_string()), + fallback: Some("https://media.example.test/fallback.jpg".to_string()), + magnet: Some("magnet:?xt=urn:btih:abc".to_string()), + content_hashes: Some(vec!["sha256:field".to_string()]), + services: Some(vec!["https://media.example.test".to_string()]), + content: Some("caption".to_string()), + } + } + + fn sample_reaction() -> Reaction { + Reaction { + target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), + content: String::new(), + } + } + + fn sample_repost() -> Repost { + Repost { + target: event_target(1, 'b'), + content: Some("field update".to_string()), + } + } + + fn sample_generic_repost() -> GenericRepost { + GenericRepost { + target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"), + target_kind: 30023, + content: Some("article share".to_string()), + } + } + + fn sample_report() -> Report { + Report { + reported_pubkey: synthetic_pubkey('b'), + report_type: ReportType::Spam, + event: Some(event_target(1, 'c')), + file: Some(ReportFileTarget { + sha256: Some( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + ), + url: Some("https://media.example.test/bad.jpg".to_string()), + magnet: None, + }), + content: Some("spam report".to_string()), + } + } + + fn sample_job_request() -> JobRequest { + JobRequest { + kind: 5100, + inputs: vec![JobInput { + data: "alpha".to_string(), + input_type: JobInputType::Text, + relay: None, + marker: None, + }], + output: None, + params: vec![JobParam { + key: "mode".to_string(), + value: "fast".to_string(), + }], + bid_sat: Some(42), + relays: vec!["wss://relay.example.com".to_string()], + providers: vec!["provider-a".to_string()], + topics: vec!["topic-a".to_string()], + encrypted: false, + } + } + + fn sample_workspace_manifest() -> FarmWorkspaceManifest { + FarmWorkspaceManifest { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(), + farm_group_id: "field-group".to_string(), + name: "Small Regen Farm".to_string(), + owner_pubkey: "workspace_owner_pubkey".to_string(), + farm: Some(FarmRef { + pubkey: "farm_pubkey".to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + }), + relays: vec![FarmWorkspaceRelay { + url: "wss://relay.example.invalid/farm/field-group".to_string(), + mode: FarmWorkspaceRelayMode::ReadWrite, + }], + media_servers: vec![FarmWorkspaceMediaServer { + url: "https://media.example.invalid/farm/field-group".to_string(), + service: "RadrootsPrivateMedia".to_string(), + }], + supported_kinds: vec![78, 30078, KIND_FARM_FILE_METADATA], + protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(), + created_at_ms: 1_780_000_000_000, + updated_at_ms: None, + } + } + + fn sample_crdt_change() -> FarmCrdtChange { + FarmCrdtChange { + schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), + workspace: FarmWorkspaceRef { + pubkey: "workspace_pubkey".to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + farm_group_id: "field-group".to_string(), + document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + document_kind: FarmCrdtDocumentKind::FarmTask, + crdt_backend: CrdtBackend::Automerge, + crdt_backend_version: Some("0.x".to_string()), + actor_id: "actor_abc".to_string(), + change_hash: "crdt_hash_abc".to_string(), + dependencies: Vec::new(), + encoded_change: "abc-DEF_012".to_string(), + semantic_kind: FarmSemanticKind::FarmTaskCreate, + business_time_ms: 1_780_000_000_000, + author_member_id: Some("member_abc".to_string()), + app_version: Some("0.1.0".to_string()), + } + } + + fn sample_file_metadata() -> FarmFileMetadata { + FarmFileMetadata { + d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(), + workspace: FarmWorkspaceRef { + pubkey: "workspace_pubkey".to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + farm_group_id: "field-group".to_string(), + owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + owner_document_kind: FarmCrdtDocumentKind::FarmTask, + caption: Some("Tomatoes harvested from Patch Y.".to_string()), + url: "https://media.example.invalid/blob/sha256".to_string(), + mime_type: "image/jpeg".to_string(), + sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + original_sha256: None, + size_bytes: Some(123_456), + dimensions: Some(FarmFileDimensions { w: 1600, h: 1200 }), + blurhash: None, + thumb: Some(FarmFileSource { + url: "https://media.example.invalid/thumb/sha256".to_string(), + mime_type: Some("image/jpeg".to_string()), + dimensions: Some(FarmFileDimensions { w: 320, h: 240 }), + }), + image: None, + alt: Some("Harvested tomatoes in a crate".to_string()), + fallbacks: Vec::new(), + } + } + + fn sample_group_metadata() -> GroupEditableMetadata { + GroupEditableMetadata { + name: Some("Small Regen Farm".to_string()), + about: Some("Field app group".to_string()), + picture: Some("https://media.example.invalid/group.png".to_string()), + is_private: false, + is_restricted: true, + is_closed: false, + is_hidden: false, + supported_kinds: Some(vec![78, 30078, KIND_FARM_FILE_METADATA]), + } + } + + fn sample_group_user(role: &str) -> GroupUserRef { + GroupUserRef { + pubkey: format!("{role}_pubkey"), + roles: vec![role.to_string()], + } + } + + fn sample_group_role() -> GroupRole { + GroupRole { + name: "member".to_string(), + description: Some("can read and write group events".to_string()), + permissions: vec!["read".to_string(), "write".to_string()], + } + } + + fn assert_tags_json(value: Result<String, RadrootsJsValue>) { + let tags = tags_json(value); + assert!(!tags.is_empty()); + } + + fn tags_json(value: Result<String, RadrootsJsValue>) -> Vec<Vec<String>> { + let json = value.expect("tags json"); + serde_json::from_str(&json).expect("tags") + } + + fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool { + tags.iter().any(|tag| { + tag.first().map(|entry| entry.as_str()) == Some(key) + && tag.get(1).map(|entry| entry.as_str()) == Some(value) + }) + } + + fn has_exact_tag(tags: &[Vec<String>], expected: &[&str]) -> bool { + tags.iter().any(|tag| { + tag.iter() + .map(|entry| entry.as_str()) + .eq(expected.iter().copied()) + }) + } + + type BindingEncoder = fn(&str) -> Result<String, RadrootsJsValue>; + + const POST_AUTHORED_VECTORS: &str = include_str!( + "../../../../lib/contracts/conformance/vectors/post/verified_profiles.v1.json" + ); + const COMMENT_AUTHORED_VECTORS: &str = include_str!( + "../../../../lib/contracts/conformance/vectors/comment/verified_profile.v1.json" + ); + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct AuthoredVectorSuite { + suite: String, + contract_version: String, + vectors: Vec<AuthoredVector>, + } + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct AuthoredVector { + id: String, + kind: String, + input: serde_json::Value, + expected: serde_json::Value, + } + + fn authored_binding(kind: &str) -> Option<BindingEncoder> { + if kind.starts_with("social.update.build_authored_draft.") { + Some(social_update_build_authored_draft) + } else if kind.starts_with("social.photo_update.build_authored_draft.") { + Some(social_photo_update_build_authored_draft) + } else if kind.starts_with("social.ask.build_authored_draft.") { + Some(social_ask_build_authored_draft) + } else if kind.starts_with("social.comment.build_authored_draft.") { + Some(social_comment_build_authored_draft) + } else { + None + } + } + + fn authored_vector_request(vector: &AuthoredVector) -> String { + let mut input = vector.input.clone(); + if let Some(images) = input + .get_mut("images") + .and_then(serde_json::Value::as_array_mut) + { + for image in images { + let image = image + .as_object_mut() + .unwrap_or_else(|| panic!("{} image must be an object", vector.id)); + let bytes_utf8 = image + .remove("bytes_utf8") + .unwrap_or_else(|| panic!("{} image must carry vector bytes", vector.id)); + let bytes_utf8 = bytes_utf8 + .as_str() + .unwrap_or_else(|| panic!("{} vector bytes must be UTF-8 text", vector.id)); + image.insert( + "bytes".to_owned(), + serde_json::Value::Array( + bytes_utf8 + .as_bytes() + .iter() + .copied() + .map(serde_json::Value::from) + .collect(), + ), + ); + } + } + serde_json::to_string(&input) + .unwrap_or_else(|error| panic!("{} request JSON failed: {error}", vector.id)) + } + + fn authored_error_value(error: RadrootsJsValue) -> serde_json::Value { + serde_json::from_str(&error).expect("authored binding error must be coded JSON") + } + + fn authored_error_inner(error: RadrootsJsValue) -> String { + let error = authored_error_value(error); + assert_eq!(error["code"], "encode_error"); + error["inner_code"] + .as_str() + .expect("authored error inner_code") + .to_owned() + } + + fn execute_authored_vectors(raw: &str, expected_suite: &str, expected_vector_count: usize) { + let suite: AuthoredVectorSuite = + serde_json::from_str(raw).expect("owner conformance vectors"); + assert_eq!(suite.suite, expected_suite); + assert_eq!(suite.contract_version, "1.0.0"); + let mut executed = 0usize; + for vector in suite.vectors { + let Some(binding) = authored_binding(&vector.kind) else { + continue; + }; + executed += 1; + let request = authored_vector_request(&vector); + if vector.kind.ends_with(".valid") { + let first = binding(&request) + .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); + let second = binding(&request) + .unwrap_or_else(|error| panic!("{} repeat failed: {error}", vector.id)); + assert_eq!(first, second, "{} repeat encoding drifted", vector.id); + let actual: serde_json::Value = serde_json::from_str(&first) + .unwrap_or_else(|error| panic!("{} output JSON failed: {error}", vector.id)); + assert_eq!(actual, vector.expected, "{}", vector.id); + } else if vector.kind.ends_with(".invalid") { + let first = binding(&request).expect_err("invalid authored vector must fail"); + let second = + binding(&request).expect_err("repeated invalid authored vector must fail"); + let expected = vector.expected["error"] + .as_str() + .unwrap_or_else(|| panic!("{} expected error must be a string", vector.id)); + assert_eq!(authored_error_inner(first), expected, "{}", vector.id); + assert_eq!(authored_error_inner(second), expected, "{}", vector.id); + } else { + panic!("{} has unsupported authored vector kind", vector.id); + } + } + assert_eq!(executed, expected_vector_count); + } + + #[test] + fn bindings_reject_invalid_json() { + let bindings: [BindingEncoder; 44] = [ + social_update_build_authored_draft, + social_photo_update_build_authored_draft, + social_ask_build_authored_draft, + social_comment_build_authored_draft, + operational_listing_tags, + operational_listing_tags_full, + article_tags, + file_metadata_tags, + repost_tags, + generic_repost_tags, + report_tags, + follow_tags, + document_tags, + coop_tags, + farm_tags, + list_tags, + list_set_tags, + plot_tags, + job_request_tags, + job_result_tags, + job_feedback_tags, + reaction_tags, + message_tags, + message_file_tags, + seal_tags, + gift_wrap_tags, + farm_workspace_manifest_tags, + farm_crdt_change_tags, + farm_file_metadata_tags, + relay_auth_tags, + http_auth_tags, + group_put_user_tags, + group_remove_user_tags, + group_create_group_tags, + group_edit_metadata_tags, + group_delete_group_tags, + group_delete_event_tags, + group_create_invite_tags, + group_join_request_tags, + group_leave_request_tags, + group_metadata_tags, + group_admins_tags, + group_members_tags, + group_roles_tags, + ]; + + for binding in bindings { + assert!(binding("{").is_err()); + } + assert!(operational_listing_tags("").is_err()); + } + + #[test] + fn strict_authored_post_bindings_execute_owner_conformance_vectors() { + execute_authored_vectors(POST_AUTHORED_VECTORS, "post_profiles", 6); + } + + #[test] + fn strict_authored_comment_binding_executes_owner_conformance_vectors() { + execute_authored_vectors(COMMENT_AUTHORED_VECTORS, "nip22_comment_profile", 31); + } + + #[test] + fn authored_comment_rejects_address_coordinate_author_and_kind_mismatches() { + let author = synthetic_pubkey('e'); + let coordinate = format!("30402:{author}:victoria-market"); + let request = |author: &str, kind: u32| { + serde_json::json!({ + "content": "Available this week", + "root": { + "type": "address", + "coordinate": coordinate, + "author": author, + "kind": kind, + "relay": null, + }, + "position": { + "type": "top_address", + "current_revision": synthetic_event_id('b'), + }, + }) + .to_string() + }; + + let author_error = + social_comment_build_authored_draft(&request(&synthetic_pubkey('f'), 30402)) + .expect_err("coordinate author mismatch"); + assert_eq!( + authored_error_inner(author_error), + "comment_root_author_mismatch" + ); + + let kind_error = social_comment_build_authored_draft(&request(&author, 31922)) + .expect_err("coordinate kind mismatch"); + assert_eq!( + authored_error_inner(kind_error), + "comment_root_kind_mismatch" + ); + } + + #[test] + fn authored_image_errors_keep_the_stable_binding_code() { + let error = image_authored_error(AuthoredImageError::MediaTypeNotImage); + assert_eq!(authored_error_inner(error), "media_type_not_image"); + } + + #[test] + fn authored_media_binding_verifies_exact_arbitrary_bytes() { + let bytes = vec![0, 159, 146, 150]; + let hash = Sha256::digest(&bytes); + let url = format!("https://media.example/{hash}.webp"); + let input = serde_json::json!({ + "content": format!("Harvest {url}"), + "images": [{ + "bytes": bytes, + "url": url, + "media_type": "image/webp", + "uploaded": 1_784_347_200_u64, + "width": 1200, + "height": 900, + "alt": "Harvest", + "fallbacks": [], + }], + }); + + let encoded = social_photo_update_build_authored_draft( + &serde_json::to_string(&input).expect("media request"), + ) + .expect("byte-verified PhotoUpdate"); + let encoded: Nip01EventWireParts = serde_json::from_str(&encoded).expect("wire parts"); + assert_eq!(encoded.tags.len(), 1); + assert!(encoded.tags[0].contains(&format!("x {hash}"))); + assert!(encoded.tags[0].contains(&"size 4".to_owned())); + + let mut wrong_bytes = input.clone(); + wrong_bytes["images"][0]["bytes"] = serde_json::json!([1, 2, 3, 4]); + let error = social_photo_update_build_authored_draft( + &serde_json::to_string(&wrong_bytes).expect("wrong-byte request"), + ) + .expect_err("URL digest must bind exact bytes"); + assert_eq!(authored_error_inner(error), "descriptor_hash_mismatch"); + } + + #[test] + fn authored_media_binding_rejects_fabricated_descriptor_fields() { + let bytes = b"image".to_vec(); + let hash = Sha256::digest(&bytes); + let url = format!("https://media.example/{hash}.webp"); + let mut input = serde_json::json!({ + "content": format!("Harvest {url}"), + "images": [{ + "bytes": bytes, + "url": url, + "media_type": "image/webp", + "uploaded": 1_784_347_200_u64, + "width": 1200, + "height": 900, + "alt": "Harvest", + }], + }); + input["images"][0]["sha256"] = serde_json::json!(hash.to_string()); + input["images"][0]["size"] = serde_json::json!(5); + + let error = social_photo_update_build_authored_draft( + &serde_json::to_string(&input).expect("fabricated descriptor request"), + ) + .expect_err("caller-supplied descriptor commitments must be rejected"); + let error = authored_error_value(error); + assert_eq!(error["code"], "invalid_json"); + assert_eq!(error["inner_code"], "authored_input"); + } + + #[test] + fn ask_allows_zero_media_while_photo_update_requires_media() { + let ask = social_ask_build_authored_draft(r#"{"content":"When is harvest?","images":[]}"#) + .expect("Ask without media"); + let ask: Nip01EventWireParts = serde_json::from_str(&ask).expect("Ask wire parts"); + assert_eq!( + ask.tags, + vec![vec!["t".to_owned(), "radroots-ask".to_owned()]] + ); + + let error = + social_photo_update_build_authored_draft(r#"{"content":"Harvest","images":[]}"#) + .expect_err("PhotoUpdate without media"); + assert_eq!(authored_error_inner(error), "photo_imeta_missing"); + } + + #[test] + fn bindings_encode_to_json_when_input_is_valid() { + let listing_json = serde_json::to_string(&sample_listing()).expect("listing json"); + let listing_tags_json = + operational_listing_tags(&listing_json).expect("operational listing tags"); + let listing_tags: Vec<Vec<String>> = + serde_json::from_str(&listing_tags_json).expect("listing tags json"); + assert!(!listing_tags.is_empty()); + + let request_json = serde_json::to_string(&sample_job_request()).expect("request json"); + let request_tags_json = job_request_tags(&request_json).expect("request tags"); + let request_tags: Vec<Vec<String>> = + serde_json::from_str(&request_tags_json).expect("request tags json"); + assert!(!request_tags.is_empty()); + } + + #[test] + fn social_bindings_encode_to_json_when_input_is_valid() { + assert_tags_json(article_tags( + &serde_json::to_string(&sample_article()).expect("article json"), + )); + assert_tags_json(file_metadata_tags( + &serde_json::to_string(&sample_public_file_metadata()).expect("file json"), + )); + assert_tags_json(reaction_tags( + &serde_json::to_string(&sample_reaction()).expect("reaction json"), + )); + assert_tags_json(repost_tags( + &serde_json::to_string(&sample_repost()).expect("repost json"), + )); + assert_tags_json(generic_repost_tags( + &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"), + )); + assert_tags_json(report_tags( + &serde_json::to_string(&sample_report()).expect("report json"), + )); + } + + #[test] + fn social_bindings_surface_builder_errors() { + let mut article = sample_article(); + article.d_tag.clear(); + assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err()); + + let mut reaction = sample_reaction(); + reaction.target = SocialTarget::External { + id: "https://example.test/object".to_string(), + external_kind: "web".to_string(), + hint: None, + }; + assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err()); + + let mut report = sample_report(); + report.reported_pubkey.clear(); + assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err()); + } + + #[test] + fn knowledge_bindings_encode_to_json_when_input_is_valid() { + let article_tags = tags_json(wiki_article_tags( + &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"), + )); + assert!(has_tag(&article_tags, "title", "Soil health")); + let fork_address = format!( + "{}:{}:soil-health", + KIND_WIKI_ARTICLE, + synthetic_pubkey('a') + ); + let fork_event_id = synthetic_event_id('b'); + assert!(has_exact_tag( + &article_tags, + &[ + "a", + fork_address.as_str(), + "wss://relay.example.test", + "fork" + ] + )); + assert!(has_exact_tag( + &article_tags, + &[ + "e", + fork_event_id.as_str(), + "wss://relay.example.test", + "fork" + ] + )); + + let redirect_tags = tags_json(wiki_redirect_tags( + &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"), + )); + let redirect_address = format!( + "{}:{}:soil-health", + KIND_WIKI_ARTICLE, + synthetic_pubkey('a') + ); + assert!(has_exact_tag( + &redirect_tags, + &["a", redirect_address.as_str(), "wss://relay.example.test"] + )); + + let merge_request = sample_wiki_merge_request(); + let merge_parts = + radroots_event_codec::encode::knowledge::wiki_merge_request_to_wire_parts( + &merge_request, + ) + .expect("merge request parts"); + assert_eq!(merge_parts.content, "Merge synthetic soil article updates"); + let merge_tags = tags_json(wiki_merge_request_tags( + &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"), + )); + let source_event_id = synthetic_event_id('f'); + assert!(has_exact_tag( + &merge_tags, + &["e", source_event_id.as_str(), "", "source"] + )); + + let source_tags = tags_json(knowledge_source_tags( + &serde_json::to_string(&sample_knowledge_source()).expect("source json"), + )); + assert!(has_tag( + &source_tags, + "contract", + RADROOTS_KNOWLEDGE_SOURCE_SCHEMA + )); + + let claim_tags = tags_json(knowledge_claim_tags( + &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"), + )); + assert!(has_tag( + &claim_tags, + "contract", + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA + )); + + assert_tags_json(knowledge_relation_tags( + &serde_json::to_string(&sample_knowledge_relation()).expect("relation json"), + )); + assert_tags_json(knowledge_review_tags( + &serde_json::to_string(&sample_knowledge_review()).expect("review json"), + )); + assert_tags_json(knowledge_field_report_tags( + &serde_json::to_string(&sample_knowledge_field_report()).expect("field report json"), + )); + } + + #[test] + fn wiki_article_tags_accept_missing_title() { + let mut article = sample_wiki_article(); + article.title = None; + let tags = tags_json(wiki_article_tags( + &serde_json::to_string(&article).expect("wiki article json"), + )); + assert!( + !tags + .iter() + .any(|tag| tag.first().map(String::as_str) == Some("title")) + ); + } + + #[test] + fn knowledge_claim_tags_enforce_citation_rules() { + let mut claim = sample_knowledge_claim(); + claim.citation_spans.clear(); + let error = knowledge_claim_tags( + &serde_json::to_string(&claim).expect("uncited source-backed claim json"), + ) + .expect_err("source-backed claim requires citations"); + assert!(error.contains("citation_spans")); + + assert_tags_json(knowledge_claim_tags( + &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"), + )); + + for claim_type in ["hypothesis", "observation", "question"] { + let mut uncited = sample_knowledge_claim(); + uncited.claim_type = claim_type.to_string(); + uncited.citation_spans.clear(); + assert_tags_json(knowledge_claim_tags( + &serde_json::to_string(&uncited).expect("uncited claim json"), + )); + } + } + + #[test] + fn knowledge_bindings_verify_decode_and_manifest_json() { + let decoded = verify_and_decode_event_json(&signed_claim_event_json()).expect("decoded"); + let decoded: serde_json::Value = serde_json::from_str(&decoded).expect("decoded json"); + assert_eq!(decoded["event_type"], "knowledge_claim"); + assert_eq!(decoded["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA); + assert_eq!( + decoded["payload"]["text"], + "Cover crops improve soil structure." + ); + + let manifest = contract_manifest_json().expect("manifest"); + let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json"); + assert_eq!(manifest["schema_version"], 2); + assert_eq!(manifest["contract_count"], 11); + let claim_contract = manifest["contracts"] + .as_array() + .expect("contracts") + .iter() + .find(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) + .expect("claim contract"); + assert_eq!(claim_contract["sdk_builder_support"], true); + assert_eq!(claim_contract["sdk_draft_support"], true); + assert_eq!(claim_contract["wasm_tag_builder_support"], true); + assert_eq!(claim_contract["wasm_verified_decode_support"], true); + } + + #[test] + fn knowledge_bindings_verify_decode_errors_are_coded_json() { + let invalid_json = verify_and_decode_event_json("not json").expect_err("invalid json"); + let invalid_json: serde_json::Value = + serde_json::from_str(&invalid_json).expect("error json"); + assert_eq!(invalid_json["code"], "invalid_json"); + assert_eq!(invalid_json["inner_code"], "event_json"); + + let mut event: serde_json::Value = + serde_json::from_str(&signed_claim_event_json()).expect("event json"); + event["sig"] = serde_json::Value::String("0".repeat(128)); + let signature_error = + verify_and_decode_event_json(&event.to_string()).expect_err("signature error"); + let signature_error: serde_json::Value = + serde_json::from_str(&signature_error).expect("signature error json"); + assert_eq!(signature_error["code"], "nip01_verification"); + assert_eq!(signature_error["inner_code"], "signature_invalid"); + } + + #[test] + fn knowledge_bindings_reject_unchecked_event_envelope_input() { + let mut oversized: serde_json::Value = + serde_json::from_str(&signed_claim_event_json()).expect("event json"); + oversized["content"] = serde_json::Value::String( + "x".repeat(EventEnvelopeLimits::default().max_content_bytes + 1), + ); + let oversized_error = + verify_and_decode_event_json(&oversized.to_string()).expect_err("oversized content"); + let oversized_error: serde_json::Value = + serde_json::from_str(&oversized_error).expect("oversized error json"); + assert_eq!(oversized_error["code"], "invalid_event"); + assert_eq!(oversized_error["inner_code"], "content_too_large"); + + let mut non_canonical: serde_json::Value = + serde_json::from_str(&signed_claim_event_json()).expect("event json"); + non_canonical["id"] = serde_json::Value::String("A".repeat(64)); + let canonical_error = + verify_and_decode_event_json(&non_canonical.to_string()).expect_err("non-canonical id"); + let canonical_error: serde_json::Value = + serde_json::from_str(&canonical_error).expect("canonical error json"); + assert_eq!(canonical_error["code"], "invalid_event"); + assert_eq!(canonical_error["inner_code"], "id_non_canonical"); + + let mut unknown_field: serde_json::Value = + serde_json::from_str(&signed_claim_event_json()).expect("event json"); + unknown_field["verified"] = serde_json::Value::Bool(true); + let unknown_error = verify_and_decode_event_json(&unknown_field.to_string()) + .expect_err("unknown event field"); + let unknown_error: serde_json::Value = + serde_json::from_str(&unknown_error).expect("unknown-field error json"); + assert_eq!(unknown_error["code"], "invalid_json"); + assert_eq!(unknown_error["inner_code"], "event_json"); + } + + #[test] + fn event_envelope_error_codes_cover_every_structural_limit_class() { + let cases = [ + ( + EventEnvelopeError::NonCanonicalAuthor, + "author_non_canonical", + ), + ( + EventEnvelopeError::NonCanonicalSignature, + "signature_non_canonical", + ), + (EventEnvelopeError::EmptyTag { index: 0 }, "tag_empty"), + ( + EventEnvelopeError::EmptyTagKey { index: 0 }, + "tag_key_empty", + ), + ( + EventEnvelopeError::ControlCharacterTagKey { index: 0 }, + "tag_key_control_character", + ), + ( + EventEnvelopeError::TooManyTags { max: 1, actual: 2 }, + "too_many_tags", + ), + ( + EventEnvelopeError::TooManyTagElements { max: 1, actual: 2 }, + "too_many_tag_elements", + ), + ( + EventEnvelopeError::TagElementTooLarge { + tag_index: 0, + element_index: 0, + max: 1, + actual: 2, + }, + "tag_element_too_large", + ), + ( + EventEnvelopeError::TagsTooLarge { max: 1, actual: 2 }, + "tags_too_large", + ), + ]; + for (error, code) in cases { + assert_eq!(envelope_error_code(&error), code); + } + } + + #[test] + fn field_bindings_encode_to_json_when_input_is_valid() { + let workspace_json = + serde_json::to_string(&sample_workspace_manifest()).expect("workspace json"); + assert_tags_json(farm_workspace_manifest_tags(&workspace_json)); + + let crdt_json = serde_json::json!({ + "change": sample_crdt_change(), + "author_pubkey": "author_pubkey" + }) + .to_string(); + assert_tags_json(farm_crdt_change_tags(&crdt_json)); + + let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json"); + assert_tags_json(farm_file_metadata_tags(&file_json)); + + let relay_auth_json = serde_json::to_string(&RelayAuth { + relay: "wss://relay.example.invalid/farm/field-group".to_string(), + challenge: "relay-provided-challenge".to_string(), + }) + .expect("relay auth json"); + assert_tags_json(relay_auth_tags(&relay_auth_json)); + + let http_auth_json = serde_json::to_string(&HttpAuth { + url: "https://media.example.invalid/upload".to_string(), + method: "POST".to_string(), + payload_sha256: Some( + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(), + ), + }) + .expect("http auth json"); + assert_tags_json(http_auth_tags(&http_auth_json)); + } + + #[test] + fn field_bindings_surface_builder_errors() { + let crdt_json = serde_json::json!({ + "change": sample_crdt_change(), + "author_pubkey": " " + }) + .to_string(); + + assert!(farm_crdt_change_tags(&crdt_json).is_err()); + } + + #[test] + fn group_bindings_encode_to_json_when_input_is_valid() { + let metadata = sample_group_metadata(); + assert_tags_json(group_put_user_tags( + &serde_json::to_string(&GroupPutUser { + group_id: "field-group".to_string(), + message: Some("add member".to_string()), + pubkey: "member_pubkey".to_string(), + roles: vec!["member".to_string()], + }) + .expect("put user json"), + )); + assert_tags_json(group_remove_user_tags( + &serde_json::to_string(&GroupRemoveUser { + group_id: "field-group".to_string(), + message: Some("remove member".to_string()), + pubkey: "member_pubkey".to_string(), + }) + .expect("remove user json"), + )); + assert_tags_json(group_create_group_tags( + &serde_json::to_string(&GroupCreateGroup { + group_id: "field-group".to_string(), + message: Some("create group".to_string()), + metadata: metadata.clone(), + }) + .expect("create group json"), + )); + assert_tags_json(group_edit_metadata_tags( + &serde_json::to_string(&GroupEditMetadata { + group_id: "field-group".to_string(), + message: Some("edit metadata".to_string()), + metadata: metadata.clone(), + }) + .expect("edit metadata json"), + )); + assert_tags_json(group_delete_group_tags( + &serde_json::to_string(&GroupDeleteGroup { + group_id: "field-group".to_string(), + message: Some("delete group".to_string()), + }) + .expect("delete group json"), + )); + assert_tags_json(group_delete_event_tags( + &serde_json::to_string(&GroupDeleteEvent { + group_id: "field-group".to_string(), + message: Some("delete event".to_string()), + event_id: "event_id".to_string(), + }) + .expect("delete event json"), + )); + let invite_tags = tags_json(group_create_invite_tags( + &serde_json::to_string(&GroupCreateInvite { + group_id: "field-group".to_string(), + message: Some("join the field group".to_string()), + code: "invite-code".to_string(), + }) + .expect("invite json"), + )); + assert!(invite_tags.contains(&vec!["code".to_string(), "invite-code".to_string()])); + assert_tags_json(group_join_request_tags( + &serde_json::to_string(&GroupJoinRequest { + group_id: "field-group".to_string(), + message: Some("requesting access".to_string()), + code: Some("invite-code".to_string()), + }) + .expect("join json"), + )); + assert_tags_json(group_leave_request_tags( + &serde_json::to_string(&GroupLeaveRequest { + group_id: "field-group".to_string(), + message: Some("leaving".to_string()), + }) + .expect("leave json"), + )); + let metadata_tags = tags_json(group_metadata_tags( + &serde_json::to_string(&GroupMetadata { + d_tag: "field-group".to_string(), + metadata, + }) + .expect("metadata json"), + )); + assert!(metadata_tags.contains(&vec!["restricted".to_string()])); + assert!(metadata_tags.contains(&vec![ + "supported_kinds".to_string(), + "78".to_string(), + "30078".to_string(), + KIND_FARM_FILE_METADATA.to_string() + ])); + assert_tags_json(group_admins_tags( + &serde_json::to_string(&GroupAdmins { + d_tag: "field-group".to_string(), + description: Some("group admins".to_string()), + admins: vec![sample_group_user("admin")], + }) + .expect("admins json"), + )); + assert_tags_json(group_members_tags( + &serde_json::to_string(&GroupMembers { + d_tag: "field-group".to_string(), + description: Some("group members".to_string()), + members: vec![sample_group_user("member")], + }) + .expect("members json"), + )); + assert_tags_json(group_roles_tags( + &serde_json::to_string(&GroupRoles { + d_tag: "field-group".to_string(), + description: Some("group roles".to_string()), + roles: vec![sample_group_role()], + }) + .expect("roles json"), + )); + } + + #[test] + fn listing_bindings_surface_builder_errors() { + let mut listing_json = serde_json::to_value(sample_listing()).expect("listing value"); + listing_json["bins"] = serde_json::Value::Array(Vec::new()); + let listing_json = serde_json::to_string(&listing_json).expect("listing json"); + + assert!(operational_listing_tags(&listing_json).is_err()); + assert!(operational_listing_tags_full(&listing_json).is_err()); + } +} diff --git a/crates/identity_bindings/Cargo.toml b/crates/identity_bindings/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "radroots_identity_bindings" +description = "Generated bindings for Radroots identity contracts" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[dependencies] +dto_bindgen_backend_ts = { workspace = true } +radroots_identity = { workspace = true, features = ["std"] } diff --git a/crates/identity_bindings/src/lib.rs b/crates/identity_bindings/src/lib.rs @@ -0,0 +1,37 @@ +pub use radroots_identity as upstream; + +use dto_bindgen_backend_ts::{TypeScriptDeclaration, TypeScriptModule, TypeScriptValue}; +use radroots_identity::username::{MAX_LENGTH, MIN_LENGTH}; + +pub fn constants_module() -> TypeScriptModule { + TypeScriptModule::new("src/generated/constants.ts") + .with_declaration(TypeScriptDeclaration::constant( + "RADROOTS_USERNAME_MIN_LENGTH", + None, + usize_value(MIN_LENGTH), + )) + .with_declaration(TypeScriptDeclaration::constant( + "RADROOTS_USERNAME_MAX_LENGTH", + None, + usize_value(MAX_LENGTH), + )) +} + +fn usize_value(value: usize) -> TypeScriptValue { + TypeScriptValue::number(i64::try_from(value).expect("TypeScript constant fits in i64")) +} + +#[cfg(test)] +mod tests { + use super::{MAX_LENGTH, MIN_LENGTH, constants_module}; + + #[test] + fn preserves_username_constant_exports() { + let rendered = constants_module().render_source(); + assert!(rendered.contains("RADROOTS_USERNAME_MIN_LENGTH")); + assert!(rendered.contains(&MIN_LENGTH.to_string())); + assert!(rendered.contains("RADROOTS_USERNAME_MAX_LENGTH")); + assert!(rendered.contains(&MAX_LENGTH.to_string())); + assert!(!rendered.contains("REGEX")); + } +} diff --git a/crates/radroots/Cargo.toml b/crates/radroots/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "radroots" +description = "Curated ordinary-user Rust facade for Radroots" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true +readme = "README.md" +documentation = "https://docs.rs/radroots" +keywords = ["radroots", "local-food", "sdk", "nostr"] +categories = ["api-bindings", "asynchronous", "network-programming"] +include = ["src/**", "tests/**", "!tests/package_boundary.rs", "examples/**", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] +publish = ["crates-io"] +autoexamples = false + +[package.metadata.docs.rs] +features = ["client", "nostr", "nip46", "geonames", "knowledge"] + +[lints] +workspace = true + +[lib] +name = "radroots" +path = "src/lib.rs" + +[dependencies] +radroots_core = { workspace = true } +radroots_event = { workspace = true } +radroots_identity = { workspace = true, features = ["std"] } +radroots_sdk = { workspace = true } +radroots_trade = { workspace = true, features = ["json", "std"] } +radroots_transport = { workspace = true } + +[features] +default = ["client"] +client = ["radroots_sdk/default"] +native = ["client", "radroots_sdk/native"] +nostr = ["client", "radroots_sdk/nostr"] +nip46 = ["nostr", "radroots_sdk/nip46"] +radrootsd = ["client", "radroots_sdk/radrootsd"] +geonames = ["client", "radroots_sdk/geonames"] +knowledge = ["client", "radroots_sdk/knowledge"] +full = ["radroots_sdk/full"] + +[[example]] +name = "ordinary_memory" +path = "examples/ordinary_memory.rs" +required-features = ["client"] diff --git a/crates/radroots/LICENSE-APACHE b/crates/radroots/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2026 Tyson Lupul + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/radroots/LICENSE-MIT b/crates/radroots/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Tyson Lupul + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/crates/radroots/README.md b/crates/radroots/README.md @@ -0,0 +1,94 @@ +# radroots + +`radroots` is the canonical ordinary-user Rust package for Radroots. It offers +safe local construction, deliberate domain paths, and the common client +boundary without exposing every lower crate or duplicating the advanced SDK +engine. + +The package is enabled for validation-only release qualification. After the +separately approved publication gate, the onboarding command is: + +```sh +cargo add radroots +``` + +## Start locally + +The default `client` feature enables deterministic in-process memory storage. +Construction creates no file, contacts no network or daemon, reads no keyring, +installs no runtime or subscriber, and starts no worker. The initial transport +profile is explicitly local-only. + +```rust +# async fn run() -> radroots::Result<()> { +let client = radroots::client::memory().build()?; +let profile = radroots::client::local_only(); +assert!(profile.is_local_only()); + +// Clone handles share lifecycle state; shutdown is explicit and asynchronous. +client.close().await?; +# Ok(()) +# } +``` + +The default memory generation is process-local and deterministic. Hosts that +persist cursors or compose their own storage generation should use +`radroots_sdk::ClientBuilder` directly. + +## Product operations + +Farm, listing, and trade writes follow one reliability boundary: + +1. `prepare` validates and freezes a side-effect-free plan. +2. The configured signer authorizes and signs that plan. +3. Enqueue durably accepts it under an operation ID and idempotency key. +4. Delivery runs only for an explicitly selected transport profile. + +Cancellation before durable acceptance makes no commit claim. Cancellation +after acceptance cannot roll back the accepted event; resume with the same +idempotency identity and inspect the canonical receipt. Public event models do +not contain exact farm coordinates, private trade terms, credentials, or key +material. + +Errors expose stable classifications and recovery metadata while retaining +their lower source chain for local diagnostics. Display, debug, diagnostics, +and protocol reports redact bearer credentials and signer material. Canonical +lower crates own serialization and versioned wire contracts; facade aliases +are Rust paths, not a second persistence format. + +## Features + +| Feature | Behavior | +| --- | --- | +| `client` | safe memory-backed client; the default | +| `native` | explicit SQLite, sync, and local-signing SDK capabilities | +| `nostr` | explicit Nostr source/sink composition | +| `nip46` | host-owned NIP-46 signer composition; implies `nostr` | +| `radrootsd` | explicitly invoked daemon delivery | +| `geonames` | concrete GeoNames provider capability | +| `knowledge` | canonical knowledge event contracts | +| `full` | the governed complete SDK capability bundle | + +Enabling a feature compiles capability; it never performs I/O. Native storage +opens only through `client::native`, transport is caller-injected, and daemon +delivery happens only when its `deliver` method is invoked. + +## When to use radroots_sdk + +Use `radroots` for ordinary applications and the primary farm, listing, trade, +identity, event, and transport paths. Use `radroots_sdk` directly when the host +must own source-generation identity, inject arbitrary storage/signing/sync +implementations, coordinate FFI/mobile lifecycle, or inspect advanced +capability and diagnostics contracts. There is intentionally no +`radroots::sdk` namespace or wildcard SDK reexport. + +The namespace separation is a compiled contract: + +```compile_fail +use radroots::sdk; +``` + +The normative package charter is the [`radroots` release-v1 +specification](../../docs/specs/radroots_crates_release_v1.md#19-radroots). +The reviewed pre-release public API is recorded in the +[`radroots` baseline](../../docs/api/radroots-0.1.0-alpha.txt). diff --git a/crates/radroots/examples/ordinary_memory.rs b/crates/radroots/examples/ordinary_memory.rs @@ -0,0 +1,10 @@ +//! Constructs the ordinary, safe, local-only Radroots front door. + +fn main() -> radroots::Result<()> { + let client = radroots::client::memory().build()?; + let profile = radroots::client::local_only(); + + assert!(!client.is_closed()); + assert!(profile.is_local_only()); + Ok(()) +} diff --git a/crates/radroots/src/client.rs b/crates/radroots/src/client.rs @@ -0,0 +1,145 @@ +//! Curated client construction and operation entry points. + +#[cfg(any(feature = "nostr", feature = "full"))] +use std::sync::Arc; + +#[cfg(any(feature = "nostr", feature = "full"))] +use radroots_transport::{EventSink, EventSource}; + +#[cfg(any( + feature = "client", + feature = "native", + feature = "nostr", + feature = "nip46", + feature = "full" +))] +use crate::ClientBuilder; +use crate::transport::Profile; + +/// Creates the safe ordinary client builder with deterministic in-process +/// storage and no transport, signer, runtime, worker, or file authority. +#[cfg(feature = "client")] +#[must_use] +pub fn memory() -> ClientBuilder { + ClientBuilder::memory_default() +} + +/// Returns the explicit no-transport profile used by ordinary local work. +#[must_use] +pub const fn local_only() -> Profile { + Profile::local_only() +} + +/// Adds caller-owned source and sink capabilities without connecting them or +/// selecting a fallback transport. +#[cfg(any(feature = "nostr", feature = "full"))] +#[must_use] +pub fn with_transport( + builder: ClientBuilder, + source: Arc<dyn EventSource>, + sink: Arc<dyn EventSink>, +) -> ClientBuilder { + builder.source(source).sink(sink) +} + +/// Adds an explicitly constructed NIP-46 signer provider. +#[cfg(any(feature = "nip46", feature = "full"))] +#[must_use] +#[cfg_attr(coverage_nightly, coverage(off))] +pub fn with_nip46_signer( + builder: ClientBuilder, + provider: crate::signing::Provider, +) -> ClientBuilder { + builder.signing(provider) +} + +/// Explicitly opens validated native SQLite storage. +#[cfg(any(feature = "native", feature = "full"))] +#[cfg_attr(coverage_nightly, coverage(off))] +pub async fn native(options: crate::storage::SqliteOptions) -> crate::Result<ClientBuilder> { + ClientBuilder::sqlite(options).await +} + +/// Reports whether the concrete GeoNames capability was selected at compile +/// time. Asset inspection, acquisition, and database opening remain explicit. +#[cfg(any(feature = "geonames", feature = "full"))] +#[must_use] +pub const fn geonames_enabled() -> bool { + true +} + +/// Reports whether canonical knowledge contracts were selected at compile +/// time. Merely selecting the feature performs no event or storage operation. +#[cfg(any(feature = "knowledge", feature = "full"))] +#[must_use] +pub const fn knowledge_enabled() -> bool { + true +} + +#[cfg(all(test, feature = "full"))] +mod tests { + use super::{ + Arc, EventSink, EventSource, Profile, geonames_enabled, knowledge_enabled, local_only, + memory, with_transport, + }; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error as TransportError, FetchPage, FetchRequest, + SinkFailure, SinkStatus, SourceStatus, outcome::Retryability, + source::BoxFuture as TransportFuture, + }; + + struct TestSource; + struct TestSink; + + impl EventSource for TestSource { + fn status(&self) -> TransportFuture<'_, Result<SourceStatus, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + + fn fetch( + &self, + _request: FetchRequest, + ) -> TransportFuture<'_, Result<FetchPage, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + } + + impl EventSink for TestSink { + fn status(&self) -> TransportFuture<'_, Result<SinkStatus, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + + fn deliver( + &self, + request: DeliveryRequest, + ) -> TransportFuture<'_, Result<DeliveryReceipt, SinkFailure>> { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + #[test] + fn curated_builders_cover_every_directly_testable_entry_point() { + let local = memory().build().expect("memory client"); + assert_eq!(local_only(), Profile::local_only()); + assert!(local.source().expect("source").is_none()); + + let composed = with_transport(memory(), Arc::new(TestSource), Arc::new(TestSink)) + .build() + .expect("transport client"); + assert!(composed.source().expect("source").is_some()); + assert!(composed.sink().expect("sink").is_some()); + + assert!(geonames_enabled()); + assert!(knowledge_enabled()); + } +} diff --git a/crates/radroots/src/event.rs b/crates/radroots/src/event.rs @@ -0,0 +1,5 @@ +//! Curated event authoring and inspection entry points. + +pub use radroots_event::{ + Error, Event, EventId, EventKind, EventTag, GenericEventDraft, SignedEvent, VerifiedEvent, +}; diff --git a/crates/radroots/src/farm.rs b/crates/radroots/src/farm.rs @@ -0,0 +1,4 @@ +//! Curated farm-domain entry points. + +pub use radroots_event::farm::{Farm, FarmPublicLocation, FarmRef}; +pub use radroots_sdk::farm::{Plan, PrepareError, PrepareErrorKind, PrepareRequest, prepare}; diff --git a/crates/radroots/src/identity.rs b/crates/radroots/src/identity.rs @@ -0,0 +1,5 @@ +//! Curated public identity entry points. + +pub use radroots_identity::{ + AccountId, Error, IdentityId, Profile, PublicIdentity, PublicKey, Username, +}; diff --git a/crates/radroots/src/knowledge.rs b/crates/radroots/src/knowledge.rs @@ -0,0 +1,8 @@ +//! Curated knowledge-domain entry points. + +#[cfg(any(feature = "knowledge", feature = "full"))] +pub use radroots_event::knowledge::{ + AddressableRef, KnowledgeClaim, KnowledgeFieldReport, KnowledgeRelation, KnowledgeReview, + KnowledgeSource, KnowledgeValidationError, WikiArticle, WikiDTagError, WikiMergeRequest, + WikiRedirect, normalize_wiki_d_tag, validate_wiki_d_tag, +}; diff --git a/crates/radroots/src/lib.rs b/crates/radroots/src/lib.rs @@ -0,0 +1,18 @@ +#![forbid(unsafe_code)] +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![warn(missing_docs)] +#![doc = include_str!("../README.md")] + +pub mod client; +pub mod event; +pub mod farm; +pub mod identity; +pub mod knowledge; +pub mod listing; +pub mod signing; +pub mod storage; +pub mod sync; +pub mod trade; +pub mod transport; + +pub use radroots_sdk::{Client, ClientBuilder, Error, Result}; diff --git a/crates/radroots/src/listing.rs b/crates/radroots/src/listing.rs @@ -0,0 +1,12 @@ +//! Curated listing-domain entry points. + +pub use radroots_event::listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingImage, OperationalListingProduct, + OperationalListingPublicLocation, OperationalListingStatus, +}; +pub use radroots_sdk::listing::{ + Action, Plan, PrepareError, PrepareErrorKind, PrepareRequest, + RadrootsOperationalListingEditDocumentV1 as EditV1, + RadrootsOperationalListingLifecycleState as Lifecycle, prepare, +}; diff --git a/crates/radroots/src/signing.rs b/crates/radroots/src/signing.rs @@ -0,0 +1,3 @@ +//! Curated signing entry points. + +pub use radroots_sdk::signing::{Mode, Provider}; diff --git a/crates/radroots/src/storage.rs b/crates/radroots/src/storage.rs @@ -0,0 +1,6 @@ +//! Curated storage entry points. + +pub use radroots_sdk::storage::{IntegrityStatus, Operations, Status}; + +#[cfg(any(feature = "native", feature = "full"))] +pub use radroots_sdk::storage::{SqliteOpenMode, SqliteOptions, SqlitePaths}; diff --git a/crates/radroots/src/sync.rs b/crates/radroots/src/sync.rs @@ -0,0 +1 @@ +//! Curated synchronization entry points. diff --git a/crates/radroots/src/trade.rs b/crates/radroots/src/trade.rs @@ -0,0 +1,13 @@ +//! Curated trade-domain entry points. + +pub use radroots_core::{Currency, Decimal, Money, Percent, Quantity, QuantityPrice, Unit}; +pub use radroots_event::trade::{ + FulfillmentProfileV1, TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1, + TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1, + TradeMutationBodyV1 as MutationBodyV1, TradeMutationEnvelopeV1 as MutationV1, + TradeMutationKindV1 as MutationKindV1, TradePrivateTermsRefV1, +}; +pub use radroots_sdk::trade::{ + Plan, PrepareError, PrepareErrorKind, PrepareRequest, prepare, project, +}; +pub use radroots_trade::{Projection, ReducerIssue, ReductionInput, ValidationError, WorkflowPlan}; diff --git a/crates/radroots/src/transport.rs b/crates/radroots/src/transport.rs @@ -0,0 +1,10 @@ +//! Curated transport entry points. + +pub use radroots_sdk::transport::Profile; +pub use radroots_transport::{ + Error, EventSink, EventSource, Target, TargetSet, TransportId, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, +}; + +#[cfg(any(feature = "radrootsd", feature = "full"))] +pub use radroots_sdk::transport::{DaemonAuth, DaemonConfig, DaemonDelivery, DaemonError}; diff --git a/crates/radroots/tests/package_boundary.rs b/crates/radroots/tests/package_boundary.rs @@ -0,0 +1,93 @@ +use std::collections::BTreeSet; + +const MANIFEST: &str = include_str!("../Cargo.toml"); +const WORKSPACE: &str = include_str!("../../../Cargo.toml"); +const ROOT: &str = include_str!("../src/lib.rs"); +const README: &str = include_str!("../README.md"); + +#[test] +fn package_has_final_identity_and_exact_dependencies() { + assert!(MANIFEST.contains("name = \"radroots\"")); + assert!(MANIFEST.contains("publish = [\"crates-io\"]")); + assert_eq!( + dependency_names(MANIFEST), + BTreeSet::from([ + "radroots_core", + "radroots_event", + "radroots_identity", + "radroots_sdk", + "radroots_trade", + "radroots_transport", + ]) + ); + assert!(WORKSPACE.contains( + "radroots_sdk = { path = \"crates/sdk\", version = \"=0.1.0-alpha\", default-features = false }" + )); +} + +#[test] +fn features_forward_the_exact_user_oriented_graph() { + for forwarding in [ + "default = [\"client\"]", + "client = [\"radroots_sdk/default\"]", + "native = [\"client\", \"radroots_sdk/native\"]", + "nostr = [\"client\", \"radroots_sdk/nostr\"]", + "nip46 = [\"nostr\", \"radroots_sdk/nip46\"]", + "radrootsd = [\"client\", \"radroots_sdk/radrootsd\"]", + "geonames = [\"client\", \"radroots_sdk/geonames\"]", + "knowledge = [\"client\", \"radroots_sdk/knowledge\"]", + "full = [\"radroots_sdk/full\"]", + ] { + assert!(MANIFEST.contains(forwarding), "missing `{forwarding}`"); + } + for forbidden in ["reticulum", "mesh", "simplex", "nostrdb", "replica", "sp1"] { + assert!( + !MANIFEST.to_ascii_lowercase().contains(forbidden), + "forbidden feature vocabulary `{forbidden}`" + ); + } +} + +#[test] +fn root_has_exact_exports_and_no_sdk_namespace() { + assert!(ROOT.contains("pub use radroots_sdk::{Client, ClientBuilder, Error, Result};")); + assert!(!ROOT.contains("pub mod sdk")); + assert!(!ROOT.contains("pub use radroots_sdk::*")); + assert_eq!(ROOT.matches("pub use ").count(), 1); + assert!(README.contains("```compile_fail\nuse radroots::sdk;\n```")); +} + +#[test] +fn modules_use_deliberate_reexports_without_wildcards() { + let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + for entry in std::fs::read_dir(source_root).expect("read facade source") { + let path = entry.expect("read facade source entry").path(); + if path.extension().and_then(|value| value.to_str()) != Some("rs") { + continue; + } + let source = std::fs::read_to_string(&path).expect("read facade module"); + assert!( + !source.contains("::*"), + "{} contains a wildcard reexport", + path.display() + ); + assert!( + !source.contains("pub mod sdk"), + "{} exposes the forbidden SDK namespace", + path.display() + ); + } +} + +fn dependency_names(manifest: &str) -> BTreeSet<&str> { + manifest + .split_once("[dependencies]") + .expect("dependency section") + .1 + .split_once("\n[") + .expect("section after dependencies") + .0 + .lines() + .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim())) + .collect() +} diff --git a/crates/radroots/tests/public_surface.rs b/crates/radroots/tests/public_surface.rs @@ -0,0 +1,120 @@ +#[test] +fn approved_module_skeleton_is_public() { + #[allow(unused_imports)] + use radroots::{ + client, event, farm, identity, knowledge, listing, signing, storage, sync, trade, transport, + }; +} + +#[test] +fn root_exports_only_the_client_boundary() { + fn assert_client<T: Clone + Send + Sync>() {} + fn assert_builder<T: Send + Sync>() {} + fn assert_error<T: std::error::Error + Send + Sync + 'static>() {} + + assert_client::<radroots::Client>(); + assert_builder::<radroots::ClientBuilder>(); + assert_error::<radroots::Error>(); + let result: radroots::Result<()> = Ok(()); + assert!(result.is_ok()); +} + +#[test] +fn canonical_domain_paths_compile() { + #[allow(unused_imports)] + use radroots::{ + event::{Event, EventId, GenericEventDraft}, + farm::{Farm, FarmPublicLocation, Plan as FarmPlan, PrepareRequest as FarmRequest}, + identity::{AccountId, PublicKey, Username}, + listing::{EditV1, OperationalListing, Plan as ListingPlan}, + signing::{Mode, Provider}, + storage::{IntegrityStatus, Operations as StorageOperations, Status}, + trade::{Money, MutationV1, Plan as TradePlan, Projection}, + transport::{Profile, SatisfactionPolicy, Target, TargetSet, TransportId}, + }; +} + +#[cfg(feature = "client")] +#[test] +fn ordinary_memory_and_local_only_construction_is_inert() { + let client = radroots::client::memory().build().expect("memory client"); + assert!(!client.is_closed()); + assert!(radroots::client::local_only().is_local_only()); +} + +#[cfg(any(feature = "nostr", feature = "full"))] +#[test] +fn explicit_transport_composition_is_inert() { + use std::sync::Arc; + + use radroots::transport::{EventSink, EventSource}; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error, FetchPage, FetchRequest, SinkFailure, SinkStatus, + SourceStatus, outcome::Retryability, source::BoxFuture, + }; + + struct Source; + struct Sink; + + impl EventSource for Source { + fn status(&self) -> BoxFuture<'_, Result<SourceStatus, Error>> { + Box::pin(async { Err(Error::UnsupportedOperation) }) + } + + fn fetch(&self, _request: FetchRequest) -> BoxFuture<'_, Result<FetchPage, Error>> { + Box::pin(async { Err(Error::UnsupportedOperation) }) + } + } + + impl EventSink for Sink { + fn status(&self) -> BoxFuture<'_, Result<SinkStatus, Error>> { + Box::pin(async { Err(Error::UnsupportedOperation) }) + } + + fn deliver( + &self, + request: DeliveryRequest, + ) -> BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + let client = radroots::client::with_transport( + radroots::client::memory(), + Arc::new(Source), + Arc::new(Sink), + ) + .build() + .expect("explicit transport client"); + assert!(client.source().expect("source").is_some()); + assert!(client.sink().expect("sink").is_some()); +} + +#[cfg(any(feature = "geonames", feature = "full"))] +#[test] +fn geonames_selection_is_explicit() { + assert!(radroots::client::geonames_enabled()); +} + +#[cfg(any(feature = "knowledge", feature = "full"))] +#[test] +fn knowledge_selection_is_explicit() { + assert!(radroots::client::knowledge_enabled()); +} + +#[cfg(any(feature = "knowledge", feature = "full"))] +#[test] +fn canonical_knowledge_paths_compile() { + #[allow(unused_imports)] + use radroots::knowledge::{KnowledgeClaim, WikiArticle, normalize_wiki_d_tag}; +} diff --git a/crates/replica_schema_bindings/Cargo.toml b/crates/replica_schema_bindings/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "radroots_replica_schema_bindings" +description = "Generated bindings for Radroots replica schemas" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[dependencies] +dto_bindgen_core = { workspace = true } +radroots_replica_schema = { workspace = true, features = ["dto-bindgen"] } diff --git a/crates/replica_schema_bindings/src/lib.rs b/crates/replica_schema_bindings/src/lib.rs @@ -0,0 +1,31 @@ +pub use radroots_replica_schema as upstream; + +pub fn dto_registry() -> dto_bindgen_core::Registry { + upstream::dto::dto_registry() +} + +#[cfg(test)] +mod tests { + use super::dto_registry; + + #[test] + fn preserves_replica_schema_registry_exports() { + let registry = dto_registry(); + let actual = registry + .types_by_id + .values() + .map(|type_def| match type_def { + dto_bindgen_core::TypeDef::Struct(def) => def.export_name.as_str(), + dto_bindgen_core::TypeDef::Enum(def) => def.export_name.as_str(), + }) + .collect::<Vec<_>>(); + + assert!(actual.contains(&"Farm")); + assert!(actual.contains(&"GcsLocation")); + assert!(actual.contains(&"IGcsLocationFindMany")); + assert!(actual.contains(&"IGcsLocationFindManyResolve")); + assert!(actual.contains(&"IMediaImageFindMany")); + assert!(actual.contains(&"INostrProfileFindMany")); + assert!(actual.contains(&"INostrRelayFindMany")); + } +} diff --git a/crates/replica_store_wasm/Cargo.toml b/crates/replica_store_wasm/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "radroots_replica_store_wasm" +publish = false +version = "0.1.0-alpha" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "WASM boundary for Radroots replica store access" +repository.workspace = true +homepage.workspace = true +readme = "README" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +radroots_sql_core = { workspace = true, features = ["web"] } +radroots_sdk_sql_wasm_runtime = { workspace = true } +radroots_replica_store = { workspace = true } +radroots_replica_schema = { workspace = true } +radroots_replica_sync = { workspace = true, features = ["std"] } +js-sys = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen = { workspace = true } + +[dev-dependencies] +wasm-bindgen-test = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica_store_wasm/README b/crates/replica_store_wasm/README @@ -0,0 +1,25 @@ +# radroots_replica_store_wasm + +This is the README for `radroots_replica_store_wasm`, which provides WebAssembly +bindings for `radroots_replica_store` in the `radroots` core libraries. + +## Overview + + * a wasm32-only wrapper around the replica store runtime surface; + * integration with `radroots_sql_wasm_core` for browser and worker SQL + execution; + * a small target-specific entry point layer rather than a separate database + implementation; + * intended for JavaScript-facing callers that need the replica store APIs + in wasm builds. + +## Copyright + +Except as otherwise noted, all files in the `radroots_replica_store_wasm` +distribution are + + Copyright (c) 2026 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_replica_store_wasm` +distribution. diff --git a/crates/replica_store_wasm/src/lib.rs b/crates/replica_store_wasm/src/lib.rs @@ -0,0 +1,10 @@ +#![forbid(unsafe_code)] + +mod snapshot; +#[cfg(target_arch = "wasm32")] +mod utils; +#[cfg(target_arch = "wasm32")] +mod wasm_impl; +pub use snapshot::*; +#[cfg(target_arch = "wasm32")] +pub use wasm_impl::*; diff --git a/crates/replica_store_wasm/src/snapshot.rs b/crates/replica_store_wasm/src/snapshot.rs @@ -0,0 +1,87 @@ +use radroots_replica_store::ReplicaStoreExportManifestRs; + +pub const EXPORT_MANIFEST_FIELD: &str = "manifest_rs"; +pub const EXPORT_DB_BYTES_FIELD: &str = "db_bytes"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExportManifestSummary { + pub export_version: String, + pub replica_store_version: String, + pub backup_format_version: String, + pub schema_hash: String, + pub schema_table_count: usize, + pub migration_count: usize, + pub table_count_count: usize, +} + +pub fn synced_export_error(pending_count: usize, expected_count: usize) -> Option<String> { + if pending_count == 0 { + None + } else { + Some(format!( + "replica store export requires synced state (pending {pending_count}/{expected_count})" + )) + } +} + +pub fn export_manifest_summary(manifest: &ReplicaStoreExportManifestRs) -> ExportManifestSummary { + ExportManifestSummary { + export_version: manifest.export_version.clone(), + replica_store_version: manifest.replica_store_version.clone(), + backup_format_version: manifest.backup_format_version.clone(), + schema_hash: manifest.schema_hash.clone(), + schema_table_count: manifest.schema.len(), + migration_count: manifest.migrations.len(), + table_count_count: manifest.table_counts.len(), + } +} + +#[cfg(test)] +mod tests { + use super::{ + EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, export_manifest_summary, synced_export_error, + }; + + fn manifest() -> radroots_replica_store::ReplicaStoreExportManifestRs { + radroots_replica_store::ReplicaStoreExportManifestRs { + export_version: "1".to_owned(), + replica_store_version: "0.1.0".to_owned(), + backup_format_version: "1".to_owned(), + schema_hash: "schema-hash".to_owned(), + schema: Vec::new(), + migrations: Vec::new(), + table_counts: Vec::new(), + } + } + + #[test] + fn export_snapshot_field_names_are_stable() { + assert_eq!(EXPORT_MANIFEST_FIELD, "manifest_rs"); + assert_eq!(EXPORT_DB_BYTES_FIELD, "db_bytes"); + } + + #[test] + fn synced_export_error_allows_empty_pending_queue() { + assert_eq!(synced_export_error(0, 4), None); + } + + #[test] + fn synced_export_error_reports_pending_and_expected_counts() { + assert_eq!( + synced_export_error(2, 4).expect("error"), + "replica store export requires synced state (pending 2/4)" + ); + } + + #[test] + fn export_manifest_summary_preserves_versions_and_counts() { + let summary = export_manifest_summary(&manifest()); + assert_eq!(summary.export_version, "1"); + assert_eq!(summary.replica_store_version, "0.1.0"); + assert_eq!(summary.backup_format_version, "1"); + assert_eq!(summary.schema_hash, "schema-hash"); + assert_eq!(summary.schema_table_count, 0); + assert_eq!(summary.migration_count, 0); + assert_eq!(summary.table_count_count, 0); + } +} diff --git a/crates/replica_store_wasm/src/utils.rs b/crates/replica_store_wasm/src/utils.rs @@ -0,0 +1,13 @@ +use serde::Serialize; +use wasm_bindgen::prelude::*; + +use radroots_sql_core::SqlError; + +pub fn value_to_js<T>(value: T) -> Result<JsValue, JsValue> +where + T: Serialize, +{ + let json = serde_json::to_string(&value) + .map_err(|err| radroots_sdk_sql_wasm_runtime::err_js(SqlError::from(err)))?; + Ok(JsValue::from_str(&json)) +} diff --git a/crates/replica_store_wasm/src/wasm_impl.rs b/crates/replica_store_wasm/src/wasm_impl.rs @@ -0,0 +1,923 @@ +use crate::{ + snapshot::{EXPORT_DB_BYTES_FIELD, EXPORT_MANIFEST_FIELD, synced_export_error}, + utils::value_to_js, +}; +use radroots_replica_store::migrations; +use radroots_replica_store::{ReplicaStoreExportManifestRs, export_manifest}; +use radroots_replica_sync::radroots_replica_sync_status; +use radroots_sdk_sql_wasm_runtime::parse_json; +use radroots_sdk_sql_wasm_runtime::{ + WasmSqlExecutor, err_js, export_bytes, export_lock_begin, export_lock_end, + with_export_lock_bypass, +}; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::*; + +use radroots_replica_schema::farm::{ + IFarmCreate, IFarmDelete, IFarmFindMany, IFarmFindOne, IFarmUpdate, +}; + +use radroots_replica_schema::farm_gcs_location::{ + IFarmGcsLocationCreate, IFarmGcsLocationDelete, IFarmGcsLocationFindMany, + IFarmGcsLocationFindOne, IFarmGcsLocationUpdate, +}; + +use radroots_replica_schema::farm_member::{ + IFarmMemberCreate, IFarmMemberDelete, IFarmMemberFindMany, IFarmMemberFindOne, + IFarmMemberUpdate, +}; + +use radroots_replica_schema::farm_member_claim::{ + IFarmMemberClaimCreate, IFarmMemberClaimDelete, IFarmMemberClaimFindMany, + IFarmMemberClaimFindOne, IFarmMemberClaimUpdate, +}; + +use radroots_replica_schema::farm_tag::{ + IFarmTagCreate, IFarmTagDelete, IFarmTagFindMany, IFarmTagFindOne, IFarmTagUpdate, +}; + +use radroots_replica_schema::gcs_location::{ + IGcsLocationCreate, IGcsLocationDelete, IGcsLocationFindMany, IGcsLocationFindOne, + IGcsLocationUpdate, +}; + +use radroots_replica_schema::log_error::{ + ILogErrorCreate, ILogErrorDelete, ILogErrorFindMany, ILogErrorFindOne, ILogErrorUpdate, +}; + +use radroots_replica_schema::media_image::{ + IMediaImageCreate, IMediaImageDelete, IMediaImageFindMany, IMediaImageFindOne, + IMediaImageUpdate, +}; + +use radroots_replica_schema::nostr_profile::{ + INostrProfileCreate, INostrProfileDelete, INostrProfileFindMany, INostrProfileFindOne, + INostrProfileUpdate, +}; + +use radroots_replica_schema::nostr_event_head::{ + INostrEventHeadCreate, INostrEventHeadDelete, INostrEventHeadFindMany, INostrEventHeadFindOne, + INostrEventHeadUpdate, +}; + +use radroots_replica_schema::nostr_relay::{ + INostrRelayCreate, INostrRelayDelete, INostrRelayFindMany, INostrRelayFindOne, + INostrRelayUpdate, +}; + +use radroots_replica_schema::trade_product::{ + ITradeProductCreate, ITradeProductDelete, ITradeProductFindMany, ITradeProductFindOne, + ITradeProductUpdate, +}; + +use radroots_replica_schema::plot::{ + IPlotCreate, IPlotDelete, IPlotFindMany, IPlotFindOne, IPlotUpdate, +}; + +use radroots_replica_schema::plot_gcs_location::{ + IPlotGcsLocationCreate, IPlotGcsLocationDelete, IPlotGcsLocationFindMany, + IPlotGcsLocationFindOne, IPlotGcsLocationUpdate, +}; + +use radroots_replica_schema::plot_tag::{ + IPlotTagCreate, IPlotTagDelete, IPlotTagFindMany, IPlotTagFindOne, IPlotTagUpdate, +}; + +use radroots_replica_schema::nostr_profile_relay::INostrProfileRelayRelation; + +use radroots_replica_schema::trade_product_location::ITradeProductLocationRelation; + +use radroots_replica_schema::trade_product_media::ITradeProductMediaRelation; + +#[wasm_bindgen(js_name = replica_store_run_migrations)] +pub fn replica_store_run_migrations() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_up(&exec).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_store_reset_database)] +pub fn replica_store_reset_database() -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + migrations::run_all_down(&exec).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_store_export_json)] +pub fn replica_store_export_json() -> Result<JsValue, JsValue> { + let exec = WasmSqlExecutor::new(); + let dump = radroots_replica_store::backup::export_database_backup(&exec).map_err(err_js)?; + value_to_js(dump) +} + +#[wasm_bindgen(js_name = replica_store_import_json)] +pub fn replica_store_import_json(dump_json: &str) -> Result<(), JsValue> { + let exec = WasmSqlExecutor::new(); + radroots_replica_store::backup::restore_database_backup_json(&exec, dump_json).map_err(err_js) +} + +#[wasm_bindgen(js_name = replica_store_export_begin)] +pub fn replica_store_export_begin() -> Result<JsValue, JsValue> { + export_lock_begin().map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let result = with_export_lock_bypass(|| export_snapshot(&exec)); + match result { + Ok(value) => Ok(value), + Err(err) => { + export_lock_end(); + Err(err) + } + } +} + +#[wasm_bindgen(js_name = replica_store_export_finish)] +pub fn replica_store_export_finish() -> Result<(), JsValue> { + export_lock_end(); + Ok(()) +} + +fn export_snapshot(exec: &WasmSqlExecutor) -> Result<JsValue, JsValue> { + let status = radroots_replica_sync_status(exec).map_err(|err| { + err_js(radroots_sql_core::SqlError::InvalidArgument( + err.to_string(), + )) + })?; + if let Some(message) = synced_export_error(status.pending_count, status.expected_count) { + return Err(err_js(radroots_sql_core::SqlError::InvalidArgument( + message, + ))); + } + let manifest = export_manifest(exec).map_err(err_js)?; + export_snapshot_value(manifest) +} + +fn export_snapshot_value(manifest: ReplicaStoreExportManifestRs) -> Result<JsValue, JsValue> { + let bytes_js = export_bytes(); + export_snapshot_value_with_bytes(manifest, bytes_js) +} + +fn export_snapshot_value_with_bytes( + manifest: ReplicaStoreExportManifestRs, + bytes_js: JsValue, +) -> Result<JsValue, JsValue> { + let manifest_js = serde_wasm_bindgen::to_value(&manifest).map_err(|err| { + err_js(radroots_sql_core::SqlError::SerializationError( + err.to_string(), + )) + })?; + let obj = js_sys::Object::new(); + js_sys::Reflect::set( + &obj, + &JsValue::from_str(EXPORT_MANIFEST_FIELD), + &manifest_js, + ) + .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; + js_sys::Reflect::set(&obj, &JsValue::from_str(EXPORT_DB_BYTES_FIELD), &bytes_js) + .map_err(|_| err_js(radroots_sql_core::SqlError::Internal))?; + Ok(JsValue::from(obj)) +} + +#[cfg(all(test, target_arch = "wasm32"))] +mod tests { + use super::export_snapshot_value_with_bytes; + use js_sys::{Reflect, Uint8Array}; + use wasm_bindgen::JsValue; + + #[wasm_bindgen_test::wasm_bindgen_test] + fn export_snapshot_value_includes_fields() { + let manifest = radroots_replica_store::ReplicaStoreExportManifestRs { + export_version: "1".to_string(), + replica_store_version: "0.0.0".to_string(), + backup_format_version: "0.0.0".to_string(), + schema_hash: "hash".to_string(), + schema: Vec::new(), + migrations: Vec::new(), + table_counts: Vec::new(), + }; + let bytes = Uint8Array::new_with_length(2); + let js = + export_snapshot_value_with_bytes(manifest, JsValue::from(bytes)).expect("snapshot"); + let manifest_rs = + Reflect::get(&js, &JsValue::from_str("manifest_rs")).expect("manifest_rs"); + let db_bytes = Reflect::get(&js, &JsValue::from_str("db_bytes")).expect("db_bytes"); + assert!(manifest_rs.is_object()); + assert!(db_bytes.is_object()); + } +} + +#[wasm_bindgen(js_name = replica_store_farm_create)] +pub fn replica_store_farm_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_find_one)] +pub fn replica_store_farm_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_find_many)] +pub fn replica_store_farm_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm::find_many(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_update)] +pub fn replica_store_farm_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_delete)] +pub fn replica_store_farm_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_create)] +pub fn replica_store_plot_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_find_one)] +pub fn replica_store_plot_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_find_many)] +pub fn replica_store_plot_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot::find_many(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_update)] +pub fn replica_store_plot_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_delete)] +pub fn replica_store_plot_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_gcs_location_create)] +pub fn replica_store_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::gcs_location::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_gcs_location_find_one)] +pub fn replica_store_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::gcs_location::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_gcs_location_find_many)] +pub fn replica_store_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::gcs_location::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_gcs_location_update)] +pub fn replica_store_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::gcs_location::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_gcs_location_delete)] +pub fn replica_store_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::gcs_location::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_gcs_location_create)] +pub fn replica_store_farm_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_gcs_location::create(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_gcs_location_find_one)] +pub fn replica_store_farm_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_gcs_location::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_gcs_location_find_many)] +pub fn replica_store_farm_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_gcs_location::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_gcs_location_update)] +pub fn replica_store_farm_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_gcs_location::update(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_gcs_location_delete)] +pub fn replica_store_farm_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_gcs_location::delete(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_gcs_location_create)] +pub fn replica_store_plot_gcs_location_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot_gcs_location::create(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_gcs_location_find_one)] +pub fn replica_store_plot_gcs_location_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot_gcs_location::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_gcs_location_find_many)] +pub fn replica_store_plot_gcs_location_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot_gcs_location::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_gcs_location_update)] +pub fn replica_store_plot_gcs_location_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot_gcs_location::update(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_gcs_location_delete)] +pub fn replica_store_plot_gcs_location_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotGcsLocationDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::plot_gcs_location::delete(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_tag_create)] +pub fn replica_store_farm_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_tag::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_tag_find_one)] +pub fn replica_store_farm_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_tag::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_tag_find_many)] +pub fn replica_store_farm_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_tag::find_many(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_tag_update)] +pub fn replica_store_farm_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_tag::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_tag_delete)] +pub fn replica_store_farm_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmTagDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_tag::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_tag_create)] +pub fn replica_store_plot_tag_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::plot_tag::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_tag_find_one)] +pub fn replica_store_plot_tag_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::plot_tag::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_tag_find_many)] +pub fn replica_store_plot_tag_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::plot_tag::find_many(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_tag_update)] +pub fn replica_store_plot_tag_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::plot_tag::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_plot_tag_delete)] +pub fn replica_store_plot_tag_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IPlotTagDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::plot_tag::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_create)] +pub fn replica_store_farm_member_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_member::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_find_one)] +pub fn replica_store_farm_member_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_member::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_find_many)] +pub fn replica_store_farm_member_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_update)] +pub fn replica_store_farm_member_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_member::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_delete)] +pub fn replica_store_farm_member_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::farm_member::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_claim_create)] +pub fn replica_store_farm_member_claim_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member_claim::create(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_claim_find_one)] +pub fn replica_store_farm_member_claim_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member_claim::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_claim_find_many)] +pub fn replica_store_farm_member_claim_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member_claim::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_claim_update)] +pub fn replica_store_farm_member_claim_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member_claim::update(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_farm_member_claim_delete)] +pub fn replica_store_farm_member_claim_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IFarmMemberClaimDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::farm_member_claim::delete(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_log_error_create)] +pub fn replica_store_log_error_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::log_error::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_log_error_find_one)] +pub fn replica_store_log_error_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::log_error::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_log_error_find_many)] +pub fn replica_store_log_error_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::log_error::find_many(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_log_error_update)] +pub fn replica_store_log_error_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::log_error::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_log_error_delete)] +pub fn replica_store_log_error_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ILogErrorDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::log_error::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_media_image_create)] +pub fn replica_store_media_image_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::media_image::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_media_image_find_one)] +pub fn replica_store_media_image_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::media_image::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_media_image_find_many)] +pub fn replica_store_media_image_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::media_image::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_media_image_update)] +pub fn replica_store_media_image_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::media_image::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_media_image_delete)] +pub fn replica_store_media_image_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: IMediaImageDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::media_image::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_create)] +pub fn replica_store_nostr_profile_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_profile::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_find_one)] +pub fn replica_store_nostr_profile_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_profile::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_find_many)] +pub fn replica_store_nostr_profile_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_profile::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_update)] +pub fn replica_store_nostr_profile_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_profile::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_delete)] +pub fn replica_store_nostr_profile_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_profile::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_event_head_create)] +pub fn replica_store_nostr_event_head_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_event_head::create(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_event_head_find_one)] +pub fn replica_store_nostr_event_head_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_event_head::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_event_head_find_many)] +pub fn replica_store_nostr_event_head_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_event_head::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_event_head_update)] +pub fn replica_store_nostr_event_head_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_event_head::update(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_event_head_delete)] +pub fn replica_store_nostr_event_head_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrEventHeadDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_event_head::delete(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_relay_create)] +pub fn replica_store_nostr_relay_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_relay::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_relay_find_one)] +pub fn replica_store_nostr_relay_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_relay::find_one(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_relay_find_many)] +pub fn replica_store_nostr_relay_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_relay::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_relay_update)] +pub fn replica_store_nostr_relay_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_relay::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_relay_delete)] +pub fn replica_store_nostr_relay_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrRelayDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::nostr_relay::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_create)] +pub fn replica_store_trade_product_create(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductCreate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::trade_product::create(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_find_one)] +pub fn replica_store_trade_product_find_one(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductFindOne = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product::find_one(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_find_many)] +pub fn replica_store_trade_product_find_many(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductFindMany = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product::find_many(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_update)] +pub fn replica_store_trade_product_update(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductUpdate = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::trade_product::update(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_delete)] +pub fn replica_store_trade_product_delete(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductDelete = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = + radroots_replica_store::trade_product::delete(&exec, &opts).map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_relay_set)] +pub fn replica_store_nostr_profile_relay_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_profile_relay::set(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_nostr_profile_relay_unset)] +pub fn replica_store_nostr_profile_relay_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: INostrProfileRelayRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::nostr_profile_relay::unset(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_location_set)] +pub fn replica_store_trade_product_location_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product_location::set(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_location_unset)] +pub fn replica_store_trade_product_location_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductLocationRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product_location::unset(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_media_set)] +pub fn replica_store_trade_product_media_set(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product_media::set(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} + +#[wasm_bindgen(js_name = replica_store_trade_product_media_unset)] +pub fn replica_store_trade_product_media_unset(opts_json: &str) -> Result<JsValue, JsValue> { + let opts: ITradeProductMediaRelation = parse_json(opts_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let out = radroots_replica_store::trade_product_media::unset(&exec, &opts) + .map_err(|e| err_js(e.error))?; + value_to_js(out) +} diff --git a/crates/replica_sync_wasm/Cargo.toml b/crates/replica_sync_wasm/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "radroots_replica_sync_wasm" +publish = false +version = "0.1.0-alpha" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "WASM boundary for Radroots replica synchronization" +repository.workspace = true +homepage.workspace = true +readme = "README" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +base64 = { workspace = true } +radroots_event = { workspace = true, default-features = false, features = [ + "serde", +] } +radroots_sdk_sql_wasm_runtime = { workspace = true } +radroots_replica_sync = { workspace = true, features = ["std", "legacy-ingest"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +uuid = { workspace = true, features = ["js"] } +wasm-bindgen = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/replica_sync_wasm/README b/crates/replica_sync_wasm/README @@ -0,0 +1,25 @@ +# radroots_replica_sync_wasm + +This is the README for `radroots_replica_sync_wasm`, which provides WebAssembly +bindings for `radroots_replica_sync` in the `radroots` core libraries. + +## Overview + + * wasm32 entry points for full sync and single-event ingest operations; + * integration with `WasmSqlExecutor` from `radroots_sql_core` for database + access; + * UUIDv7 id generation and JSON and base64 boundary handling for JavaScript + callers; + * a small target-specific wrapper around the Rust sync crate rather than a + separate sync engine. + +## Copyright + +Except as otherwise noted, all files in the `radroots_replica_sync_wasm` +distribution are + + Copyright (c) 2026 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `radroots_replica_sync_wasm` +distribution. diff --git a/crates/replica_sync_wasm/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs @@ -0,0 +1,179 @@ +#![forbid(unsafe_code)] + +#[cfg(target_arch = "wasm32")] +use base64::Engine; +#[cfg(target_arch = "wasm32")] +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use radroots_event::envelope::{EventEnvelope, EventEnvelopeParts}; +#[cfg(any(target_arch = "wasm32", test))] +use radroots_replica_sync::RadrootsReplicaIngestOutcome; +use radroots_replica_sync::RadrootsReplicaSyncRequest; +#[cfg(target_arch = "wasm32")] +use radroots_replica_sync::{ + RadrootsReplicaIdFactory, radroots_replica_ingest_event_with_factory, radroots_replica_sync_all, +}; +#[cfg(target_arch = "wasm32")] +use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor; +use serde::Deserialize; +#[cfg(target_arch = "wasm32")] +use uuid::Uuid; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +fn err_js<E: ToString>(err: E) -> JsValue { + JsValue::from_str(&err.to_string()) +} + +#[cfg(target_arch = "wasm32")] +struct WasmIdFactory; + +#[cfg(target_arch = "wasm32")] +impl RadrootsReplicaIdFactory for WasmIdFactory { + fn new_d_tag(&self) -> String { + let uuid = Uuid::now_v7(); + URL_SAFE_NO_PAD.encode(uuid.as_bytes()) + } +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct EventEnvelopeInput { + id: String, + author: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, +} + +pub fn parse_request_model(request_json: &str) -> Result<RadrootsReplicaSyncRequest, String> { + serde_json::from_str(request_json).map_err(|error| error.to_string()) +} + +pub fn parse_event_model(event_json: &str) -> Result<EventEnvelope, String> { + let envelope: EventEnvelopeInput = + serde_json::from_str(event_json).map_err(|error| error.to_string())?; + EventEnvelope::new(EventEnvelopeParts { + id: envelope.id, + author: envelope.author, + created_at: envelope.created_at, + kind: envelope.kind, + tags: envelope.tags, + content: envelope.content, + sig: envelope.sig, + }) + .map_err(|error| error.to_string()) +} + +#[cfg(any(target_arch = "wasm32", test))] +fn ingest_outcome_label(outcome: RadrootsReplicaIngestOutcome) -> &'static str { + match outcome { + RadrootsReplicaIngestOutcome::Applied => "applied", + RadrootsReplicaIngestOutcome::Excluded => "excluded", + RadrootsReplicaIngestOutcome::Rejected => "rejected", + RadrootsReplicaIngestOutcome::Skipped => "skipped", + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = replica_sync_sync_all)] +pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> { + let request = parse_request_model(request_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let bundle = radroots_replica_sync_all(&exec, &request).map_err(err_js)?; + serde_wasm_bindgen::to_value(&bundle).map_err(err_js) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen(js_name = replica_sync_ingest_event)] +pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> { + let event = parse_event_model(event_json).map_err(err_js)?; + let exec = WasmSqlExecutor::new(); + let factory = WasmIdFactory; + let outcome = + radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?; + Ok(JsValue::from_str(ingest_outcome_label(outcome))) +} + +#[cfg(test)] +mod tests { + use super::{ingest_outcome_label, parse_event_model, parse_request_model}; + use radroots_replica_sync::RadrootsReplicaIngestOutcome; + + fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String { + let mut fields = vec![ + format!(r#""id":"{}""#, "0".repeat(64)), + r#""created_at":123"#.to_owned(), + r#""kind":30023"#.to_owned(), + r#""tags":[["d","one"]]"#.to_owned(), + r#""content":"content""#.to_owned(), + format!(r#""sig":"{}""#, "f".repeat(128)), + ]; + if let Some(author) = author { + fields.push(format!(r#""author":"{author}""#)); + } + if let Some(pubkey) = pubkey { + fields.push(format!(r#""pubkey":"{pubkey}""#)); + } + format!("{{{}}}", fields.join(",")) + } + + #[test] + fn parse_event_accepts_author_domain_envelope() { + let author = "a".repeat(64); + let event = parse_event_model(&event_json(Some(author.as_str()), None)).expect("event"); + assert_eq!(event.author().to_hex(), author); + assert_eq!( + event.tags_as_vec(), + vec![vec!["d".to_owned(), "one".to_owned()]] + ); + } + + #[test] + fn parse_event_rejects_pubkey_wire_alias() { + let author = "a".repeat(64); + let error = parse_event_model(&event_json(Some(author.as_str()), Some(author.as_str()))) + .expect_err("error"); + assert!(error.contains("unknown field `pubkey`")); + } + + #[test] + fn parse_event_rejects_missing_author() { + let error = parse_event_model(&event_json(None, None)).expect_err("error"); + assert!(error.contains("missing field `author`")); + } + + #[test] + fn parse_event_rejects_malformed_json() { + let error = parse_event_model("{").expect_err("error"); + assert!(error.contains("EOF")); + } + + #[test] + fn parse_request_rejects_malformed_json() { + let error = parse_request_model("{").expect_err("error"); + assert!(error.contains("EOF")); + } + + #[test] + fn ingest_outcome_labels_cover_the_replica_contract() { + assert_eq!( + ingest_outcome_label(RadrootsReplicaIngestOutcome::Applied), + "applied" + ); + assert_eq!( + ingest_outcome_label(RadrootsReplicaIngestOutcome::Excluded), + "excluded" + ); + assert_eq!( + ingest_outcome_label(RadrootsReplicaIngestOutcome::Rejected), + "rejected" + ); + assert_eq!( + ingest_outcome_label(RadrootsReplicaIngestOutcome::Skipped), + "skipped" + ); + } +} diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -0,0 +1,126 @@ +[package] +name = "radroots_sdk" +publish = ["crates-io"] +version = "0.1.0-alpha" +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Host-neutral asynchronous client engine for Radroots" +repository.workspace = true +homepage.workspace = true +readme = "README.md" +documentation = "https://docs.rs/radroots_sdk" +keywords = ["radroots", "sdk", "local-food", "nostr"] +categories = ["api-bindings", "asynchronous", "network-programming"] +include = ["src/**", "tests/**", "!tests/package_boundary.rs", "examples/**", "Cargo.toml", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] +autotests = false +autoexamples = false + +[package.metadata.docs.rs] +features = ["memory", "sync", "nostr", "nip46", "local-signing", "geonames", "knowledge"] + +[lib] +name = "radroots_sdk" + +[features] +default = ["memory"] +memory = ["radroots_storage/memory"] +sqlite = ["dep:radroots_storage_sqlite"] +sync = ["dep:radroots_sync", "dep:uuid"] +nostr = [ + "sync", + "dep:radroots_nostr", + "dep:radroots_transport_nostr", +] +nip46 = [ + "nostr", + "dep:radroots_nostr_connect", +] +local-signing = [ + "dep:radroots_nostr", + "dep:radroots_secrets", + "radroots_nostr/signing", +] +radrootsd = [ + "sync", + "dep:reqwest", + "dep:serde", + "dep:serde_json", +] +geonames = ["dep:radroots_geonames"] +knowledge = [ + "radroots_event/knowledge", + "radroots_event_codec/knowledge", +] +native = ["sqlite", "sync", "local-signing"] +full = [ + "native", + "nostr", + "nip46", + "radrootsd", + "geonames", + "knowledge", +] + +[dependencies] +radroots_core = { workspace = true, default-features = false } +radroots_event = { workspace = true, default-features = false } +radroots_event_codec = { workspace = true, default-features = false, features = [ + "json", + "std", +] } +radroots_identity = { workspace = true, default-features = false, features = ["std"] } +radroots_protocol = { workspace = true, default-features = false } +radroots_signing = { workspace = true, default-features = false } +radroots_storage = { workspace = true, default-features = false } +radroots_trade = { workspace = true, default-features = false, features = [ + "json", + "std", +] } +radroots_transport = { workspace = true, default-features = false } + +radroots_geonames = { workspace = true, optional = true, default-features = false } +radroots_nostr = { workspace = true, optional = true, default-features = false } +radroots_nostr_connect = { workspace = true, optional = true, default-features = false } +radroots_secrets = { workspace = true, optional = true, default-features = false } +radroots_storage_sqlite = { workspace = true, optional = true, default-features = false } +radroots_sync = { workspace = true, optional = true, default-features = false } +radroots_transport_nostr = { workspace = true, optional = true, default-features = false } + +reqwest = { workspace = true, optional = true, default-features = false, features = [ + "json", + "rustls-tls", +] } +serde = { workspace = true, optional = true, features = ["derive"] } +serde_json = { workspace = true, optional = true, features = ["std"] } +uuid = { workspace = true, optional = true, features = ["v4"] } + +[dev-dependencies] +nostr = { workspace = true, features = ["std"] } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +[[test]] +name = "package_boundary" +path = "tests/package_boundary.rs" + +[[test]] +name = "lifecycle" +path = "tests/lifecycle.rs" + +[[test]] +name = "public_api" +path = "tests/public_api.rs" + +[[example]] +name = "safe_memory_client" +path = "examples/safe_memory_client.rs" +required-features = ["memory"] + +[[example]] +name = "transport_profile" +path = "examples/transport_profile.rs" + +[lints] +workspace = true diff --git a/crates/sdk/LICENSE-APACHE b/crates/sdk/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2026 Tyson Lupul + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/crates/sdk/LICENSE-MIT b/crates/sdk/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 Tyson Lupul + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/crates/sdk/README.md b/crates/sdk/README.md @@ -0,0 +1,171 @@ +# radroots_sdk + +`radroots_sdk` is the host-neutral asynchronous client engine for Radroots. +It composes the canonical event, trade, signing, transport, storage, and sync +crates without installing a runtime, starting workers, opening files, probing +the network, selecting an account, or choosing fallback transports. + +The crate root intentionally exports only `Client`, `ClientBuilder`, `Error`, +and `Result`. Advanced operations live in the `farm`, `listing`, `trade`, +`signing`, `transport`, `storage`, `sync`, `diagnostics`, and `capability` +modules. + +The package charter is the normative [`radroots_sdk` crate +specification](../../docs/specs/radroots_crates_release_v1.md#18-radroots_sdk). +This advanced front door is intended for CLI, Studio, FFI/mobile, and native +applications that need to compose storage, signing, transport, or sync +capabilities directly. Ordinary Rust applications should use the curated +`radroots` facade. + +## Getting started + +The default `memory` feature provides an inert, in-process backend. The caller +supplies its source generation; building the client creates no files, opens no +network connection, installs no runtime, and starts no worker. + +```rust +use radroots_sdk::{ClientBuilder, capability::CapabilityId}; +use radroots_storage::event::SourceGeneration; + +let generation = SourceGeneration::new([1; 32])?; +let client = ClientBuilder::memory(generation).build()?; +let storage = client + .capabilities() + .get(CapabilityId::CANONICAL_STORAGE); +assert!(storage.is_some()); + +# Ok::<(), Box<dyn std::error::Error>>(()) +``` + +The complete executable form, including explicit asynchronous shutdown, lives +in [`examples/safe_memory_client.rs`](examples/safe_memory_client.rs). The +side-effect-free transport-selection example lives in +[`examples/transport_profile.rs`](examples/transport_profile.rs). + +## Feature contract + +The complete public feature vocabulary is: + +| Feature | Capability | +| --- | --- | +| `memory` | deterministic in-process reference storage; the default feature | +| `sqlite` | explicit canonical SQLite storage construction | +| `sync` | composition with a caller-supplied canonical sync engine | +| `nostr` | Nostr conversion and concrete source/sink adapters; implies `sync` | +| `nip46` | NIP-46 signer provider; implies `nostr` | +| `local-signing` | explicit local signing and secret-provider adapters | +| `radrootsd` | explicitly invoked private daemon execution adapter; implies `sync` | +| `geonames` | concrete GeoNames provider integration | +| `knowledge` | deterministic knowledge event contracts/codecs | +| `native` | `sqlite`, `sync`, and `local-signing` | +| `full` | every supported production capability | + +There are no `runtime`, `local-runtime`, `signer-adapters`, +`transport-nostr-runtime`, `transport-nostr-client`, or fixture features. +Features compile capabilities; they do not perform I/O. Optional dependencies +are activated only by their owning feature. + +The supported qualification matrix is: + +```sh +cargo check -p radroots_sdk --all-targets --no-default-features +cargo check -p radroots_sdk --all-targets +cargo check -p radroots_sdk --all-targets --no-default-features --features memory +cargo check -p radroots_sdk --all-targets --no-default-features --features sqlite +cargo check -p radroots_sdk --all-targets --no-default-features --features sync +cargo check -p radroots_sdk --all-targets --no-default-features --features nostr +cargo check -p radroots_sdk --all-targets --no-default-features --features nip46 +cargo check -p radroots_sdk --all-targets --no-default-features --features local-signing +cargo check -p radroots_sdk --all-targets --no-default-features --features radrootsd +cargo check -p radroots_sdk --all-targets --no-default-features --features geonames +cargo check -p radroots_sdk --all-targets --no-default-features --features knowledge +cargo check -p radroots_sdk --all-targets --no-default-features --features native +cargo check -p radroots_sdk --all-targets --no-default-features --features full +cargo check -p radroots_sdk --all-targets --all-features +``` + +## Explicit composition + +`ClientBuilder` requires a storage capability. `ClientBuilder::memory(...)` +and `ClientBuilder::sqlite(...)` are explicit constructors; merely enabling a +feature or constructing an empty builder creates no resource. Signers, event +sources, event sinks, and the sync engine are injected separately. + +Native mobile hosts can retain one client while changing host-owned identity +and relay selection. `signing::Slot` accepts a key restored from secure host +storage or generates a one-time `nsec` handoff; it never persists that secret. +`transport::NostrSlot` validates a complete relay set before atomically +installing it. `ClientBuilder::host_sync(sync::HostPolicy)` explicitly opts +SDK-created memory or SQLite storage into system-clock and random operation-ID +policy without creating a runtime, timer, retry loop, or worker. + +With `sync`, `nostr`, and `local-signing`, `Client::social()` exposes bounded +profile/post fetch and publish operations. Fetch verifies and durably ingests +each accepted event. Publish durably commits a signed event, then performs one +explicit delivery pass and reports whether delivery remains pending. Host UI +lifecycle code owns polling, background policy, keychain access, and any later +retry. Profile authoring is deliberately media-free until the host can supply +the canonical byte-verified media descriptor required by the event contract. + +Transport profiles are explicit. `Profile::local_only()` contains no target. +`Profile::delivery(...)` retains the exact canonical target set and +satisfaction policy. Preview transports report unavailable and never +substitute Nostr, daemon, local persistence, or another route. + +Farm, listing, and trade preparation is deterministic and side-effect-free. +Commit operations accept native operation and idempotency identities, +cancellation policy, and an explicit transport profile, then return the +canonical sync receipt. Repeating the same idempotent request is the supported +resume/replay path. + +Preparation has no commit point. During commit, durable local acceptance is +the first commit point; a cancellation observed before that point returns +without claiming acceptance. Cancellation after durable acceptance cannot +roll the accepted event back: the returned error/receipt identifies the safe +resume path, and retrying with the same idempotency identity must not create a +second logical operation. Dropping `Client::close` before completion leaves +the shared client in a retry-required closing state; call `close` again. + +The SDK does not define a second wire model. Canonical lower-crate domain and +protocol types own serialization, validation, versioning, and size limits. +SDK request, plan, and receipt structs are constructor-led orchestration types; +their private representation is not a persistence or interchange format. + +## Reliability and privacy + +Backup, restore, integrity, and status operations delegate to +`radroots_storage::StorageReliability` and return its native versioned plans, +manifests, revisions, stages, and status values. Restore is staged and must be +explicitly finalized. Client shutdown is explicit and asynchronous. + +Public farm/listing events contain only the coarse locality represented by the +canonical event model. Exact coordinates, private trade terms, protected +content, and key references remain behind the private-artifact and secrets +SPIs. Diagnostics contain only capability and canonical storage status. Public +errors and daemon failures use stable, redacted classifications while retaining +private source chains for local diagnostics. + +Signer material and bearer credentials are caller-owned capabilities. The SDK +does not read a keyring, persist secrets, or include credentials +in `Debug`, `Display`, diagnostics, receipts, or public error text. Hosts remain +responsible for protecting source chains and any lower-level logs they choose +to expose. When explicitly requested, `signing::Slot::generate` creates one +ephemeral key and immediately hands its only persistence representation to the +host for secure custody. + +## Daemon execution + +The `radrootsd` feature compiles a private HTTP/RPC adapter using the versioned +`radroots_protocol::radrootsd::transport_publish::v5` contract. Constructing +`transport::DaemonDelivery` is inert. Network contact occurs only when the host +invokes `deliver`; bearer credentials are redacted, HTTP error bodies are not +surfaced, and the response must match the signed event and requested policies. + +## Release posture + +This package is enabled for package-realistic validation only. Actual crates.io +publication remains blocked pending the approval packet and a separately +authorized operator step. The crate is licensed under `MIT OR Apache-2.0`. + +The reviewed all-features public API baseline is recorded at +[`docs/api/radroots_sdk-0.1.0-alpha.txt`](../../docs/api/radroots_sdk-0.1.0-alpha.txt). diff --git a/crates/sdk/examples/safe_memory_client.rs b/crates/sdk/examples/safe_memory_client.rs @@ -0,0 +1,20 @@ +//! Builds and explicitly closes the safe in-process client. + +use radroots_sdk::{ClientBuilder, capability::CapabilityId}; +use radroots_storage::event::SourceGeneration; + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + let generation = SourceGeneration::new([1; 32])?; + let client = ClientBuilder::memory(generation).build()?; + + assert!( + client + .capabilities() + .get(CapabilityId::CANONICAL_STORAGE) + .is_some() + ); + + client.close().await?; + Ok(()) +} diff --git a/crates/sdk/examples/transport_profile.rs b/crates/sdk/examples/transport_profile.rs @@ -0,0 +1,11 @@ +//! Selects local-only behavior without constructing or probing a transport. + +use radroots_sdk::transport::Profile; + +fn main() { + let profile = Profile::local_only(); + + assert!(profile.is_local_only()); + assert!(profile.targets().is_none()); + assert!(profile.satisfaction().is_none()); +} diff --git a/crates/sdk/src/adapters/mod.rs b/crates/sdk/src/adapters/mod.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "radrootsd")] +pub(crate) mod radrootsd; diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -0,0 +1,352 @@ +use core::fmt; +use core::time::Duration; + +use radroots_event::draft::SignedEvent; +use radroots_protocol::radrootsd::transport_publish::v5::{ + DeliveryPolicy as TransportPublishDeliveryPolicy, Error as TransportPublishProtocolError, + EventRequest as TransportPublishEventRequest, EventResponse as TransportPublishEventResponse, + METHOD_EVENT, TargetPolicy as TransportPublishTargetPolicy, +}; +use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; + +pub const SDK_RADROOTSD_PUBLISH_REQUEST_ID: &str = "radroots-sdk-transport-publish-event"; +pub const SDK_RADROOTSD_PUBLISH_MAX_TARGETS: usize = 20; + +#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum RadrootsdAuth { + #[default] + None, + BearerToken(String), +} + +impl fmt::Debug for RadrootsdAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => f.write_str("None"), + Self::BearerToken(_) => f.write_str("BearerToken(<redacted>)"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsdPublishConfig { + pub endpoint: String, + pub auth: RadrootsdAuth, + pub timeout: Duration, +} + +impl RadrootsdPublishConfig { + pub fn new(endpoint: impl Into<String>) -> Self { + Self { + endpoint: endpoint.into(), + auth: RadrootsdAuth::None, + timeout: Duration::from_secs(10), + } + } + + pub fn with_auth(mut self, auth: RadrootsdAuth) -> Self { + self.auth = auth; + self + } + + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsdPublishAdapter { + config: RadrootsdPublishConfig, +} + +impl RadrootsdPublishAdapter { + pub fn new(config: RadrootsdPublishConfig) -> Self { + Self { config } + } + + #[cfg(test)] + pub fn config(&self) -> &RadrootsdPublishConfig { + &self.config + } + + pub async fn publish_signed_event( + &self, + request: RadrootsdPublishRequest, + ) -> Result<TransportPublishEventResponse, RadrootsdError> { + let event_identity = + RadrootsdPublishEventIdentity::from_signed_event(&request.signed_event); + let request = request.into_protocol_request(); + request + .validate(SDK_RADROOTSD_PUBLISH_MAX_TARGETS) + .map_err(RadrootsdError::from_protocol)?; + let response = publish_event( + self.config.endpoint.as_str(), + &self.config.auth, + &request, + self.config.timeout, + ) + .await?; + validate_transport_publish_response_for_request(&request, &event_identity, &response)?; + Ok(response) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsdPublishRequest { + pub signed_event: SignedEvent, + pub target_policy: TransportPublishTargetPolicy, + pub delivery_policy: TransportPublishDeliveryPolicy, + pub idempotency_key: Option<String>, + pub timeout_ms: Option<u64>, +} + +impl RadrootsdPublishRequest { + fn into_protocol_request(self) -> TransportPublishEventRequest { + TransportPublishEventRequest { + raw_event_json: self.signed_event.raw_json().to_owned(), + target_policy: self.target_policy, + delivery_policy: self.delivery_policy, + idempotency_key: self.idempotency_key, + timeout_ms: self.timeout_ms, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RadrootsdPublishEventIdentity { + event_id: String, + pubkey: String, + kind: u32, +} + +impl RadrootsdPublishEventIdentity { + fn from_signed_event(event: &SignedEvent) -> Self { + Self { + event_id: event.id_str().to_owned(), + pubkey: event.pubkey().to_hex().to_owned(), + kind: event.kind(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsdError { + InvalidAuthHeader(String), + InvalidRequest(String), + Http(String), + JsonRpc { code: i64, message: String }, + MalformedResponse(String), +} + +impl RadrootsdError { + fn from_protocol(error: TransportPublishProtocolError) -> Self { + Self::InvalidRequest(error.to_string()) + } +} + +impl fmt::Display for RadrootsdError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidAuthHeader(value) => { + write!(f, "invalid radrootsd bearer token header: {value}") + } + Self::InvalidRequest(value) => f.write_str(value), + Self::Http(value) => f.write_str(value), + Self::MalformedResponse(value) => f.write_str(value), + Self::JsonRpc { code, message } => { + write!(f, "radrootsd jsonrpc failed {code}: {message}") + } + } + } +} + +impl std::error::Error for RadrootsdError {} + +#[derive(Debug, Deserialize)] +struct JsonRpcEnvelope<T> { + jsonrpc: Option<String>, + id: Option<Value>, + result: Option<T>, + error: Option<JsonRpcError>, +} + +#[derive(Debug, Deserialize)] +struct JsonRpcError { + code: i64, + message: String, +} + +pub async fn publish_event( + endpoint: &str, + auth: &RadrootsdAuth, + request: &TransportPublishEventRequest, + timeout: Duration, +) -> Result<TransportPublishEventResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + SDK_RADROOTSD_PUBLISH_REQUEST_ID, + METHOD_EVENT, + request, + timeout, + ) + .await +} + +fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { + let mut headers = HeaderMap::new(); + match auth { + RadrootsdAuth::None => Ok(headers), + RadrootsdAuth::BearerToken(token) => { + let header = format!("Bearer {token}"); + let value = HeaderValue::from_str(header.as_str()) + .map_err(|err| RadrootsdError::InvalidAuthHeader(err.to_string()))?; + headers.insert(AUTHORIZATION, value); + Ok(headers) + } + } +} + +#[cfg(test)] +pub fn publish_event_request_json( + request: &TransportPublishEventRequest, +) -> Result<Value, RadrootsdError> { + Ok(serde_json::to_value(request).expect("radrootsd transport publish request serializes")) +} + +fn http_status_error(status: reqwest::StatusCode, body: &str) -> RadrootsdError { + let body_summary = if body.is_empty() { + "response body empty".to_owned() + } else { + format!("response body omitted ({} bytes)", body.len()) + }; + RadrootsdError::Http(format!( + "radrootsd returned http {}: {}", + status.as_u16(), + body_summary + )) +} + +fn decode_jsonrpc_response<R>( + method: &str, + expected_id: &str, + body: &str, +) -> Result<R, RadrootsdError> +where + R: DeserializeOwned, +{ + let envelope: JsonRpcEnvelope<R> = serde_json::from_str(body).map_err(|err| { + RadrootsdError::MalformedResponse(format!("decode radrootsd {method} response: {err}")) + })?; + if envelope.jsonrpc.as_deref() != Some("2.0") { + return Err(RadrootsdError::MalformedResponse(format!( + "radrootsd {method} returned invalid jsonrpc version" + ))); + } + let expected_id_value = Value::String(expected_id.to_owned()); + if envelope.id.as_ref() != Some(&expected_id_value) { + return Err(RadrootsdError::MalformedResponse(format!( + "radrootsd {method} returned mismatched jsonrpc id" + ))); + } + match (envelope.result, envelope.error) { + (Some(result), None) => Ok(result), + (None, Some(error)) => Err(RadrootsdError::JsonRpc { + code: error.code, + message: error.message, + }), + (Some(_), Some(error)) => Err(RadrootsdError::MalformedResponse(format!( + "radrootsd {method} returned result and error: {} {}", + error.code, error.message + ))), + (None, None) => Err(RadrootsdError::MalformedResponse(format!( + "radrootsd {method} returned neither result nor error" + ))), + } +} + +async fn jsonrpc_call<P, R>( + endpoint: &str, + auth: &RadrootsdAuth, + request_id: &str, + method: &str, + params: &P, + timeout: Duration, +) -> Result<R, RadrootsdError> +where + P: Serialize + ?Sized, + R: DeserializeOwned, +{ + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .map_err(|err| RadrootsdError::Http(format!("build radrootsd client: {err}")))?; + let mut request_builder = client + .post(endpoint) + .headers(auth_headers(auth)?) + .json(&json!({ + "jsonrpc": "2.0", + "id": request_id, + "method": method, + "params": params, + })); + + request_builder = request_builder.header(CONTENT_TYPE, "application/json"); + + let response = request_builder + .send() + .await + .map_err(|err| RadrootsdError::Http(format!("send radrootsd {method} request: {err}")))?; + let status = response.status(); + let body = response + .text() + .await + .map_err(|err| RadrootsdError::Http(format!("read radrootsd response body: {err}")))?; + + if !status.is_success() { + return Err(http_status_error(status, body.as_str())); + } + + decode_jsonrpc_response(method, request_id, body.as_str()) +} + +fn validate_transport_publish_response_for_request( + request: &TransportPublishEventRequest, + event_identity: &RadrootsdPublishEventIdentity, + response: &TransportPublishEventResponse, +) -> Result<(), RadrootsdError> { + response.job.validate().map_err(|error| { + RadrootsdError::MalformedResponse(format!( + "radrootsd transport publish response invalid: {error}" + )) + })?; + if response.job.event_id != event_identity.event_id { + return Err(response_mismatch("event_id")); + } + if response.job.pubkey != event_identity.pubkey { + return Err(response_mismatch("pubkey")); + } + if response.job.event_kind != event_identity.kind { + return Err(response_mismatch("event_kind")); + } + if response.job.delivery_policy != request.delivery_policy { + return Err(response_mismatch("delivery_policy")); + } + if response.job.target_policy != request.target_policy { + return Err(response_mismatch("target_policy")); + } + Ok(()) +} + +fn response_mismatch(field: &str) -> RadrootsdError { + RadrootsdError::MalformedResponse(format!( + "radrootsd transport publish response {field} does not match request" + )) +} + +#[cfg(test)] +#[path = "../../tests/unit/adapters_radrootsd_tests.rs"] +mod tests; diff --git a/crates/sdk/src/capability.rs b/crates/sdk/src/capability.rs @@ -0,0 +1,423 @@ +//! Side-effect-free client capability reporting. + +use std::collections::{BTreeMap, BTreeSet}; + +/// Stable runtime identity for an SDK capability. +/// +/// These values intentionally describe behavior rather than Cargo features. +/// Construction is private so every reported ID comes from the governed +/// catalog below. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CapabilityId(&'static str); + +impl CapabilityId { + /// Canonical storage operations. + pub const CANONICAL_STORAGE: Self = Self("storage.canonical"); + /// Persistent storage operations. + pub const PERSISTENT_STORAGE: Self = Self("storage.persistent"); + /// Storage backup and restore operations. + pub const BACKUP_RESTORE: Self = Self("storage.backup-restore"); + /// Local signing. + pub const LOCAL_SIGNING: Self = Self("signing.local"); + /// NIP-46 remote signing. + pub const NIP46_SIGNING: Self = Self("signing.nip46"); + /// Nostr event fetch. + pub const NOSTR_FETCH: Self = Self("transport.nostr.fetch"); + /// Nostr event delivery. + pub const NOSTR_DELIVERY: Self = Self("transport.nostr.delivery"); + /// Reticulum event fetch preview. + pub const RETICULUM_FETCH: Self = Self("transport.reticulum.fetch"); + /// Reticulum event delivery preview. + pub const RETICULUM_DELIVERY: Self = Self("transport.reticulum.delivery"); + /// Mesh transport preview. + pub const MESH_TRANSPORT: Self = Self("transport.mesh"); + /// SimpleX transport experiment. + pub const SIMPLEX_TRANSPORT: Self = Self("transport.simplex"); + /// Daemon-mediated event delivery. + pub const DAEMON_DELIVERY: Self = Self("transport.daemon.delivery"); + /// Inbound synchronization. + pub const SYNC_PULL: Self = Self("sync.pull"); + /// Outbound synchronization. + pub const SYNC_PUSH: Self = Self("sync.push"); + /// Farm event publication. + pub const FARM_PUBLICATION: Self = Self("product.farm.publish"); + /// Listing event publication. + pub const LISTING_PUBLICATION: Self = Self("product.listing.publish"); + /// Trade command execution. + pub const TRADE_COMMANDS: Self = Self("product.trade.command"); + /// Trade queries. + pub const TRADE_QUERIES: Self = Self("product.trade.query"); + /// Knowledge event support. + pub const KNOWLEDGE_EVENTS: Self = Self("event.knowledge"); + + /// Returns the stable presentation-independent identity. + #[must_use] + pub const fn as_str(self) -> &'static str { + self.0 + } +} + +impl std::fmt::Display for CapabilityId { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.0) + } +} + +/// Product maturity independent of runtime availability. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Maturity { + /// Supported Release V1 behavior. + Stable, + /// Preserved pre-stable behavior with an explicit compatibility warning. + Preview, + /// Exploratory behavior without a compatibility commitment. + Experimental, +} + +/// Current runtime availability independent of compilation and configuration. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Availability { + /// The configured capability is ready. + Available, + /// The configured capability is usable with reduced functionality. + Degraded, + /// The capability is compiled but not currently usable. + Unavailable, + /// The capability is not supported by this build. + Unsupported, +} + +/// One immutable capability observation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CapabilityStatus { + id: CapabilityId, + compiled: bool, + configured: bool, + availability: Availability, + maturity: Maturity, +} + +impl CapabilityStatus { + /// Returns the stable runtime identity. + #[must_use] + pub const fn id(self) -> CapabilityId { + self.id + } + + /// Returns whether support was compiled into this package. + #[must_use] + pub const fn is_compiled(self) -> bool { + self.compiled + } + + /// Returns whether the host configured the capability for this client. + #[must_use] + pub const fn is_configured(self) -> bool { + self.configured + } + + /// Returns the current side-effect-free availability observation. + #[must_use] + pub const fn availability(self) -> Availability { + self.availability + } + + /// Returns the independent product maturity classification. + #[must_use] + pub const fn maturity(self) -> Maturity { + self.maturity + } +} + +/// Complete deterministic capability report for one client observation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapabilityReport { + statuses: Vec<CapabilityStatus>, +} + +impl CapabilityReport { + /// Returns all known capabilities in stable catalog order. + pub fn iter(&self) -> impl ExactSizeIterator<Item = &CapabilityStatus> { + self.statuses.iter() + } + + /// Finds one capability by stable runtime identity. + #[must_use] + pub fn get(&self, id: CapabilityId) -> Option<CapabilityStatus> { + self.statuses.iter().copied().find(|status| status.id == id) + } +} + +#[derive(Clone, Copy)] +struct Definition { + id: CapabilityId, + maturity: Maturity, + compiled: bool, +} + +const CATALOG: &[Definition] = &[ + Definition::stable(CapabilityId::CANONICAL_STORAGE, true), + Definition::stable(CapabilityId::PERSISTENT_STORAGE, cfg!(feature = "sqlite")), + Definition::stable(CapabilityId::BACKUP_RESTORE, true), + Definition::stable(CapabilityId::LOCAL_SIGNING, cfg!(feature = "local-signing")), + Definition::stable(CapabilityId::NIP46_SIGNING, cfg!(feature = "nip46")), + Definition::stable(CapabilityId::NOSTR_FETCH, cfg!(feature = "nostr")), + Definition::stable(CapabilityId::NOSTR_DELIVERY, cfg!(feature = "nostr")), + Definition::preview(CapabilityId::RETICULUM_FETCH), + Definition::preview(CapabilityId::RETICULUM_DELIVERY), + Definition::experimental(CapabilityId::MESH_TRANSPORT), + Definition::experimental(CapabilityId::SIMPLEX_TRANSPORT), + Definition::stable(CapabilityId::DAEMON_DELIVERY, cfg!(feature = "radrootsd")), + Definition::stable(CapabilityId::SYNC_PULL, cfg!(feature = "sync")), + Definition::stable(CapabilityId::SYNC_PUSH, cfg!(feature = "sync")), + Definition::stable(CapabilityId::FARM_PUBLICATION, true), + Definition::stable(CapabilityId::LISTING_PUBLICATION, true), + Definition::stable(CapabilityId::TRADE_COMMANDS, true), + Definition::stable(CapabilityId::TRADE_QUERIES, true), + Definition::stable(CapabilityId::KNOWLEDGE_EVENTS, cfg!(feature = "knowledge")), +]; + +impl Definition { + const fn stable(id: CapabilityId, compiled: bool) -> Self { + Self { + id, + maturity: Maturity::Stable, + compiled, + } + } + + const fn preview(id: CapabilityId) -> Self { + Self { + id, + maturity: Maturity::Preview, + compiled: false, + } + } + + const fn experimental(id: CapabilityId) -> Self { + Self { + id, + maturity: Maturity::Experimental, + compiled: false, + } + } +} + +pub(crate) struct Context<'a> { + pub(crate) storage: bool, + pub(crate) signer: bool, + pub(crate) source: bool, + pub(crate) sink: bool, + pub(crate) sync: bool, + pub(crate) lifecycle_availability: Availability, + pub(crate) explicitly_configured: &'a BTreeSet<CapabilityId>, + pub(crate) overrides: &'a BTreeMap<CapabilityId, Availability>, +} + +pub(crate) fn report(context: Context<'_>) -> CapabilityReport { + let statuses = CATALOG + .iter() + .map(|definition| { + let configured = configured(definition.id, &context); + let availability = if !definition.compiled { + Availability::Unsupported + } else if !configured { + Availability::Unavailable + } else if context.lifecycle_availability != Availability::Available { + context.lifecycle_availability + } else { + context + .overrides + .get(&definition.id) + .copied() + .unwrap_or(context.lifecycle_availability) + }; + CapabilityStatus { + id: definition.id, + compiled: definition.compiled, + configured, + availability, + maturity: definition.maturity, + } + }) + .collect(); + CapabilityReport { statuses } +} + +fn configured(id: CapabilityId, context: &Context<'_>) -> bool { + match id { + CapabilityId::CANONICAL_STORAGE + | CapabilityId::BACKUP_RESTORE + | CapabilityId::TRADE_QUERIES => context.storage, + CapabilityId::SYNC_PULL => (context.sync, context.source) == (true, true), + CapabilityId::SYNC_PUSH => (context.sync, context.sink) == (true, true), + CapabilityId::FARM_PUBLICATION + | CapabilityId::LISTING_PUBLICATION + | CapabilityId::TRADE_COMMANDS => (context.signer, context.sink) == (true, true), + CapabilityId::KNOWLEDGE_EVENTS => cfg!(feature = "knowledge"), + CapabilityId::RETICULUM_FETCH + | CapabilityId::RETICULUM_DELIVERY + | CapabilityId::MESH_TRANSPORT + | CapabilityId::SIMPLEX_TRANSPORT => false, + _ => context.explicitly_configured.contains(&id), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn catalog_ids_are_unique_and_not_feature_names() { + for (index, definition) in CATALOG.iter().enumerate() { + assert!( + !CATALOG[..index] + .iter() + .any(|candidate| candidate.id == definition.id), + "duplicate capability {}", + definition.id + ); + assert!(definition.id.as_str().contains('.')); + } + } + + #[test] + fn maturity_and_unsupported_preview_states_are_independent() { + let overrides = BTreeMap::new(); + let explicitly_configured = BTreeSet::new(); + let report = report(Context { + storage: true, + signer: false, + source: false, + sink: false, + sync: false, + lifecycle_availability: Availability::Available, + explicitly_configured: &explicitly_configured, + overrides: &overrides, + }); + let storage = report + .get(CapabilityId::CANONICAL_STORAGE) + .expect("storage"); + assert!(storage.is_compiled()); + assert!(storage.is_configured()); + assert_eq!(storage.availability(), Availability::Available); + assert_eq!(storage.maturity(), Maturity::Stable); + + let knowledge = report + .get(CapabilityId::KNOWLEDGE_EVENTS) + .expect("knowledge"); + assert_eq!(knowledge.is_compiled(), cfg!(feature = "knowledge")); + assert_eq!(knowledge.is_configured(), cfg!(feature = "knowledge")); + + let reticulum = report + .get(CapabilityId::RETICULUM_FETCH) + .expect("reticulum"); + assert!(!reticulum.is_compiled()); + assert!(!reticulum.is_configured()); + assert_eq!(reticulum.availability(), Availability::Unsupported); + assert_eq!(reticulum.maturity(), Maturity::Preview); + + let mesh = report.get(CapabilityId::MESH_TRANSPORT).expect("mesh"); + assert_eq!(mesh.maturity(), Maturity::Experimental); + } + + #[test] + fn configuration_and_availability_matrix_covers_every_decision_path() { + let explicitly_configured = BTreeSet::from([ + CapabilityId::PERSISTENT_STORAGE, + CapabilityId::NIP46_SIGNING, + CapabilityId::NOSTR_FETCH, + ]); + let overrides = BTreeMap::from([ + (CapabilityId::PERSISTENT_STORAGE, Availability::Degraded), + (CapabilityId::NOSTR_FETCH, Availability::Unavailable), + ]); + + let complete = report(Context { + storage: false, + signer: true, + source: true, + sink: true, + sync: true, + lifecycle_availability: Availability::Available, + explicitly_configured: &explicitly_configured, + overrides: &overrides, + }); + assert!( + complete + .get(CapabilityId::SYNC_PULL) + .expect("pull") + .is_configured() + ); + assert!( + complete + .get(CapabilityId::SYNC_PUSH) + .expect("push") + .is_configured() + ); + assert!( + complete + .get(CapabilityId::FARM_PUBLICATION) + .expect("farm") + .is_configured() + ); + assert_eq!( + complete + .get(CapabilityId::NOSTR_FETCH) + .expect("fetch") + .availability(), + if cfg!(feature = "nostr") { + Availability::Unavailable + } else { + Availability::Unsupported + } + ); + + let partial = report(Context { + storage: false, + signer: true, + source: false, + sink: false, + sync: true, + lifecycle_availability: Availability::Degraded, + explicitly_configured: &explicitly_configured, + overrides: &overrides, + }); + assert!( + !partial + .get(CapabilityId::SYNC_PULL) + .expect("pull") + .is_configured() + ); + assert!( + !partial + .get(CapabilityId::SYNC_PUSH) + .expect("push") + .is_configured() + ); + assert!( + !partial + .get(CapabilityId::TRADE_COMMANDS) + .expect("trade") + .is_configured() + ); + if partial + .get(CapabilityId::NIP46_SIGNING) + .expect("nip46") + .is_compiled() + { + assert_eq!( + partial + .get(CapabilityId::NIP46_SIGNING) + .expect("nip46") + .availability(), + Availability::Degraded + ); + } + assert_eq!(complete.iter().len(), CATALOG.len()); + assert_eq!( + CapabilityId::NOSTR_FETCH.to_string(), + "transport.nostr.fetch" + ); + } +} diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -0,0 +1,1668 @@ +//! Client construction and lifecycle. + +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }, +}; + +use radroots_signing::Signer; +use radroots_storage::Storage; +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +use radroots_storage::authored_delivery::AuthoredDeliveryState; +#[cfg(feature = "memory")] +use radroots_storage::{event::SourceGeneration, memory::MemoryStorage}; +use radroots_transport::{EventSink, EventSource}; + +use crate::{ + Error, Result, + capability::{Availability, CapabilityId, CapabilityReport}, +}; + +/// Cloneable handle to a composed Radroots client. +#[derive(Clone)] +pub struct Client { + inner: Arc<ClientInner>, +} + +/// Explicit composition boundary for a [`Client`]. +#[derive(Default)] +pub struct ClientBuilder { + storage: Option<Arc<dyn Storage>>, + #[cfg(feature = "sync")] + sync_storage: Option<Arc<dyn radroots_sync::policy::SyncStorage>>, + signer: Option<Arc<dyn Signer>>, + source: Option<Arc<dyn EventSource>>, + sink: Option<Arc<dyn EventSink>>, + #[cfg(feature = "sync")] + sync: Option<radroots_sync::Engine>, + #[cfg(feature = "sync")] + host_sync: Option<crate::sync::HostPolicy>, + #[cfg(feature = "local-signing")] + signing_slot: Option<crate::signing::Slot>, + #[cfg(feature = "nostr")] + nostr_slot: Option<crate::transport::NostrSlot>, + capability_availability: BTreeMap<CapabilityId, Availability>, + explicitly_configured_capabilities: BTreeSet<CapabilityId>, +} + +struct ClientInner { + storage: Arc<dyn Storage>, + signer: Option<Arc<dyn Signer>>, + source: Option<Arc<dyn EventSource>>, + sink: Option<Arc<dyn EventSink>>, + #[cfg(feature = "sync")] + sync: Option<radroots_sync::Engine>, + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + signing_slot: Option<crate::signing::Slot>, + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + nostr_slot: Option<crate::transport::NostrSlot>, + capability_availability: BTreeMap<CapabilityId, Availability>, + explicitly_configured_capabilities: BTreeSet<CapabilityId>, + lifecycle: AtomicU8, +} + +const OPEN: u8 = 0; +const CLOSING: u8 = 1; +const CLOSE_RETRY_REQUIRED: u8 = 2; +const CLOSED: u8 = 3; + +/// Host-authored, media-free profile replacement. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProfileDraft { + name: String, + display_name: Option<String>, + about: Option<String>, + nip05: Option<String>, + bot: Option<bool>, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl ProfileDraft { + /// Creates a complete replacement with the required canonical name. + #[must_use] + pub fn new(name: impl Into<String>) -> Self { + Self { + name: name.into(), + display_name: None, + about: None, + nip05: None, + bot: None, + } + } + + /// Sets the optional display name. + #[must_use] + pub fn with_display_name(mut self, value: impl Into<String>) -> Self { + self.display_name = Some(value.into()); + self + } + + /// Sets the optional profile description. + #[must_use] + pub fn with_about(mut self, value: impl Into<String>) -> Self { + self.about = Some(value.into()); + self + } + + /// Sets a syntax-checked NIP-05 identifier at publish time. + #[must_use] + pub fn with_nip05(mut self, value: impl Into<String>) -> Self { + self.nip05 = Some(value.into()); + self + } + + /// Sets the optional NIP-05 bot marker. + #[must_use] + pub const fn with_bot(mut self, value: bool) -> Self { + self.bot = Some(value); + self + } +} + +/// One verified, durably ingested profile observation. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProfileEvent { + event_id: String, + author: String, + created_at: u64, + name: Option<String>, + display_name: Option<String>, + about: Option<String>, + picture: Option<String>, + banner: Option<String>, + nip05: Option<String>, + bot: Option<bool>, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl ProfileEvent { + /// Returns the canonical event identifier. + pub fn event_id(&self) -> &str { + self.event_id.as_str() + } + /// Returns the canonical author public key. + pub fn author(&self) -> &str { + self.author.as_str() + } + /// Returns the event timestamp in Unix seconds. + pub const fn created_at(&self) -> u64 { + self.created_at + } + /// Returns the projected profile name. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + /// Returns the projected display name. + pub fn display_name(&self) -> Option<&str> { + self.display_name.as_deref() + } + /// Returns the projected description. + pub fn about(&self) -> Option<&str> { + self.about.as_deref() + } + /// Returns the unverified inbound picture reference. + pub fn picture(&self) -> Option<&str> { + self.picture.as_deref() + } + /// Returns the unverified inbound banner reference. + pub fn banner(&self) -> Option<&str> { + self.banner.as_deref() + } + /// Returns the syntax-checked, unresolved NIP-05 identifier. + pub fn nip05(&self) -> Option<&str> { + self.nip05.as_deref() + } + /// Returns the optional bot marker. + pub const fn bot(&self) -> Option<bool> { + self.bot + } +} + +/// One verified, durably ingested kind-1 social event. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PostEvent { + event_id: String, + author: String, + created_at: u64, + content: String, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl PostEvent { + /// Returns the canonical event identifier. + pub fn event_id(&self) -> &str { + self.event_id.as_str() + } + /// Returns the canonical author public key. + pub fn author(&self) -> &str { + self.author.as_str() + } + /// Returns the event timestamp in Unix seconds. + pub const fn created_at(&self) -> u64 { + self.created_at + } + /// Returns the canonical event content. + pub fn content(&self) -> &str { + self.content.as_str() + } +} + +/// Result of one explicit local commit followed by one delivery pass. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PublishReceipt { + event_id: String, + replay: bool, + delivery_state: AuthoredDeliveryState, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl PublishReceipt { + /// Returns the canonical signed event identifier committed locally. + pub fn event_id(&self) -> &str { + self.event_id.as_str() + } + /// Returns whether preparation replayed an identical durable operation. + pub const fn is_replay(&self) -> bool { + self.replay + } + /// Returns the complete durable delivery state after this explicit pass. + pub const fn delivery_state(&self) -> AuthoredDeliveryState { + self.delivery_state + } + /// Returns whether this explicit pass satisfied the delivery policy. + pub const fn is_delivered(&self) -> bool { + matches!(self.delivery_state, AuthoredDeliveryState::Satisfied) + } + /// Returns whether durable local intent remains eligible for delivery. + pub const fn is_delivery_pending(&self) -> bool { + matches!( + self.delivery_state, + AuthoredDeliveryState::Pending | AuthoredDeliveryState::Retryable + ) + } +} + +/// Passive status of the configured shared Nostr source and sink. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransportHealth { + configured: bool, + source_available: bool, + sink_available: bool, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl TransportHealth { + /// Returns whether a validated relay set is installed. + pub const fn is_configured(&self) -> bool { + self.configured + } + /// Returns whether passive source status is fully available. + pub const fn is_source_available(&self) -> bool { + self.source_available + } + /// Returns whether passive sink status is fully available. + pub const fn is_sink_available(&self) -> bool { + self.sink_available + } +} + +/// Borrowed high-level social operations over one shared SDK engine. +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +#[derive(Clone, Copy)] +pub struct SocialOperations<'a> { + client: &'a Client, +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +enum SocialDraft { + Profile(Box<radroots_event::profile::AuthoredProfile>), + Update(radroots_event::post::AuthoredUpdate), + Reply(radroots_event::post::reply::AuthoredNip10Reply), +} + +impl ClientBuilder { + /// Creates an empty builder with no hidden storage, network, signing, or + /// runtime side effects. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Creates a builder backed by deterministic in-process memory storage. + #[cfg(feature = "memory")] + #[must_use] + pub fn memory(generation: SourceGeneration) -> Self { + let storage = Arc::new(MemoryStorage::new(generation)); + let builder = Self::new().storage(storage.clone()); + #[cfg(feature = "sync")] + { + let mut builder = builder; + builder.sync_storage = Some(storage); + builder + } + #[cfg(not(feature = "sync"))] + builder + } + + /// Creates the ordinary deterministic in-process memory configuration. + /// + /// This performs no I/O and is intended for ephemeral local clients whose + /// source-generation identity does not need to survive the process. Hosts + /// that persist cursors should call [`Self::memory`] with their own + /// generation instead. + #[cfg(feature = "memory")] + #[must_use] + pub fn memory_default() -> Self { + let storage = Arc::new(MemoryStorage::default()); + let builder = Self::new().storage(storage.clone()); + #[cfg(feature = "sync")] + { + let mut builder = builder; + builder.sync_storage = Some(storage); + builder + } + #[cfg(not(feature = "sync"))] + builder + } + + /// Explicitly opens canonical SQLite storage from validated host-owned + /// configuration and returns a builder containing only the storage SPI. + #[cfg(feature = "sqlite")] + pub async fn sqlite(options: crate::storage::SqliteOptions) -> Result<Self> { + let storage = radroots_storage_sqlite::SqliteStorage::open(options) + .await + .map_err(Error::storage_open_failed)?; + let storage = Arc::new(storage); + let builder = Self::new() + .storage(storage.clone()) + .capability_availability(CapabilityId::PERSISTENT_STORAGE, Availability::Available); + #[cfg(feature = "sync")] + { + let mut builder = builder; + builder.sync_storage = Some(storage); + Ok(builder) + } + #[cfg(not(feature = "sync"))] + Ok(builder) + } + + /// Injects the canonical storage capability. + #[must_use] + pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self { + self.storage = Some(storage); + self + } + + /// Injects an optional canonical signer capability. + #[must_use] + pub fn signer(mut self, signer: Arc<dyn Signer>) -> Self { + self.signer = Some(signer); + self + } + + /// Installs a module-scoped signer provider over the canonical SPI and + /// records its presentation-independent runtime capability. + #[must_use] + pub fn signing(mut self, provider: crate::signing::Provider) -> Self { + let capability = match provider.mode() { + crate::signing::Mode::Local => Some(CapabilityId::LOCAL_SIGNING), + crate::signing::Mode::Nip46 => Some(CapabilityId::NIP46_SIGNING), + crate::signing::Mode::Host => None, + }; + #[cfg(feature = "local-signing")] + { + let (signer, slot) = provider.into_parts(); + self.signer = Some(signer); + self.signing_slot = slot; + } + #[cfg(not(feature = "local-signing"))] + { + self.signer = Some(provider.into_signer()); + } + if let Some(capability) = capability { + self.explicitly_configured_capabilities.insert(capability); + self.capability_availability + .insert(capability, Availability::Available); + } + self + } + + /// Injects an optional inbound event source. + #[must_use] + pub fn source(mut self, source: Arc<dyn EventSource>) -> Self { + self.source = Some(source); + self + } + + /// Injects an optional outbound event sink. + #[must_use] + pub fn sink(mut self, sink: Arc<dyn EventSink>) -> Self { + self.sink = Some(sink); + self + } + + /// Installs one host-reconfigurable Nostr source and sink. + #[cfg(feature = "nostr")] + #[must_use] + pub fn nostr(mut self, slot: crate::transport::NostrSlot) -> Self { + self.source = Some(Arc::new(slot.clone())); + self.sink = Some(Arc::new(slot.clone())); + self.nostr_slot = Some(slot); + self + } + + /// Injects an explicitly composed synchronization engine. + #[cfg(feature = "sync")] + #[must_use] + pub fn sync_engine(mut self, sync: radroots_sync::Engine) -> Self { + self.sync = Some(sync); + self + } + + /// Requests one SDK-composed engine using explicit native host policy. + /// + /// This is available only for SDK-created memory or SQLite storage, whose + /// complete synchronization capability is known without downcasting. + #[cfg(feature = "sync")] + #[must_use] + pub fn host_sync(mut self, policy: crate::sync::HostPolicy) -> Self { + self.host_sync = Some(policy); + self + } + + /// Marks a specialized capability as configured and records its + /// host-observed initial availability without probing resources. + /// + /// Reports ignore this observation for capabilities that are not compiled + /// or configured. Runtime IDs are independent from Cargo feature names. + #[must_use] + pub fn capability_availability(mut self, id: CapabilityId, availability: Availability) -> Self { + self.explicitly_configured_capabilities.insert(id); + self.capability_availability.insert(id, availability); + self + } + + /// Validates the selected capabilities and creates a client handle. + #[allow(unused_mut)] + pub fn build(mut self) -> Result<Client> { + let storage = self.storage.ok_or_else(Error::missing_storage)?; + if (self.signer.is_some(), self.sink.is_some()) == (true, false) { + return Err(Error::signer_without_sink()); + } + #[cfg(feature = "sync")] + if let Some(policy) = self.host_sync { + let sync_storage = self + .sync_storage + .take() + .ok_or_else(Error::shared_operation_unavailable)?; + let (clock, ids, deadlines) = policy.composition(); + let mut builder = radroots_sync::Engine::builder(sync_storage, clock, ids, deadlines); + if let Some(source) = self.source.as_ref() { + builder = builder.source(Arc::clone(source)); + } + if let Some(sink) = self.sink.as_ref() { + builder = builder.sink(Arc::clone(sink)); + } + if let Some(signer) = self.signer.as_ref() { + builder = builder.signer(Arc::clone(signer)); + } + self.sync = Some(builder.build().map_err(Error::invalid_host_configuration)?); + } + Ok(Client { + inner: Arc::new(ClientInner { + storage, + signer: self.signer, + source: self.source, + sink: self.sink, + #[cfg(feature = "sync")] + sync: self.sync, + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + signing_slot: self.signing_slot, + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + nostr_slot: self.nostr_slot, + capability_availability: self.capability_availability, + explicitly_configured_capabilities: self.explicitly_configured_capabilities, + lifecycle: AtomicU8::new(OPEN), + }), + }) + } +} + +impl Client { + /// Returns a deterministic capability report without probing resources or + /// performing filesystem, network, signing, or storage operations. + #[must_use] + pub fn capabilities(&self) -> CapabilityReport { + let lifecycle_availability = match self.inner.lifecycle.load(Ordering::Acquire) { + OPEN => Availability::Available, + CLOSING | CLOSE_RETRY_REQUIRED => Availability::Degraded, + _ => Availability::Unavailable, + }; + crate::capability::report(crate::capability::Context { + storage: true, + signer: self.inner.signer.is_some(), + source: self.inner.source.is_some(), + sink: self.inner.sink.is_some(), + sync: self.sync_is_configured(), + lifecycle_availability, + explicitly_configured: &self.inner.explicitly_configured_capabilities, + overrides: &self.inner.capability_availability, + }) + } + + /// Returns canonical backend status without exposing a backend handle. + pub async fn storage_status(&self) -> Result<crate::storage::Status> { + let storage = self.storage()?; + radroots_storage::BackupSource::status(storage) + .await + .map_err(Error::storage_inspection_failed) + } + + /// Runs canonical integrity inspection without exposing backend internals. + pub async fn storage_integrity(&self) -> Result<crate::storage::IntegrityStatus> { + let storage = self.storage()?; + radroots_storage::BackupSource::integrity(storage) + .await + .map_err(Error::storage_inspection_failed) + } + + /// Returns the injected canonical storage capability. + pub fn storage(&self) -> Result<&dyn Storage> { + self.require_open()?; + Ok(self.inner.storage.as_ref()) + } + + /// Returns backend-neutral backup, restore, status, and integrity operations. + pub fn storage_operations(&self) -> Result<crate::storage::Operations<'_>> { + Ok(crate::storage::Operations::new(self.storage()?)) + } + + /// Returns the injected signer, when outbound authoring is enabled. + pub fn signer(&self) -> Result<Option<&dyn Signer>> { + self.require_open()?; + Ok(self.inner.signer.as_deref()) + } + + /// Returns the injected inbound source, when pull is enabled. + pub fn source(&self) -> Result<Option<&dyn EventSource>> { + self.require_open()?; + Ok(self.inner.source.as_deref()) + } + + /// Returns the injected outbound sink, when delivery is enabled. + pub fn sink(&self) -> Result<Option<&dyn EventSink>> { + self.require_open()?; + Ok(self.inner.sink.as_deref()) + } + + /// Returns client-scoped canonical synchronization operations, when configured. + #[cfg(feature = "sync")] + pub fn sync(&self) -> Result<Option<crate::sync::Operations<'_>>> { + self.require_open()?; + Ok(self.inner.sync.as_ref().map(crate::sync::Operations::new)) + } + + /// Returns farm commit operations when canonical synchronization is configured. + #[cfg(feature = "sync")] + pub fn farm(&self) -> Result<Option<crate::farm::Operations<'_>>> { + Ok(self.sync()?.map(crate::farm::Operations::new)) + } + + /// Returns listing commit operations when canonical synchronization is configured. + #[cfg(feature = "sync")] + pub fn listing(&self) -> Result<Option<crate::listing::Operations<'_>>> { + Ok(self.sync()?.map(crate::listing::Operations::new)) + } + + /// Returns trade operations when canonical synchronization is configured. + #[cfg(feature = "sync")] + pub fn trade(&self) -> Result<Option<crate::trade::Operations<'_>>> { + let storage = self.storage()?; + Ok(self + .sync()? + .map(|sync| crate::trade::Operations::new(storage, sync))) + } + + /// Returns high-level shared social operations when the required explicit + /// signer, transport, and synchronization composition is present. + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + pub fn social(&self) -> Result<SocialOperations<'_>> { + self.require_open()?; + let social_composition = ( + self.inner.sync.is_some(), + self.inner.signing_slot.is_some(), + self.inner.nostr_slot.is_some(), + ); + if social_composition != (true, true, true) { + return Err(Error::shared_operation_unavailable()); + } + Ok(SocialOperations { client: self }) + } + + /// Returns whether explicit close completed successfully or reached the + /// lower storage commit point. + #[must_use] + pub fn is_closed(&self) -> bool { + self.inner.lifecycle.load(Ordering::Acquire) == CLOSED + } + + /// Explicitly closes active storage resources across every client clone. + /// + /// Dropping this future before its first poll has no effect. Cancellation + /// after close begins leaves the client unavailable and permits an + /// explicit retry; it never reports rollback. Repeated completed close is + /// idempotent. No worker, executor, or blocking `Drop` path is installed. + pub async fn close(&self) -> Result<()> { + loop { + match self.inner.lifecycle.load(Ordering::Acquire) { + CLOSED => return Ok(()), + CLOSING => return Err(Error::close_in_progress()), + state @ (OPEN | CLOSE_RETRY_REQUIRED) => { + if self + .inner + .lifecycle + .compare_exchange(state, CLOSING, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + break; + } + } + _ => return Err(Error::client_closed()), + } + } + + let attempt = CloseAttempt::new(Arc::clone(&self.inner)); + let close_result = radroots_storage::BackupSource::close(self.inner.storage.as_ref()).await; + attempt.complete(); + close_result + .map(|_| ()) + .map_err(Error::storage_close_failed) + } + + fn require_open(&self) -> Result<()> { + match self.inner.lifecycle.load(Ordering::Acquire) { + OPEN => Ok(()), + CLOSING | CLOSE_RETRY_REQUIRED => Err(Error::client_closing()), + _ => Err(Error::client_closed()), + } + } + + #[cfg(feature = "sync")] + fn sync_is_configured(&self) -> bool { + self.inner.sync.is_some() + } + + #[cfg(not(feature = "sync"))] + fn sync_is_configured(&self) -> bool { + false + } +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +impl SocialOperations<'_> { + /// Observes both transport directions without initiating relay work. + pub async fn transport_health(&self) -> Result<TransportHealth> { + use radroots_transport::capability::Availability as TransportAvailability; + let slot = self.nostr()?; + let configured = slot.targets().is_some(); + let source = radroots_transport::EventSource::status(slot) + .await + .map_err(|_| Error::shared_operation_failed_without_source())?; + let sink = radroots_transport::EventSink::status(slot) + .await + .map_err(|_| Error::shared_operation_failed_without_source())?; + Ok(TransportHealth { + configured, + source_available: source.availability() == TransportAvailability::Available, + sink_available: sink.availability() == TransportAvailability::Available, + }) + } + + /// Fetches, verifies, and durably ingests the latest profile for the active signer. + pub async fn fetch_profile_for_signer(&self) -> Result<Option<ProfileEvent>> { + let identity = self.identity()?; + let selector = radroots_transport::source::FetchSelector::all() + .with_kinds(vec![radroots_event::envelope::kind::KIND_PROFILE]) + .and_then(|selector| selector.with_authors(vec![identity.public_key()])) + .map_err(|_| Error::invalid_host_configuration_without_source())?; + let events = self.fetch(32, selector).await?; + let mut profiles = events + .into_iter() + .filter_map(|event| profile_event(&event).ok()) + .collect::<Vec<_>>(); + profiles.sort_by_key(|event| std::cmp::Reverse(event.created_at)); + Ok(profiles.into_iter().next()) + } + + /// Fetches, verifies, and durably ingests a bounded kind-1 page. + pub async fn fetch_posts( + &self, + limit: u16, + since_unix_seconds: Option<u64>, + ) -> Result<Vec<PostEvent>> { + let mut selector = radroots_transport::source::FetchSelector::all() + .with_kinds(vec![radroots_event::envelope::kind::KIND_POST]) + .map_err(|_| Error::invalid_host_configuration_without_source())?; + if let Some(since) = since_unix_seconds { + selector = selector + .with_since_unix_seconds(since) + .map_err(|_| Error::invalid_host_configuration_without_source())?; + } + let mut posts = self + .fetch(limit, selector) + .await? + .into_iter() + .map(|event| PostEvent { + event_id: event.id_hex(), + author: event.pubkey().to_hex(), + created_at: event.created_at(), + content: event.content().to_owned(), + }) + .collect::<Vec<_>>(); + posts.sort_by_key(|event| std::cmp::Reverse(event.created_at)); + Ok(posts) + } + + /// Publishes a complete media-free profile replacement. + pub async fn publish_profile(&self, draft: ProfileDraft) -> Result<PublishReceipt> { + let mut profile = radroots_event::profile::AuthoredProfile::new(draft.name) + .map_err(Error::invalid_host_configuration)?; + if let Some(value) = draft.display_name { + profile = profile.with_display_name(value); + } + if let Some(value) = draft.about { + profile = profile.with_about(value); + } + if let Some(value) = draft.nip05 { + profile = profile.with_nip05( + radroots_event::profile::Nip05Identifier::parse(value.as_str()) + .map_err(Error::invalid_host_configuration)?, + ); + } + if let Some(value) = draft.bot { + profile = profile.with_bot(value); + } + self.publish(SocialDraft::Profile(Box::new(profile))).await + } + + /// Publishes one strict root kind-1 update. + pub async fn publish_text(&self, content: impl Into<String>) -> Result<PublishReceipt> { + let update = radroots_event::post::AuthoredUpdate::new(content) + .map_err(Error::invalid_host_configuration)?; + self.publish(SocialDraft::Update(update)).await + } + + /// Publishes one strict direct NIP-10 reply. + pub async fn publish_reply( + &self, + content: impl Into<String>, + root_event_id: &str, + root_author: &str, + relay_hint: Option<&str>, + ) -> Result<PublishReceipt> { + let reference = radroots_event::post::reply::Nip10ReplyReference::parse( + root_event_id, + root_author, + relay_hint, + ) + .map_err(Error::invalid_host_configuration)?; + let reply = radroots_event::post::reply::AuthoredNip10Reply::direct(content, reference) + .map_err(Error::invalid_host_configuration)?; + self.publish(SocialDraft::Reply(reply)).await + } + + async fn fetch( + &self, + limit: u16, + selector: radroots_transport::source::FetchSelector, + ) -> Result<Vec<radroots_event::SignedEvent>> { + let slot = self.nostr()?; + let targets = slot + .targets() + .ok_or_else(Error::shared_operation_unavailable)?; + let request_id = format!("sdk-fetch-{}", uuid::Uuid::new_v4()); + let deadline = now_unix_ms()?.saturating_add(30_000); + let request = radroots_transport::FetchRequest::new( + request_id, + targets, + radroots_transport::source::FetchBounds::new(limit, deadline) + .map_err(|_| Error::invalid_host_configuration_without_source())?, + ) + .map_err(|_| Error::invalid_host_configuration_without_source())? + .with_selector(selector); + let page = radroots_transport::EventSource::fetch(slot, request) + .await + .map_err(|_| Error::shared_operation_failed_without_source())?; + let observed = page.events().to_vec(); + let receipt = self + .client + .sync()? + .ok_or_else(Error::shared_operation_unavailable)? + .ingest_batch( + observed.clone(), + &radroots_sync::ingest::RegistryPolicy::verified(), + ) + .await; + Ok(observed + .into_iter() + .zip(receipt.outcomes()) + .filter_map(|(observed, outcome)| { + outcome.as_ref().ok().map(|_| observed.event().clone()) + }) + .collect()) + } + + async fn publish(&self, authored: SocialDraft) -> Result<PublishReceipt> { + use radroots_event::contract::AuthorRole; + use radroots_event_codec::authoring::AuthoredEventPlan; + use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy}; + use radroots_storage::journal::IdempotencyKey; + use radroots_sync::{PushRequest, policy::SyncId}; + use radroots_transport::policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}; + + let identity = self.identity()?; + let targets = self + .nostr()? + .targets() + .ok_or_else(Error::shared_operation_unavailable)?; + let operation_uuid = uuid::Uuid::new_v4(); + let operation_id = + SyncId::new(*operation_uuid.as_bytes()).map_err(Error::invalid_host_configuration)?; + let now = now_unix_ms()?; + let created_at = now / 1_000; + let plan = match authored { + SocialDraft::Profile(profile) => { + AuthoredEventPlan::from_profile(&profile, created_at, identity.public_key_hex()) + } + SocialDraft::Update(update) => { + AuthoredEventPlan::from_update(&update, created_at, identity.public_key_hex()) + } + SocialDraft::Reply(reply) => { + AuthoredEventPlan::from_nip10_reply(&reply, created_at, identity.public_key_hex()) + } + } + .map_err(Error::invalid_host_configuration)?; + let actor = Actor::new( + identity.public_key(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Any], + ) + .map_err(Error::invalid_host_configuration)?; + let request = PushRequest::new( + operation_id, + IdempotencyKey::parse(format!("sdk-{operation_uuid}")) + .map_err(Error::invalid_host_configuration)?, + actor, + plan, + targets, + SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), + now.checked_add(30_000) + .ok_or_else(Error::invalid_host_configuration_without_source)?, + CancellationPolicy::PreservePublishedRequest, + ) + .map_err(Error::invalid_host_configuration)?; + let sync = self + .client + .sync()? + .ok_or_else(Error::shared_operation_unavailable)?; + let preparation = sync + .prepare_push(request.clone()) + .await + .map_err(Error::shared_operation_failed)?; + sync.sign_prepared(request) + .await + .map_err(Error::shared_operation_failed)?; + sync.admit_signed(operation_id) + .await + .map_err(Error::shared_operation_failed)?; + let status = sync + .push_status(operation_id) + .await + .map_err(Error::shared_operation_failed)? + .ok_or_else(Error::shared_operation_failed_without_source)?; + let event_id = status + .artifact() + .signed() + .ok_or_else(Error::shared_operation_failed_without_source)? + .event() + .id_hex(); + let delivery = sync + .deliver_push(operation_id) + .await + .map_err(Error::shared_operation_failed)?; + Ok(PublishReceipt { + event_id, + replay: preparation.is_replay(), + delivery_state: delivery.plan().state(), + }) + } + + fn identity(&self) -> Result<crate::signing::LocalIdentity> { + self.client + .inner + .signing_slot + .as_ref() + .and_then(crate::signing::Slot::identity) + .ok_or_else(Error::shared_operation_unavailable) + } + + fn nostr(&self) -> Result<&crate::transport::NostrSlot> { + self.client + .inner + .nostr_slot + .as_ref() + .ok_or_else(Error::shared_operation_unavailable) + } +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +fn now_unix_ms() -> Result<u64> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|duration| u64::try_from(duration.as_millis()).ok()) + .filter(|value| *value != 0) + .ok_or_else(Error::shared_operation_unavailable) +} + +#[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] +fn profile_event( + event: &radroots_event::SignedEvent, +) -> std::result::Result< + ProfileEvent, + radroots_event_codec::decode::profile::RadrootsProfileMetadataParseError, +> { + let profile = + radroots_event_codec::decode::profile::parse_inbound_profile_metadata(event.content())?; + Ok(ProfileEvent { + event_id: event.id_hex(), + author: event.pubkey().to_hex(), + created_at: event.created_at(), + name: profile.name().map(str::to_owned), + display_name: profile.display_name().map(str::to_owned), + about: profile.about().map(str::to_owned), + picture: profile.picture().map(|value| value.as_str().to_owned()), + banner: profile.banner().map(|value| value.as_str().to_owned()), + nip05: profile.nip05().map(|value| value.as_str().to_owned()), + bot: profile.bot(), + }) +} + +struct CloseAttempt { + inner: Arc<ClientInner>, + completed: bool, +} + +impl CloseAttempt { + fn new(inner: Arc<ClientInner>) -> Self { + Self { + inner, + completed: false, + } + } + + fn complete(mut self) { + self.inner.lifecycle.store(CLOSED, Ordering::Release); + self.completed = true; + } +} + +impl Drop for CloseAttempt { + fn drop(&mut self) { + if !self.completed { + self.inner + .lifecycle + .store(CLOSE_RETRY_REQUIRED, Ordering::Release); + } + } +} + +impl std::fmt::Debug for Client { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Client") + .field("signer", &self.inner.signer.is_some()) + .field("source", &self.inner.source.is_some()) + .field("sink", &self.inner.sink.is_some()) + .field("closed", &self.is_closed()) + .finish_non_exhaustive() + } +} + +impl std::fmt::Debug for ClientBuilder { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("ClientBuilder") + .field("storage", &self.storage.is_some()) + .field("signer", &self.signer.is_some()) + .field("source", &self.source.is_some()) + .field("sink", &self.sink.is_some()) + .finish_non_exhaustive() + } +} + +#[cfg(all(test, feature = "memory"))] +mod tests { + use super::*; + use radroots_signing::{ + Error as SigningError, SignReceipt, SignRequest, SignerStatus, error::Kind, + signer::BoxFuture as SigningFuture, + }; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error as TransportError, FetchPage, FetchRequest, + SinkFailure, SinkStatus, SourceStatus, outcome::Retryability, + source::BoxFuture as TransportFuture, + }; + use std::{ + future::Future, + sync::Arc, + task::{Context, Poll, Wake, Waker}, + }; + + struct TestSource; + struct TestSink; + struct TestSigner; + + impl EventSource for TestSource { + fn status(&self) -> TransportFuture<'_, std::result::Result<SourceStatus, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + + fn fetch( + &self, + _request: FetchRequest, + ) -> TransportFuture<'_, std::result::Result<FetchPage, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + } + + impl EventSink for TestSink { + fn status(&self) -> TransportFuture<'_, std::result::Result<SinkStatus, TransportError>> { + Box::pin(async { Err(TransportError::UnsupportedOperation) }) + } + + fn deliver( + &self, + request: DeliveryRequest, + ) -> TransportFuture<'_, std::result::Result<DeliveryReceipt, SinkFailure>> { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + impl Signer for TestSigner { + fn status(&self) -> SigningFuture<'_, std::result::Result<SignerStatus, SigningError>> { + Box::pin(async { Err(SigningError::new(Kind::InternalError)) }) + } + + fn sign( + &self, + _request: SignRequest, + ) -> SigningFuture<'_, std::result::Result<SignReceipt, SigningError>> { + Box::pin(async { Err(SigningError::new(Kind::InternalError)) }) + } + } + + fn generation() -> SourceGeneration { + SourceGeneration::new([1; 32]).expect("non-zero generation") + } + + #[test] + fn missing_storage_and_signer_without_sink_fail_closed() { + assert!(matches!( + ClientBuilder::new().build(), + Err(error) if error.kind() == crate::error::ErrorKind::MissingStorage + )); + assert!(matches!( + ClientBuilder::memory(generation()) + .signer(Arc::new(TestSigner)) + .build(), + Err(error) if error.kind() == crate::error::ErrorKind::SignerWithoutSink + )); + } + + #[test] + fn memory_local_source_only_and_sink_only_compositions_are_explicit() { + let local = ClientBuilder::memory(generation()).build().expect("local"); + assert!(local.source().expect("source capability").is_none()); + assert!(local.sink().expect("sink capability").is_none()); + assert!(local.signer().expect("signer capability").is_none()); + + let source = ClientBuilder::memory(generation()) + .source(Arc::new(TestSource)) + .build() + .expect("source-only"); + assert!(source.source().expect("source capability").is_some()); + assert!(source.sink().expect("sink capability").is_none()); + + let sink = ClientBuilder::memory(generation()) + .sink(Arc::new(TestSink)) + .build() + .expect("sink-only"); + assert!(sink.source().expect("source capability").is_none()); + assert!(sink.sink().expect("sink capability").is_some()); + } + + #[test] + fn signer_with_sink_is_valid_and_diagnostics_are_capability_only() { + let client = ClientBuilder::memory(generation()) + .sink(Arc::new(TestSink)) + .signer(Arc::new(TestSigner)) + .build() + .expect("outbound client"); + assert!(client.signer().expect("signer capability").is_some()); + assert_eq!( + format!("{client:?}"), + "Client { signer: true, source: false, sink: true, closed: false, .. }" + ); + } + + #[cfg(all( + feature = "sync", + feature = "nostr", + feature = "local-signing", + feature = "nip46" + ))] + #[test] + fn curated_models_accessors_and_builder_modes_are_complete() { + let profile_draft = ProfileDraft::new("farm") + .with_display_name("Farm") + .with_about("Local food") + .with_nip05("farm@example.test") + .with_bot(false); + assert_eq!(profile_draft.name, "farm"); + assert_eq!(profile_draft.display_name.as_deref(), Some("Farm")); + assert_eq!(profile_draft.about.as_deref(), Some("Local food")); + assert_eq!(profile_draft.nip05.as_deref(), Some("farm@example.test")); + assert_eq!(profile_draft.bot, Some(false)); + + let profile = ProfileEvent { + event_id: "event".to_owned(), + author: "author".to_owned(), + created_at: 7, + name: Some("farm".to_owned()), + display_name: Some("Farm".to_owned()), + about: Some("Local food".to_owned()), + picture: Some("https://example.test/picture".to_owned()), + banner: Some("https://example.test/banner".to_owned()), + nip05: Some("farm@example.test".to_owned()), + bot: Some(false), + }; + assert_eq!(profile.event_id(), "event"); + assert_eq!(profile.author(), "author"); + assert_eq!(profile.created_at(), 7); + assert_eq!(profile.name(), Some("farm")); + assert_eq!(profile.display_name(), Some("Farm")); + assert_eq!(profile.about(), Some("Local food")); + assert_eq!(profile.picture(), Some("https://example.test/picture")); + assert_eq!(profile.banner(), Some("https://example.test/banner")); + assert_eq!(profile.nip05(), Some("farm@example.test")); + assert_eq!(profile.bot(), Some(false)); + + let post = PostEvent { + event_id: "post".to_owned(), + author: "author".to_owned(), + created_at: 8, + content: "content".to_owned(), + }; + assert_eq!(post.event_id(), "post"); + assert_eq!(post.author(), "author"); + assert_eq!(post.created_at(), 8); + assert_eq!(post.content(), "content"); + + for (delivery_state, delivered, pending) in [ + (AuthoredDeliveryState::Pending, false, true), + (AuthoredDeliveryState::Retryable, false, true), + (AuthoredDeliveryState::Satisfied, true, false), + (AuthoredDeliveryState::Exhausted, false, false), + (AuthoredDeliveryState::FailedTerminal, false, false), + (AuthoredDeliveryState::Cancelled, false, false), + ] { + let receipt = PublishReceipt { + event_id: "published".to_owned(), + replay: true, + delivery_state, + }; + assert_eq!(receipt.event_id(), "published"); + assert!(receipt.is_replay()); + assert_eq!(receipt.delivery_state(), delivery_state); + assert_eq!(receipt.is_delivered(), delivered); + assert_eq!(receipt.is_delivery_pending(), pending); + } + + let health = TransportHealth { + configured: true, + source_available: true, + sink_available: false, + }; + assert!(health.is_configured()); + assert!(health.is_source_available()); + assert!(!health.is_sink_available()); + + let builder = ClientBuilder::memory_default() + .source(Arc::new(TestSource)) + .host_sync(crate::sync::HostPolicy::default()); + assert!(format!("{builder:?}").contains("storage: true")); + let client = builder.build().expect("sync client"); + assert!(client.sync().expect("sync").is_some()); + assert!(client.farm().expect("farm").is_some()); + assert!(client.listing().expect("listing").is_some()); + assert!(client.trade().expect("trade").is_some()); + assert!(client.social().is_err()); + assert!( + format!( + "{:?}", + client.storage_operations().expect("storage operations") + ) + .contains("borrowed canonical storage") + ); + assert!( + format!("{:?}", client.sync().expect("sync").expect("operations")) + .contains("borrowed canonical engine") + ); + + let host = ClientBuilder::memory_default() + .sink(Arc::new(TestSink)) + .signing(crate::signing::Provider::host(Arc::new(TestSigner))) + .build() + .expect("host signer"); + assert!( + !host + .capabilities() + .get(CapabilityId::NIP46_SIGNING) + .expect("nip46") + .is_configured() + ); + + let nip46 = ClientBuilder::memory_default() + .sink(Arc::new(TestSink)) + .signing(crate::signing::Provider::nip46(Arc::new(TestSigner))) + .build() + .expect("nip46 signer"); + assert!( + nip46 + .capabilities() + .get(CapabilityId::NIP46_SIGNING) + .expect("nip46") + .is_configured() + ); + } + + #[cfg(feature = "local-signing")] + #[test] + fn module_scoped_signing_provider_configures_the_matching_capability() { + let signer = radroots_nostr::signing::LocalSigner::generate().expect("local signer"); + let client = ClientBuilder::memory(generation()) + .sink(Arc::new(TestSink)) + .signing(crate::signing::Provider::local(signer)) + .build() + .expect("client"); + let status = client + .capabilities() + .get(CapabilityId::LOCAL_SIGNING) + .expect("local signing"); + assert!(status.is_compiled()); + assert!(status.is_configured()); + assert_eq!(status.availability(), Availability::Available); + } + + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + #[tokio::test] + async fn shared_mobile_composition_is_single_client_explicit_and_fail_closed() { + let signing = crate::signing::Slot::new(); + let nostr = crate::transport::NostrSlot::new(crate::transport::RelayUrlPolicy::Local); + let client = ClientBuilder::memory(generation()) + .signing(crate::signing::Provider::slot(signing.clone())) + .nostr(nostr.clone()) + .host_sync(crate::sync::HostPolicy::standard()) + .build() + .expect("shared client"); + + let health = client + .social() + .expect("social composition") + .transport_health() + .await + .expect("passive health"); + assert!(!health.is_configured()); + assert!(!health.is_source_available()); + assert!(!health.is_sink_available()); + assert!(matches!( + client + .social() + .expect("social composition") + .fetch_posts(1, None) + .await, + Err(error) if error.kind() == crate::error::ErrorKind::SharedOperationUnavailable + )); + assert!( + client + .social() + .expect("social composition") + .fetch_profile_for_signer() + .await + .is_err() + ); + + let (_secret, identity) = signing.generate().expect("host key handoff"); + assert_eq!(signing.identity(), Some(identity)); + let social = client.social().expect("social composition"); + assert!(social.fetch_profile_for_signer().await.is_err()); + assert!(social.fetch_posts(2, Some(1)).await.is_err()); + assert!( + social + .publish_profile( + ProfileDraft::new("farm") + .with_display_name("Farm") + .with_about("Local food") + .with_nip05("farm@example.test") + .with_bot(false), + ) + .await + .is_err() + ); + assert!( + social + .publish_profile(ProfileDraft::new("farm")) + .await + .is_err() + ); + assert!(social.publish_text("local update").await.is_err()); + assert!( + social + .publish_reply( + "local reply", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Some("ws://127.0.0.1:7447"), + ) + .await + .is_err() + ); + assert!( + social + .publish_reply("local reply", "invalid", "invalid", None) + .await + .is_err() + ); + nostr + .configure(["ws://127.0.0.1:7447"]) + .expect("relay selection"); + assert!(nostr.targets().is_some()); + let social = client.social().expect("social composition"); + assert!( + social + .transport_health() + .await + .expect("configured passive health") + .is_configured() + ); + let fetch = tokio::time::timeout( + core::time::Duration::from_secs(3), + social.fetch_posts(2, Some(1)), + ) + .await + .expect("localhost fetch remains bounded"); + assert!(fetch.is_err() || fetch.is_ok_and(|events| events.is_empty())); + let profile_fetch = tokio::time::timeout( + core::time::Duration::from_secs(3), + social.fetch_profile_for_signer(), + ) + .await + .expect("localhost profile fetch remains bounded"); + assert!(profile_fetch.is_err() || profile_fetch.is_ok_and(|event| event.is_none())); + let publish = tokio::time::timeout( + core::time::Duration::from_secs(3), + social.publish_text("configured local update"), + ) + .await + .expect("localhost publish remains bounded"); + match publish { + Ok(receipt) => { + assert_eq!(receipt.event_id().len(), 64); + assert!(!receipt.is_replay()); + } + Err(error) => assert_eq!(error.kind(), crate::error::ErrorKind::SharedOperationFailed), + } + let profile_publish = tokio::time::timeout( + core::time::Duration::from_secs(3), + social.publish_profile( + ProfileDraft::new("configured-farm") + .with_display_name("Configured Farm") + .with_about("Local food"), + ), + ) + .await + .expect("localhost profile publish remains bounded"); + assert!( + profile_publish.is_ok() + || matches!( + profile_publish, + Err(error) + if error.kind() == crate::error::ErrorKind::SharedOperationFailed + ) + ); + let reply_publish = tokio::time::timeout( + core::time::Duration::from_secs(3), + social.publish_reply( + "configured reply", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + None, + ), + ) + .await + .expect("localhost reply publish remains bounded"); + assert!( + reply_publish.is_ok() + || matches!( + reply_publish, + Err(error) + if error.kind() == crate::error::ErrorKind::SharedOperationFailed + ) + ); + nostr.clear(); + assert!(nostr.targets().is_none()); + signing.clear(); + assert!(signing.identity().is_none()); + assert_eq!( + radroots_signing::Signer::status(&signing) + .await + .expect("empty slot status") + .availability(), + radroots_signing::status::SignerAvailability::Unavailable + ); + } + + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + #[test] + fn verified_profile_projection_preserves_the_curated_public_fields() { + use radroots_event::wire::v1::Nip01EventWire; + + let author = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + let content = r#"{"name":"farm","display_name":"Farm","about":"Local food","nip05":"farm@example.test","bot":false}"#; + let id = radroots_event::draft::compute_nip01_event_id(author, 7, 0, &[], content) + .expect("canonical profile id") + .to_hex(); + let raw = serde_json::json!({ + "id": id, + "pubkey": author, + "created_at": 7, + "kind": 0, + "tags": [], + "content": content, + "sig": "d".repeat(128), + }) + .to_string(); + let wire = Nip01EventWire::parse_json(&raw).expect("profile wire"); + let event = radroots_event::SignedEvent::from_wire_verified_id(wire, raw) + .expect("verified profile event"); + let profile = profile_event(&event).expect("profile projection"); + assert_eq!(profile.event_id(), id); + assert_eq!(profile.author(), author); + assert_eq!(profile.created_at(), 7); + assert_eq!(profile.name(), Some("farm")); + assert_eq!(profile.display_name(), Some("Farm")); + assert_eq!(profile.about(), Some("Local food")); + assert_eq!(profile.nip05(), Some("farm@example.test")); + assert_eq!(profile.bot(), Some(false)); + assert_eq!(profile.picture(), None); + assert_eq!(profile.banner(), None); + } + + #[test] + fn close_is_clone_shared_idempotent_and_rejects_later_capability_access() { + let client = ClientBuilder::memory(generation()).build().expect("client"); + let clone = client.clone(); + assert!(!client.is_closed()); + block_on(client.close()).expect("first close"); + assert!(clone.is_closed()); + block_on(clone.close()).expect("repeated close"); + assert!(matches!( + clone.storage(), + Err(error) if error.kind() == crate::error::ErrorKind::ClientClosed + )); + assert!(matches!( + client.source(), + Err(error) if error.kind() == crate::error::ErrorKind::ClientClosed + )); + } + + #[test] + fn close_cancellation_boundaries_are_explicit_and_retryable() { + let client = ClientBuilder::memory(generation()).build().expect("client"); + let unpolled = client.close(); + drop(unpolled); + assert!(client.storage().is_ok()); + + client.inner.lifecycle.store(CLOSING, Ordering::Release); + let attempt = CloseAttempt::new(Arc::clone(&client.inner)); + drop(attempt); + assert!(matches!( + client.storage(), + Err(error) if error.kind() == crate::error::ErrorKind::ClientClosing + )); + block_on(client.close()).expect("retry close"); + assert!(client.is_closed()); + } + + #[test] + fn concurrent_clones_converge_on_one_closed_state() { + let client = ClientBuilder::memory(generation()).build().expect("client"); + let first = client.clone(); + let second = client.clone(); + let outcomes = std::thread::scope(|scope| { + let first_close = scope.spawn(move || block_on(first.close())); + let second_close = scope.spawn(move || block_on(second.close())); + [ + first_close.join().expect("first thread"), + second_close.join().expect("second thread"), + ] + }); + assert!(outcomes.iter().all(|outcome| { + outcome.is_ok() + || matches!( + outcome, + Err(error) + if error.kind() == crate::error::ErrorKind::CloseInProgress + ) + })); + if !client.is_closed() { + block_on(client.close()).expect("finish close"); + } + assert!(client.is_closed()); + } + + #[test] + fn capability_reports_separate_configuration_degradation_and_lifecycle() { + let local = ClientBuilder::memory(generation()) + .capability_availability(CapabilityId::CANONICAL_STORAGE, Availability::Degraded) + .build() + .expect("client"); + let report = local.capabilities(); + let storage = report + .get(CapabilityId::CANONICAL_STORAGE) + .expect("storage"); + assert!(storage.is_compiled()); + assert!(storage.is_configured()); + assert_eq!(storage.availability(), Availability::Degraded); + + let signing = report.get(CapabilityId::LOCAL_SIGNING).expect("signing"); + assert!(!signing.is_configured()); + assert!(matches!( + signing.availability(), + Availability::Unavailable | Availability::Unsupported + )); + + block_on(local.close()).expect("close"); + assert_eq!( + local + .capabilities() + .get(CapabilityId::BACKUP_RESTORE) + .expect("backup") + .availability(), + Availability::Unavailable + ); + } + + #[test] + fn memory_storage_status_integrity_and_lifecycle_use_native_contracts() { + use radroots_storage::status::{ + IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy, + }; + + let client = ClientBuilder::memory(generation()).build().expect("client"); + let status = block_on(client.storage_status()).expect("status"); + assert_eq!(status.backend(), StorageBackend::Memory); + assert_eq!(status.open_mode(), StorageOpenMode::Create); + assert_eq!(status.writer_policy(), WriterPolicy::NoWriter); + assert_eq!(status.shutdown(), ShutdownState::Open); + assert_eq!( + block_on(client.storage_integrity()) + .expect("integrity") + .health(), + IntegrityHealth::Healthy + ); + block_on(client.close()).expect("close"); + assert_eq!( + block_on(client.storage_status()) + .expect_err("closed") + .kind(), + crate::error::ErrorKind::ClientClosed + ); + } + + #[cfg(feature = "sqlite")] + #[tokio::test] + async fn sqlite_builder_exposes_native_status_integrity_and_lifecycle() { + use radroots_storage::status::{ + IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy, + }; + + let directory = tempfile::tempdir().expect("temporary directory"); + let paths = crate::storage::SqlitePaths::from_directory(directory.path()).expect("paths"); + let options = + crate::storage::SqliteOptions::new(paths, crate::storage::SqliteOpenMode::Create) + .with_source_generation(generation(), 1) + .expect("source generation"); + let client = ClientBuilder::sqlite(options) + .await + .expect("open builder") + .build() + .expect("client"); + let status = client.storage_status().await.expect("status"); + assert_eq!(status.backend(), StorageBackend::Sqlite); + assert_eq!(status.open_mode(), StorageOpenMode::Create); + assert_eq!(status.writer_policy(), WriterPolicy::AdvisoryProcessLock); + assert_eq!(status.shutdown(), ShutdownState::Open); + assert!(status.wal_enabled()); + assert_ne!(status.busy_timeout_ms(), 0); + assert_eq!( + client + .storage_integrity() + .await + .expect("integrity") + .health(), + IntegrityHealth::Unknown + ); + client.close().await.expect("close"); + assert!(client.is_closed()); + } + + struct ThreadWaker; + + impl Wake for ThreadWaker { + fn wake(self: Arc<Self>) { + std::thread::current().unpark(); + } + } + + fn block_on<F: Future>(future: F) -> F::Output { + let waker = Waker::from(Arc::new(ThreadWaker)); + let mut context = Context::from_waker(&waker); + let mut future = Box::pin(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => std::thread::park(), + } + } + } +} diff --git a/crates/sdk/src/diagnostics.rs b/crates/sdk/src/diagnostics.rs @@ -0,0 +1,59 @@ +//! Redacted client diagnostics composed from canonical capability and storage status. + +/// One passive diagnostics snapshot containing only lower-owned status types. +#[derive(Clone, Debug)] +pub struct Report { + capabilities: crate::capability::CapabilityReport, + storage: radroots_storage::StorageStatus, +} + +impl Report { + /// Returns the side-effect-free SDK capability report. + #[must_use] + pub const fn capabilities(&self) -> &crate::capability::CapabilityReport { + &self.capabilities + } + + /// Returns the canonical backend status without implementation details. + #[must_use] + pub const fn storage(&self) -> radroots_storage::StorageStatus { + self.storage + } +} + +/// Captures passive, secret-safe diagnostics without filesystem paths, SQL, +/// connection handles, private artifacts, or recovery side effects. +pub async fn inspect(client: &crate::Client) -> crate::Result<Report> { + Ok(Report { + capabilities: client.capabilities(), + storage: client.storage_status().await?, + }) +} + +#[cfg(all(test, feature = "memory"))] +mod tests { + use std::sync::Arc; + + use radroots_storage::{ + event::SourceGeneration, memory::MemoryStorage, status::StorageBackend, + }; + + use crate::ClientBuilder; + + #[tokio::test] + async fn report_is_passive_native_and_redacted() { + let client = ClientBuilder::new() + .storage(Arc::new(MemoryStorage::new( + SourceGeneration::new([9; 32]).expect("generation"), + ))) + .build() + .expect("client"); + let report = super::inspect(&client).await.expect("report"); + + assert_eq!(report.storage().backend(), StorageBackend::Memory); + assert!(report.capabilities().iter().all(|status| { + !status.id().as_str().contains('/') && !status.id().as_str().contains('\\') + })); + assert!(!format!("{report:?}").contains("sqlite")); + } +} diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -0,0 +1,465 @@ +//! SDK-owned native errors and secret-safe protocol conversion. + +use std::{error, fmt}; + +use radroots_protocol::{ + error::v1::{ + CapabilityId as ProtocolCapabilityId, Class, ErrorReport, KnownCode, RecoveryAction, + SafeDetails, SafeMessage, + }, + runtime::v1::OperationId, +}; + +use crate::capability::CapabilityId; + +macro_rules! error_catalog { + ($( + $kind:ident => { + code: $code:ident, + operation: $operation:expr, + capability: $capability:expr, + message: $message:literal, + safe_detail_keys: [$($detail_key:literal),* $(,)?] + } + ),+ $(,)?) => { + /// Stable native SDK error category. + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] + #[non_exhaustive] + pub enum ErrorKind { + $($kind,)+ + } + + impl ErrorKind { + /// Every SDK error category in catalog order. + pub const ALL: &'static [Self] = &[$(Self::$kind),+]; + + /// Returns metadata generated from the single SDK authority. + #[must_use] + pub const fn descriptor(self) -> ErrorDescriptor { + match self { + $(Self::$kind => ErrorDescriptor { + kind: Self::$kind, + code: KnownCode::$code, + operation: $operation, + capability: $capability, + message: $message, + safe_detail_keys: &[$($detail_key),*], + },)+ + } + } + } + + /// Complete SDK-native error metadata catalog. + pub const CATALOG: &[ErrorDescriptor] = &[ + $(ErrorDescriptor { + kind: ErrorKind::$kind, + code: KnownCode::$code, + operation: $operation, + capability: $capability, + message: $message, + safe_detail_keys: &[$($detail_key),*], + },)+ + ]; + }; +} + +error_catalog! { + MissingStorage => { + code: MissingStorage, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK storage capability is not configured", + safe_detail_keys: [] + }, + SignerWithoutSink => { + code: SignerWithoutSink, + operation: None, + capability: None, + message: "SDK signer requires an outbound event sink", + safe_detail_keys: [] + }, + CloseInProgress => { + code: ClientCloseInProgress, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK client close is in progress", + safe_detail_keys: [] + }, + ClientClosing => { + code: ClientClosing, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK client close requires completion or retry", + safe_detail_keys: [] + }, + ClientClosed => { + code: ClientClosed, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK client is closed", + safe_detail_keys: [] + }, + StorageCloseFailed => { + code: StorageCloseFailed, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK storage close failed", + safe_detail_keys: [] + }, + StorageOpenFailed => { + code: InternalError, + operation: None, + capability: Some(CapabilityId::PERSISTENT_STORAGE), + message: "SDK persistent storage open failed", + safe_detail_keys: [] + }, + StorageInspectionFailed => { + code: StorageIntegrityFailed, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK storage inspection failed", + safe_detail_keys: [] + }, + InvalidHostConfiguration => { + code: InvalidArgument, + operation: None, + capability: None, + message: "SDK host configuration is invalid", + safe_detail_keys: [] + }, + SharedOperationUnavailable => { + code: TransportOperationUnavailable, + operation: None, + capability: None, + message: "SDK shared network operation is unavailable", + safe_detail_keys: [] + }, + SharedOperationFailed => { + code: SyncPartial, + operation: None, + capability: None, + message: "SDK shared network operation failed", + safe_detail_keys: [] + }, +} + +/// Stable metadata for one native SDK failure. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ErrorDescriptor { + kind: ErrorKind, + code: KnownCode, + operation: Option<OperationId>, + capability: Option<CapabilityId>, + message: &'static str, + safe_detail_keys: &'static [&'static str], +} + +impl ErrorDescriptor { + /// Returns the native category. + #[must_use] + pub const fn kind(self) -> ErrorKind { + self.kind + } + + /// Returns the stable protocol code. + #[must_use] + pub const fn code(self) -> KnownCode { + self.code + } + + /// Returns the class from the generated protocol authority. + #[must_use] + pub const fn class(self) -> Class { + self.code.descriptor().class + } + + /// Returns retryability from the generated protocol authority. + #[must_use] + pub const fn retryable(self) -> bool { + self.code.descriptor().retryable + } + + /// Returns recovery actions from the generated protocol authority. + #[must_use] + pub const fn recovery_actions(self) -> &'static [RecoveryAction] { + self.code.descriptor().recovery_actions + } + + /// Returns the related operation when the protocol catalog defines one. + #[must_use] + pub const fn operation(self) -> Option<OperationId> { + self.operation + } + + /// Returns the related runtime capability. + #[must_use] + pub const fn capability(self) -> Option<CapabilityId> { + self.capability + } + + /// Returns the secret-safe native display message. + #[must_use] + pub const fn message(self) -> &'static str { + self.message + } + + /// Returns the only structured detail keys permitted for this category. + #[must_use] + pub const fn safe_detail_keys(self) -> &'static [&'static str] { + self.safe_detail_keys + } +} + +/// Native SDK failure retaining an optional private source chain. +pub struct Error { + kind: ErrorKind, + source: Option<Box<dyn error::Error + Send + Sync>>, +} + +impl Error { + pub(crate) fn missing_storage() -> Self { + Self::without_source(ErrorKind::MissingStorage) + } + + pub(crate) fn signer_without_sink() -> Self { + Self::without_source(ErrorKind::SignerWithoutSink) + } + + pub(crate) fn close_in_progress() -> Self { + Self::without_source(ErrorKind::CloseInProgress) + } + + pub(crate) fn client_closing() -> Self { + Self::without_source(ErrorKind::ClientClosing) + } + + pub(crate) fn client_closed() -> Self { + Self::without_source(ErrorKind::ClientClosed) + } + + pub(crate) fn storage_close_failed(source: radroots_storage::Error) -> Self { + Self { + kind: ErrorKind::StorageCloseFailed, + source: Some(Box::new(source)), + } + } + + #[cfg(feature = "sqlite")] + pub(crate) fn storage_open_failed(source: radroots_storage_sqlite::Error) -> Self { + Self { + kind: ErrorKind::StorageOpenFailed, + source: Some(Box::new(source)), + } + } + + pub(crate) fn storage_inspection_failed(source: radroots_storage::Error) -> Self { + Self { + kind: ErrorKind::StorageInspectionFailed, + source: Some(Box::new(source)), + } + } + + #[cfg(any(feature = "sync", feature = "nostr"))] + pub(crate) fn invalid_host_configuration( + source: impl error::Error + Send + Sync + 'static, + ) -> Self { + Self { + kind: ErrorKind::InvalidHostConfiguration, + source: Some(Box::new(source)), + } + } + + #[cfg(feature = "nostr")] + pub(crate) fn invalid_host_configuration_without_source() -> Self { + Self::without_source(ErrorKind::InvalidHostConfiguration) + } + + #[cfg(any(feature = "sync", feature = "nostr"))] + pub(crate) fn shared_operation_unavailable() -> Self { + Self::without_source(ErrorKind::SharedOperationUnavailable) + } + + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + pub(crate) fn shared_operation_failed( + source: impl error::Error + Send + Sync + 'static, + ) -> Self { + Self { + kind: ErrorKind::SharedOperationFailed, + source: Some(Box::new(source)), + } + } + + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + pub(crate) fn shared_operation_failed_without_source() -> Self { + Self::without_source(ErrorKind::SharedOperationFailed) + } + + fn without_source(kind: ErrorKind) -> Self { + Self { kind, source: None } + } + + /// Returns the stable native category. + #[must_use] + pub const fn kind(&self) -> ErrorKind { + self.kind + } + + /// Returns metadata from the single SDK catalog. + #[must_use] + pub const fn descriptor(&self) -> ErrorDescriptor { + self.kind.descriptor() + } + + /// Converts to the V1 secret-safe protocol boundary. + /// + /// Native source messages are deliberately excluded. Catalog validation + /// tests guarantee that static messages and capability IDs are valid; this + /// conversion still fails closed to redacted text or no capability if a + /// future catalog edit violates those invariants. + #[must_use] + pub fn to_report(&self) -> ErrorReport { + let descriptor = self.descriptor(); + let capability = descriptor + .capability() + .and_then(|id| ProtocolCapabilityId::parse(id.as_str().to_owned()).ok()); + let message = SafeMessage::parse(descriptor.message().to_owned()) + .unwrap_or_else(|_| SafeMessage::redacted()); + ErrorReport::known( + descriptor.code(), + descriptor.operation(), + capability, + message, + SafeDetails::default(), + ) + } +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.descriptor().message()) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Error") + .field("kind", &self.kind) + .field("code", &self.descriptor().code()) + .finish_non_exhaustive() + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn error::Error + 'static)) + } +} + +/// SDK result alias. +pub type Result<T> = std::result::Result<T, Error>; + +#[cfg(test)] +mod tests { + use std::{collections::BTreeSet, error::Error as _}; + + use super::*; + + #[test] + fn catalog_is_exhaustive_unique_and_protocol_valid() { + assert_eq!(CATALOG.len(), ErrorKind::ALL.len()); + let mut kinds = BTreeSet::new(); + let mut codes = BTreeSet::new(); + for (index, descriptor) in CATALOG.iter().copied().enumerate() { + assert!(kinds.insert(descriptor.kind())); + assert!(codes.insert(descriptor.code().as_str())); + assert_eq!(descriptor.kind(), ErrorKind::ALL[index]); + assert_eq!(descriptor, descriptor.kind().descriptor()); + assert!(!descriptor.recovery_actions().is_empty()); + assert!(SafeMessage::parse(descriptor.message()).is_ok()); + if let Some(capability) = descriptor.capability() { + assert!(ProtocolCapabilityId::parse(capability.as_str()).is_ok()); + } + assert!(descriptor.safe_detail_keys().is_empty()); + Error::without_source(descriptor.kind()) + .to_report() + .validate() + .expect("protocol report"); + } + } + + #[test] + fn native_source_is_preserved_but_protocol_report_is_redacted_from_it() { + let error = Error::storage_close_failed(radroots_storage::Error::BackendUnavailable); + assert!(error.source().is_some()); + assert_eq!(error.kind(), ErrorKind::StorageCloseFailed); + assert_eq!(error.to_string(), "SDK storage close failed"); + let report = error.to_report(); + assert_eq!(report.code().as_str(), "storage_close_failed"); + assert_eq!(report.message().as_str(), "SDK storage close failed"); + assert!(!report.message().as_str().contains("backend")); + assert!(format!("{error:?}").contains("StorageCloseFailed")); + assert!(!format!("{error:?}").contains("BackendUnavailable")); + } + + #[test] + fn native_constructor_and_descriptor_surface_is_complete() { + let source_free = [ + Error::missing_storage(), + Error::signer_without_sink(), + Error::close_in_progress(), + Error::client_closing(), + Error::client_closed(), + ]; + for error in source_free { + assert!(error.source().is_none()); + let descriptor = error.descriptor(); + assert_eq!(descriptor.kind(), error.kind()); + assert_eq!(descriptor.class(), descriptor.code().descriptor().class); + assert_eq!( + descriptor.retryable(), + descriptor.code().descriptor().retryable + ); + assert_eq!(descriptor.operation(), None); + assert_eq!(descriptor.message(), error.to_string()); + } + + let inspection = + Error::storage_inspection_failed(radroots_storage::Error::BackendUnavailable); + assert_eq!(inspection.kind(), ErrorKind::StorageInspectionFailed); + assert!(inspection.source().is_some()); + + #[cfg(any(feature = "sync", feature = "nostr"))] + { + let host = Error::invalid_host_configuration(std::io::Error::other("private")); + assert_eq!(host.kind(), ErrorKind::InvalidHostConfiguration); + assert!(host.source().is_some()); + assert!(!host.to_string().contains("private")); + assert_eq!( + Error::shared_operation_unavailable().kind(), + ErrorKind::SharedOperationUnavailable + ); + } + + #[cfg(feature = "nostr")] + assert!( + Error::invalid_host_configuration_without_source() + .source() + .is_none() + ); + + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + { + let failed = Error::shared_operation_failed(std::io::Error::other("private")); + assert_eq!(failed.kind(), ErrorKind::SharedOperationFailed); + assert!(failed.source().is_some()); + assert!( + Error::shared_operation_failed_without_source() + .source() + .is_none() + ); + } + } +} diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs @@ -0,0 +1,549 @@ +//! Side-effect-free farm planning and canonical durable enqueue operations. + +use std::{error, fmt}; + +use radroots_event::{ + GenericEventDraft, + contract::AuthorRole, + envelope::kind::KIND_FARM, + farm::Farm, + id::{AddressableCoordinate, ParseError}, +}; +use radroots_event_codec::{ + authoring::AuthoredEventPlan, encode::EventEncodeError, encode::farm::to_wire_parts, +}; +use radroots_signing::Actor; + +const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1"; + +/// Pure inputs for one frozen farm profile plan. +#[derive(Clone, Debug)] +pub struct PrepareRequest { + actor: Actor, + farm: Farm, + created_at_unix: u64, +} + +impl PrepareRequest { + /// Creates explicit canonical planning inputs. + #[must_use] + pub const fn new(actor: Actor, farm: Farm, created_at_unix: u64) -> Self { + Self { + actor, + farm, + created_at_unix, + } + } +} + +/// Frozen, replay-stable farm publication plan. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Plan { + actor: Actor, + coordinate: AddressableCoordinate, + authored_event: AuthoredEventPlan, +} + +impl Plan { + /// Returns the exact authorized actor carried into signing. + #[must_use] + pub const fn actor(&self) -> &Actor { + &self.actor + } + + /// Returns the canonical addressable farm coordinate. + #[must_use] + pub const fn coordinate(&self) -> &AddressableCoordinate { + &self.coordinate + } + + /// Returns the immutable canonical authored event plan. + #[must_use] + pub const fn authored_event(&self) -> &AuthoredEventPlan { + &self.authored_event + } +} + +/// Farm plan validation stage. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PrepareErrorKind { + /// The actor does not claim the required farm author role. + UnauthorizedActor, + /// The canonical farm codec rejected the native farm model. + Encode, + /// The canonical addressable coordinate rejected the farm identity. + Coordinate, + /// The canonical event draft rejected the encoded parts. + Draft, +} + +/// One secret-safe farm planning failure retaining its lower source. +pub struct PrepareError { + kind: PrepareErrorKind, + source: Option<Box<dyn error::Error + Send + Sync>>, +} + +impl PrepareError { + /// Returns the stable client-level planning stage. + #[must_use] + pub const fn kind(&self) -> PrepareErrorKind { + self.kind + } + + fn unauthorized_actor() -> Self { + Self { + kind: PrepareErrorKind::UnauthorizedActor, + source: None, + } + } + + fn encode(source: EventEncodeError) -> Self { + Self::with_source(PrepareErrorKind::Encode, source) + } + + fn coordinate(source: ParseError) -> Self { + Self::with_source(PrepareErrorKind::Coordinate, source) + } + + fn draft(source: radroots_event::draft::DraftError) -> Self { + Self::with_source(PrepareErrorKind::Draft, source) + } + + fn with_source( + kind: PrepareErrorKind, + source: impl error::Error + Send + Sync + 'static, + ) -> Self { + Self { + kind, + source: Some(Box::new(source)), + } + } +} + +impl fmt::Display for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self.kind { + PrepareErrorKind::UnauthorizedActor => "farm actor is not authorized", + PrepareErrorKind::Encode => "farm model is invalid", + PrepareErrorKind::Coordinate => "farm coordinate is invalid", + PrepareErrorKind::Draft => "farm event draft is invalid", + }) + } +} + +impl fmt::Debug for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrepareError") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +impl error::Error for PrepareError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn error::Error + 'static)) + } +} + +/// Validates and freezes one farm profile without storage, signing, or network work. +/// +/// `Farm` contains only public profile locality. Exact coordinates and other +/// private farm artifacts are deliberately not accepted here; hosts persist +/// their typed references and metadata through +/// `radroots_storage::private_artifact::PrivateArtifactStore`. +pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> { + if !request.actor.satisfies(AuthorRole::Farmer) { + return Err(PrepareError::unauthorized_actor()); + } + let parts = to_wire_parts(&request.farm).map_err(PrepareError::encode)?; + let coordinate = AddressableCoordinate::parse(format!( + "{KIND_FARM}:{}:{}", + request.actor.public_key(), + request.farm.d_tag + )) + .map_err(PrepareError::coordinate)?; + let authored_event = AuthoredEventPlan::from_generic( + GenericEventDraft::new( + FARM_PROFILE_CONTRACT_ID, + parts.kind, + request.created_at_unix, + parts.tags, + parts.content, + request.actor.public_key().to_hex(), + ) + .map_err(PrepareError::draft)?, + ) + .map_err(PrepareError::draft)?; + Ok(Plan { + actor: request.actor, + coordinate, + authored_event, + }) +} + +#[cfg(feature = "sync")] +use radroots_signing::request::CancellationPolicy; +#[cfg(feature = "sync")] +use radroots_storage::journal::IdempotencyKey; +#[cfg(feature = "sync")] +use radroots_sync::{ + policy::{Error as SyncError, SyncId}, + push::PushStatus, +}; + +/// Explicit commit inputs for one prepared farm publication. +#[cfg(feature = "sync")] +#[derive(Clone, Debug)] +pub struct EnqueueRequest { + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, +} + +#[cfg(feature = "sync")] +impl EnqueueRequest { + /// Creates an enqueue request whose transport selection has no fallback. + #[must_use] + pub const fn new( + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, + ) -> Self { + Self { + operation_id, + idempotency_key, + plan, + profile, + delivery_deadline_unix_ms, + cancellation, + } + } +} + +/// Borrowed farm commit operations over the canonical sync engine. +#[cfg(feature = "sync")] +#[derive(Clone, Copy, Debug)] +pub struct Operations<'a> { + sync: crate::sync::Operations<'a>, +} + +#[cfg(feature = "sync")] +impl<'a> Operations<'a> { + pub(crate) const fn new(sync: crate::sync::Operations<'a>) -> Self { + Self { sync } + } + + /// Durably prepares, signs, and locally admits a farm publication. + /// + /// Once preparation commits, cancellation cannot erase the authored + /// intent. Replay with identical operation and idempotency inputs resumes + /// from its durable signing or admission phase. + pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> { + let targets = request + .profile + .targets() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + let satisfaction = request + .profile + .satisfaction() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + self.sync + .submit_push(radroots_sync::PushRequest::new( + request.operation_id, + request.idempotency_key, + request.plan.actor, + request.plan.authored_event, + targets, + satisfaction, + request.delivery_deadline_unix_ms, + request.cancellation, + )?) + .await + } +} + +#[cfg(test)] +mod tests { + use radroots_event::farm::FarmPublicLocation; + use radroots_signing::actor::ActorSource; + + use super::*; + + const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + + fn actor(role: AuthorRole) -> Actor { + Actor::from_public_key_hex(PUBLIC_KEY, ActorSource::ExplicitPublicKey, [role]) + .expect("actor") + } + + fn farm() -> Farm { + Farm { + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), + name: "Moss Street Farm".to_owned(), + about: Some("seasonal vegetables".to_owned()), + website: None, + picture: None, + banner: None, + location: Some(FarmPublicLocation { + primary: "Santa Cruz, California".to_owned(), + city: Some("Santa Cruz".to_owned()), + region: Some("California".to_owned()), + country: Some("US".to_owned()), + geohash: "9q8yy".to_owned(), + }), + tags: Some(vec!["vegetables".to_owned()]), + } + } + + #[test] + fn prepare_is_pure_deterministic_and_uses_canonical_public_types() { + let request = PrepareRequest::new(actor(AuthorRole::Farmer), farm(), 1_800_000_000); + let first = prepare(request.clone()).expect("first plan"); + let second = prepare(request).expect("second plan"); + + assert_eq!(first, second); + assert_eq!( + first + .authored_event() + .body() + .contract() + .contract_id() + .as_str(), + FARM_PROFILE_CONTRACT_ID + ); + assert_eq!(first.authored_event().body().kind(), KIND_FARM); + assert_eq!(first.authored_event().created_at(), 1_800_000_000); + assert_eq!( + first.authored_event().author(), + &actor(AuthorRole::Farmer).public_key() + ); + assert_eq!( + first.coordinate().as_str(), + format!("{KIND_FARM}:{PUBLIC_KEY}:AAAAAAAAAAAAAAAAAAAAAA") + ); + assert!( + first + .authored_event() + .body() + .content() + .contains("Moss Street Farm") + ); + assert!(!first.authored_event().body().content().contains("latitude")); + assert!( + !first + .authored_event() + .body() + .content() + .contains("longitude") + ); + } + + #[test] + fn prepare_maps_authorization_and_lower_validation_once() { + let unauthorized = prepare(PrepareRequest::new( + actor(AuthorRole::Buyer), + farm(), + 1_800_000_000, + )) + .expect_err("unauthorized"); + assert_eq!(unauthorized.kind(), PrepareErrorKind::UnauthorizedActor); + assert!(std::error::Error::source(&unauthorized).is_none()); + + let mut invalid = farm(); + invalid.name.clear(); + let encoded = prepare(PrepareRequest::new( + actor(AuthorRole::Farmer), + invalid, + 1_800_000_000, + )) + .expect_err("invalid model"); + assert_eq!(encoded.kind(), PrepareErrorKind::Encode); + assert!(std::error::Error::source(&encoded).is_some()); + assert_eq!(encoded.to_string(), "farm model is invalid"); + assert!(!format!("{encoded:?}").contains("name")); + } + + #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))] + mod enqueue { + use std::sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }; + + use radroots_storage::{ + Outbox, event::SourceGeneration, journal::IdempotencyKey, memory::MemoryStorage, + }; + use radroots_sync::{ + Engine, + policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage}, + }; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure, + SinkStatus, Target, TargetSet, TransportId, + capability::{Availability, Maturity, SinkCapabilities}, + outcome::Retryability, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, + }; + + use crate::{ClientBuilder, transport::Profile}; + + use super::*; + + struct HostClock; + struct SequenceIds(AtomicU8); + struct NoopSink; + + impl Clock for HostClock { + fn now_unix_ms(&self) -> Result<u64, Error> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|duration| u64::try_from(duration.as_millis()).ok()) + .filter(|value| *value != 0) + .ok_or(Error::ClockUnavailable) + } + } + + impl IdSource for SequenceIds { + fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> { + SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16]) + } + } + + impl EventSink for NoopSink { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> { + Box::pin(async { + Ok(SinkStatus::new( + TransportId::NOSTR, + true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, + "ready", + )) + }) + } + + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> + { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + #[tokio::test] + async fn enqueue_preserves_commit_cancellation_idempotency_and_delivery_policy() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([4; 32]).expect("generation"), + )); + let signer = + Arc::new(radroots_nostr::signing::LocalSigner::generate().expect("local signer")); + let farm_actor = Actor::new( + signer.public_key(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); + let plan = + prepare(PrepareRequest::new(farm_actor, farm(), 1_800_000_000)).expect("plan"); + let targets = TargetSet::new(vec![ + Target::nostr_relay("wss://farm.example").expect("target"), + ]) + .expect("targets"); + let satisfaction = + SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all()); + let profile = Profile::delivery(targets.clone(), satisfaction.clone()) + .expect("transport profile"); + let capability: Arc<dyn SyncStorage> = storage.clone(); + let engine = Engine::builder( + capability, + Arc::new(HostClock), + Arc::new(SequenceIds(AtomicU8::new(1))), + DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"), + ) + .sink(Arc::new(NoopSink)) + .signer(signer) + .build() + .expect("engine"); + let client = ClientBuilder::new() + .storage(storage.clone()) + .sync_engine(engine) + .build() + .expect("client"); + let operations = client.farm().expect("open").expect("farm operations"); + let request = EnqueueRequest::new( + SyncId::new([7; 16]).expect("operation id"), + IdempotencyKey::parse("farm-publish-a").expect("idempotency key"), + plan, + profile, + 2_000_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ); + + drop(operations.enqueue(request.clone())); + assert_eq!( + Outbox::status(storage.as_ref()) + .await + .expect("outbox status") + .pending, + 0 + ); + + let committed = operations + .enqueue(request.clone()) + .await + .expect("committed enqueue"); + assert_eq!(committed.delivery_plan().intent().target_set(), &targets); + assert_eq!( + committed.delivery_plan().intent().satisfaction(), + &satisfaction + ); + let replay = operations.enqueue(request).await.expect("replay"); + assert_eq!(replay, committed); + + let unavailable = EnqueueRequest::new( + SyncId::new([8; 16]).expect("operation id"), + IdempotencyKey::parse("farm-publish-preview").expect("idempotency key"), + prepare(PrepareRequest::new( + actor(AuthorRole::Farmer), + farm(), + 1_800_000_000, + )) + .expect("plan"), + Profile::unavailable_preview(TransportId::RETICULUM), + 2_000_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ); + assert_eq!( + operations.enqueue(unavailable).await, + Err(Error::InvalidPushRequest) + ); + } + } +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -0,0 +1,44 @@ +//! Host-neutral asynchronous client engine for Radroots. +//! +//! Advanced hosts compose capabilities through [`ClientBuilder`] and operate +//! through a cloneable [`Client`]. All fallible root operations use [`Result`] +//! and the SDK-owned [`Error`] boundary. +//! +//! Constructing an empty builder performs no I/O and makes missing composition +//! explicit: +//! +//! ``` +//! use radroots_sdk::ClientBuilder; +//! +//! let result = ClientBuilder::new().build(); +//! assert!(result.is_err()); +//! ``` +//! +//! Native structs are constructor-led and representation-private. Hosts must +//! not depend on field layout: +//! +//! ```compile_fail +//! use radroots_sdk::ClientBuilder; +//! +//! let _ = ClientBuilder { storage: None }; +//! ``` + +#![forbid(unsafe_code)] +#![doc = include_str!("../README.md")] + +mod adapters; + +pub mod capability; +pub mod client; +pub mod diagnostics; +pub mod error; +pub mod farm; +pub mod listing; +pub mod signing; +pub mod storage; +pub mod sync; +pub mod trade; +pub mod transport; + +pub use crate::client::{Client, ClientBuilder}; +pub use crate::error::{Error, Result}; diff --git a/crates/sdk/src/listing.rs b/crates/sdk/src/listing.rs @@ -0,0 +1,641 @@ +//! Side-effect-free listing planning and canonical durable enqueue operations. + +use std::{error, fmt}; + +use radroots_event::{contract::AuthorRole, id::ClassifiedListingAddress}; +use radroots_event_codec::authoring::AuthoredEventPlan; +use radroots_signing::Actor; + +mod operational_listing; + +pub use operational_listing::{ + BinPricingTryExt, RadrootsClassifiedListingAddressParts, + RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1, + RadrootsOperationalListingEditError, RadrootsOperationalListingLifecycleState, + RadrootsOperationalListingMutation, RadrootsOperationalListingMutationError, + RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal, + RadrootsOperationalListingTradeProjection, RadrootsPublicClassifiedListingAddress, + parse_classified_listing_address, parse_operational_listing_event, + parse_public_classified_listing_address, validate_operational_listing_event, + validate_operational_listing_model, +}; +use operational_listing::{ + build_operational_listing_mutation_plan, canonicalize_operational_listing_edit, +}; + +/// Supported public listing mutation intent. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Action { + /// Publish the first public version of a listing. + Publish, + /// Replace an existing addressable listing with a new public version. + Update, +} + +/// Pure inputs for one frozen public listing plan. +#[derive(Clone, Debug)] +pub struct PrepareRequest { + actor: Actor, + document: RadrootsOperationalListingEditDocumentV1, + action: Action, + created_at_unix: u64, +} + +impl PrepareRequest { + /// Creates a public listing publication request. + #[must_use] + pub const fn publish( + actor: Actor, + document: RadrootsOperationalListingEditDocumentV1, + created_at_unix: u64, + ) -> Self { + Self { + actor, + document, + action: Action::Publish, + created_at_unix, + } + } + + /// Creates a public listing replacement request. + #[must_use] + pub const fn update( + actor: Actor, + document: RadrootsOperationalListingEditDocumentV1, + created_at_unix: u64, + ) -> Self { + Self { + actor, + document, + action: Action::Update, + created_at_unix, + } + } +} + +/// Frozen, replay-stable public listing mutation plan. +#[derive(Clone, Debug)] +pub struct Plan { + actor: Actor, + action: Action, + address: ClassifiedListingAddress, + lifecycle: RadrootsOperationalListingLifecycleState, + authored_event: AuthoredEventPlan, +} + +impl Plan { + /// Returns the exact authorized actor carried into signing. + #[must_use] + pub const fn actor(&self) -> &Actor { + &self.actor + } + + /// Returns the requested listing mutation intent. + #[must_use] + pub const fn action(&self) -> Action { + self.action + } + + /// Returns the canonical addressable listing identity. + #[must_use] + pub const fn address(&self) -> &ClassifiedListingAddress { + &self.address + } + + /// Returns the lower-owned lifecycle state resulting from the mutation. + #[must_use] + pub const fn lifecycle(&self) -> RadrootsOperationalListingLifecycleState { + self.lifecycle + } + + /// Returns the immutable canonical authored event plan. + #[must_use] + pub const fn authored_event(&self) -> &AuthoredEventPlan { + &self.authored_event + } +} + +/// Listing plan validation stage. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PrepareErrorKind { + /// The actor does not claim the seller author role. + UnauthorizedActor, + /// The lower trade boundary rejected the untrusted edit document. + Edit, + /// The lower trade/event boundary rejected mutation preparation. + Mutation, +} + +/// One secret-safe listing planning failure retaining its lower source. +pub struct PrepareError { + kind: PrepareErrorKind, + source: Option<Box<dyn error::Error + Send + Sync>>, +} + +impl PrepareError { + /// Returns the stable client-level planning stage. + #[must_use] + pub const fn kind(&self) -> PrepareErrorKind { + self.kind + } + + fn unauthorized_actor() -> Self { + Self { + kind: PrepareErrorKind::UnauthorizedActor, + source: None, + } + } + + fn edit(source: RadrootsOperationalListingEditError) -> Self { + Self::with_source(PrepareErrorKind::Edit, source) + } + + fn mutation(source: RadrootsOperationalListingMutationError) -> Self { + Self::with_source(PrepareErrorKind::Mutation, source) + } + + fn with_source( + kind: PrepareErrorKind, + source: impl error::Error + Send + Sync + 'static, + ) -> Self { + Self { + kind, + source: Some(Box::new(source)), + } + } +} + +impl fmt::Display for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self.kind { + PrepareErrorKind::UnauthorizedActor => "listing actor is not authorized", + PrepareErrorKind::Edit => "listing edit is invalid", + PrepareErrorKind::Mutation => "listing mutation is invalid", + }) + } +} + +impl fmt::Debug for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrepareError") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +impl error::Error for PrepareError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn error::Error + 'static)) + } +} + +/// Validates and freezes one listing mutation without storage, signing, or network work. +/// +/// The canonical public model permits only coarse public locality. Exact +/// coordinates and other private artifacts are deliberately absent; hosts +/// persist those through `radroots_storage::private_artifact::PrivateArtifactStore`. +pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> { + if !request.actor.satisfies(AuthorRole::Seller) { + return Err(PrepareError::unauthorized_actor()); + } + let canonical = + canonicalize_operational_listing_edit(request.actor.public_key(), request.document) + .map_err(PrepareError::edit)?; + let address = canonical.public_listing_addr().clone(); + let mutation = match request.action { + Action::Publish => RadrootsOperationalListingMutation::publish(canonical), + Action::Update => RadrootsOperationalListingMutation::update(canonical), + }; + let lifecycle = mutation.lifecycle_state().map_err(PrepareError::mutation)?; + let authored_event = + build_operational_listing_mutation_plan(&mutation, request.created_at_unix) + .map_err(PrepareError::mutation)?; + Ok(Plan { + actor: request.actor, + action: request.action, + address, + lifecycle, + authored_event, + }) +} + +#[cfg(feature = "sync")] +use radroots_signing::request::CancellationPolicy; +#[cfg(feature = "sync")] +use radroots_storage::journal::IdempotencyKey; +#[cfg(feature = "sync")] +use radroots_sync::{ + policy::{Error as SyncError, SyncId}, + push::PushStatus, +}; + +/// Explicit commit inputs for one prepared public listing mutation. +#[cfg(feature = "sync")] +#[derive(Clone, Debug)] +pub struct EnqueueRequest { + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, +} + +#[cfg(feature = "sync")] +impl EnqueueRequest { + /// Creates an enqueue request whose transport selection has no fallback. + #[must_use] + pub const fn new( + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, + ) -> Self { + Self { + operation_id, + idempotency_key, + plan, + profile, + delivery_deadline_unix_ms, + cancellation, + } + } +} + +/// Borrowed listing commit operations over the canonical sync engine. +#[cfg(feature = "sync")] +#[derive(Clone, Copy, Debug)] +pub struct Operations<'a> { + sync: crate::sync::Operations<'a>, +} + +#[cfg(feature = "sync")] +impl<'a> Operations<'a> { + pub(crate) const fn new(sync: crate::sync::Operations<'a>) -> Self { + Self { sync } + } + + /// Durably prepares, signs, and locally admits a public listing mutation. + pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> { + let targets = request + .profile + .targets() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + let satisfaction = request + .profile + .satisfaction() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + self.sync + .submit_push(radroots_sync::PushRequest::new( + request.operation_id, + request.idempotency_key, + request.plan.actor, + request.plan.authored_event, + targets, + satisfaction, + request.delivery_deadline_unix_ms, + request.cancellation, + )?) + .await + } +} + +#[cfg(test)] +mod tests { + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::{ + envelope::kind::KIND_CLASSIFIED_LISTING, + farm::FarmRef, + id::{DTag, InventoryBinId}, + listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingProduct, + OperationalListingPublicLocation, OperationalListingStatus, + }, + }; + use radroots_signing::actor::ActorSource; + + use super::*; + + const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + + fn actor(role: AuthorRole) -> Actor { + Actor::from_public_key_hex(PUBLIC_KEY, ActorSource::ExplicitPublicKey, [role]) + .expect("actor") + } + + fn listing(seller: &str) -> OperationalListing { + OperationalListing { + d_tag: DTag::parse("AAAAAAAAAAAAAAAAAAAAAg").expect("d tag"), + published_at: None, + farm: FarmRef { + pubkey: seller.to_owned(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), + }, + product: OperationalListingProduct { + key: "coffee".to_owned(), + title: "Coffee".to_owned(), + category: "coffee".to_owned(), + summary: Some("Single origin coffee".to_owned()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: InventoryBinId::parse("bin-1").expect("bin id"), + bins: vec![OperationalListingBin { + bin_id: InventoryBinId::parse("bin-1").expect("bin id"), + quantity: Quantity::try_new(Decimal::from(1000_u32), Unit::MassG) + .expect("quantity"), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(20_u32), Currency::USD).expect("money"), + Quantity::try_new(Decimal::from(1_u32), Unit::MassG).expect("unit"), + ) + .expect("price"), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: Some(Decimal::from(5_u32)), + availability: Some(OperationalListingAvailability::Status { + status: OperationalListingStatus::Active, + }), + delivery_method: Some(OperationalListingDeliveryMethod::Pickup), + location: Some(OperationalListingPublicLocation { + primary: "Santa Cruz, California".to_owned(), + city: Some("Santa Cruz".to_owned()), + region: Some("California".to_owned()), + country: Some("US".to_owned()), + geohash: "9q8yy".to_owned(), + }), + images: None, + } + } + + fn document(seller: &str) -> RadrootsOperationalListingEditDocumentV1 { + RadrootsOperationalListingEditDocumentV1::new(listing(seller)) + } + + #[test] + fn prepare_publish_and_update_are_pure_canonical_and_privacy_bounded() { + let publish_request = PrepareRequest::publish( + actor(AuthorRole::Seller), + document(PUBLIC_KEY), + 1_800_000_000, + ); + let first = prepare(publish_request.clone()).expect("publish plan"); + let replay = prepare(publish_request).expect("replayed plan"); + let update = prepare(PrepareRequest::update( + actor(AuthorRole::Seller), + document(PUBLIC_KEY), + 1_800_000_001, + )) + .expect("update plan"); + + assert_eq!(first.action(), Action::Publish); + assert_eq!(update.action(), Action::Update); + assert_eq!( + first.lifecycle(), + RadrootsOperationalListingLifecycleState::Published + ); + assert_eq!( + update.lifecycle(), + RadrootsOperationalListingLifecycleState::Published + ); + assert_eq!(first.address(), replay.address()); + assert_eq!(first.authored_event(), replay.authored_event()); + assert_eq!(first.address(), update.address()); + assert_eq!( + first.authored_event().body().kind(), + KIND_CLASSIFIED_LISTING + ); + assert_eq!(first.authored_event().created_at(), 1_800_000_000); + assert!(!first.authored_event().body().content().contains("latitude")); + assert!( + !first + .authored_event() + .body() + .content() + .contains("longitude") + ); + } + + #[test] + fn prepare_maps_authorization_and_lower_validation_classes_once() { + let unauthorized = prepare(PrepareRequest::publish( + actor(AuthorRole::Buyer), + document(PUBLIC_KEY), + 1_800_000_000, + )) + .expect_err("unauthorized"); + assert_eq!(unauthorized.kind(), PrepareErrorKind::UnauthorizedActor); + assert!(std::error::Error::source(&unauthorized).is_none()); + + let mut invalid = listing(PUBLIC_KEY); + invalid.product.title.clear(); + let edit = prepare(PrepareRequest::publish( + actor(AuthorRole::Seller), + RadrootsOperationalListingEditDocumentV1::new(invalid), + 1_800_000_000, + )) + .expect_err("invalid listing"); + assert_eq!(edit.kind(), PrepareErrorKind::Edit); + assert!(std::error::Error::source(&edit).is_some()); + assert_eq!(edit.to_string(), "listing edit is invalid"); + assert!(!format!("{edit:?}").contains("title")); + + let mismatch = prepare(PrepareRequest::update( + actor(AuthorRole::Seller), + document("8f"), + 1_800_000_000, + )) + .expect_err("invalid seller identity"); + assert_eq!(mismatch.kind(), PrepareErrorKind::Edit); + } + + #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))] + mod enqueue { + use std::sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }; + + use radroots_storage::{ + Outbox, event::SourceGeneration, journal::IdempotencyKey, memory::MemoryStorage, + }; + use radroots_sync::{ + Engine, + policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage}, + }; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure, + SinkStatus, Target, TargetSet, TransportId, + capability::{Availability, Maturity, SinkCapabilities}, + outcome::Retryability, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, + }; + + use super::*; + use crate::{ClientBuilder, transport::Profile}; + + struct HostClock; + struct SequenceIds(AtomicU8); + struct NoopSink; + + impl Clock for HostClock { + fn now_unix_ms(&self) -> Result<u64, Error> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|duration| u64::try_from(duration.as_millis()).ok()) + .filter(|value| *value != 0) + .ok_or(Error::ClockUnavailable) + } + } + impl IdSource for SequenceIds { + fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> { + SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16]) + } + } + impl EventSink for NoopSink { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> { + Box::pin(async { + Ok(SinkStatus::new( + TransportId::NOSTR, + true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, + "ready", + )) + }) + } + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> + { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + #[tokio::test] + async fn enqueue_preserves_commit_cancellation_idempotency_and_transport_outcomes() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([5; 32]).expect("generation"), + )); + let signer = + Arc::new(radroots_nostr::signing::LocalSigner::generate().expect("local signer")); + let seller = Actor::new( + signer.public_key(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("actor"); + let plan = prepare(PrepareRequest::publish( + seller, + document(&signer.public_key().to_hex()), + 1_800_000_000, + )) + .expect("plan"); + let targets = TargetSet::new(vec![ + Target::nostr_relay("wss://listing.example").expect("target"), + ]) + .expect("targets"); + let satisfaction = + SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all()); + let profile = Profile::delivery(targets.clone(), satisfaction.clone()) + .expect("transport profile"); + let capability: Arc<dyn SyncStorage> = storage.clone(); + let engine = Engine::builder( + capability, + Arc::new(HostClock), + Arc::new(SequenceIds(AtomicU8::new(1))), + DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"), + ) + .sink(Arc::new(NoopSink)) + .signer(signer) + .build() + .expect("engine"); + let client = ClientBuilder::new() + .storage(storage.clone()) + .sync_engine(engine) + .build() + .expect("client"); + let operations = client.listing().expect("open").expect("listing operations"); + let request = EnqueueRequest::new( + SyncId::new([9; 16]).expect("operation id"), + IdempotencyKey::parse("listing-publish-a").expect("idempotency key"), + plan, + profile, + 2_000_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ); + + drop(operations.enqueue(request.clone())); + assert_eq!( + Outbox::status(storage.as_ref()) + .await + .expect("outbox status") + .pending, + 0 + ); + let committed = operations + .enqueue(request.clone()) + .await + .expect("committed enqueue"); + assert_eq!(committed.delivery_plan().intent().target_set(), &targets); + assert_eq!( + committed.delivery_plan().intent().satisfaction(), + &satisfaction + ); + let replay = operations.enqueue(request).await.expect("replay"); + assert_eq!(replay, committed); + + let unavailable = EnqueueRequest::new( + SyncId::new([10; 16]).expect("operation id"), + IdempotencyKey::parse("listing-update-preview").expect("idempotency key"), + prepare(PrepareRequest::update( + actor(AuthorRole::Seller), + document(PUBLIC_KEY), + 1_800_000_001, + )) + .expect("plan"), + Profile::unavailable_preview(TransportId::RETICULUM), + 2_000_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ); + assert_eq!( + operations.enqueue(unavailable).await, + Err(Error::InvalidPushRequest) + ); + } + } +} diff --git a/crates/sdk/src/listing/operational_listing/draft.rs b/crates/sdk/src/listing/operational_listing/draft.rs @@ -0,0 +1,548 @@ +//! Canonicalization for Radroots Listing v1 drafts. + +#![forbid(unsafe_code)] + +use core::fmt; + +use std::{format, vec::Vec}; + +use radroots_event::{ + envelope::kind::KIND_CLASSIFIED_LISTING, + id::{ClassifiedListingAddress, InventoryBinId, ParseError}, + listing::operational::OperationalListing, + trade::validation::OperationalListingValidationError, +}; +use radroots_identity::{Error as PublicKeyError, PublicKey}; + +use super::validation::validate_operational_listing_model; + +#[derive(Clone, Debug)] +pub struct RadrootsOperationalListingEditDocumentV1 { + pub listing: OperationalListing, +} + +impl RadrootsOperationalListingEditDocumentV1 { + pub fn new(listing: OperationalListing) -> Self { + Self { listing } + } +} + +#[derive(Clone, Debug)] +pub struct RadrootsOperationalListingCanonicalEdit { + listing: OperationalListing, + seller_pubkey: PublicKey, + public_listing_addr: ClassifiedListingAddress, +} + +impl RadrootsOperationalListingCanonicalEdit { + pub fn new( + mut listing: OperationalListing, + seller_pubkey: PublicKey, + ) -> Result<Self, RadrootsOperationalListingEditError> { + let farm_pubkey = PublicKey::from_hex(listing.farm.pubkey.as_str()) + .map_err(RadrootsOperationalListingEditError::InvalidFarmPubkey)?; + if farm_pubkey != seller_pubkey { + return Err(RadrootsOperationalListingEditError::FarmPubkeyMismatch { + expected_pubkey: seller_pubkey, + actual_pubkey: farm_pubkey, + }); + } + listing.farm.pubkey = farm_pubkey.to_hex(); + validate_listing_bins(&listing)?; + let listing = validate_operational_listing_model(listing, &seller_pubkey) + .map_err(RadrootsOperationalListingEditError::InvalidModel)? + .listing; + + let public_listing_addr = listing_addr( + KIND_CLASSIFIED_LISTING, + &seller_pubkey, + listing.d_tag.as_str(), + ); + + Ok(Self { + listing, + seller_pubkey, + public_listing_addr, + }) + } + + pub fn listing(&self) -> &OperationalListing { + &self.listing + } + + pub fn seller_pubkey(&self) -> &PublicKey { + &self.seller_pubkey + } + + pub fn public_listing_addr(&self) -> &ClassifiedListingAddress { + &self.public_listing_addr + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsOperationalListingEditError { + InvalidFarmPubkey(PublicKeyError), + InvalidClassifiedListingAddress(ParseError), + InvalidModel(OperationalListingValidationError), + FarmPubkeyMismatch { + expected_pubkey: PublicKey, + actual_pubkey: PublicKey, + }, + MissingPrimaryBin { + primary_bin_id: InventoryBinId, + }, + DuplicateBinId { + bin_id: InventoryBinId, + }, +} + +impl fmt::Display for RadrootsOperationalListingEditError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidFarmPubkey(error) => { + write!(f, "invalid listing edit farm pubkey: {error}") + } + Self::InvalidClassifiedListingAddress(error) => { + write!(f, "invalid listing edit address: {error}") + } + Self::InvalidModel(error) => { + write!(f, "invalid listing edit model: {error}") + } + Self::FarmPubkeyMismatch { .. } => { + f.write_str("listing edit farm pubkey does not match seller") + } + Self::MissingPrimaryBin { .. } => f.write_str("listing edit primary bin is missing"), + Self::DuplicateBinId { .. } => f.write_str("listing edit contains duplicate bin ID"), + } + } +} + +impl core::error::Error for RadrootsOperationalListingEditError {} + +fn validate_listing_bins( + listing: &OperationalListing, +) -> Result<(), RadrootsOperationalListingEditError> { + let primary_bin_id = listing.primary_bin_id.clone(); + let mut seen_bin_ids = Vec::new(); + let mut primary_bin_found = false; + for bin in &listing.bins { + if seen_bin_ids + .iter() + .any(|seen_bin_id| seen_bin_id == &bin.bin_id) + { + return Err(RadrootsOperationalListingEditError::DuplicateBinId { + bin_id: bin.bin_id.clone(), + }); + } + if bin.bin_id == primary_bin_id { + primary_bin_found = true; + } + seen_bin_ids.push(bin.bin_id.clone()); + } + + if !primary_bin_found { + return Err(RadrootsOperationalListingEditError::MissingPrimaryBin { primary_bin_id }); + } + Ok(()) +} + +fn listing_addr(kind: u32, seller_pubkey: &PublicKey, d_tag: &str) -> ClassifiedListingAddress { + ClassifiedListingAddress::parse(format!("{kind}:{seller_pubkey}:{d_tag}")) + .expect("typed listing identity must form a listing address") +} + +/// Canonicalizes an untrusted listing edit for an already-authorized seller. +/// +/// Actor provenance and role authorization belong to the signing/workflow +/// boundary. This deterministic function validates only the supplied public +/// identity and listing data and performs no signing or authorization. +pub fn canonicalize_operational_listing_edit( + seller_pubkey: PublicKey, + mut document: RadrootsOperationalListingEditDocumentV1, +) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditError> { + let farm_pubkey = document.listing.farm.pubkey.as_str(); + if farm_pubkey.is_empty() { + document.listing.farm.pubkey = seller_pubkey.to_hex(); + } + + RadrootsOperationalListingCanonicalEdit::new(document.listing, seller_pubkey) +} + +#[cfg(test)] +mod tests { + const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + const FIXTURE_BOB_PUBLIC_KEY_HEX: &str = + "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::{ + envelope::kind::KIND_CLASSIFIED_LISTING, + farm::FarmRef, + id::{ClassifiedListingAddress, DTag, InventoryBinId}, + listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingProduct, + OperationalListingPublicLocation, OperationalListingStatus, + }, + trade::validation::OperationalListingValidationError, + }; + use radroots_identity::PublicKey; + + use super::{ + RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1, + RadrootsOperationalListingEditError, canonicalize_operational_listing_edit, + }; + + const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX; + const OTHER: &str = FIXTURE_BOB_PUBLIC_KEY_HEX; + + fn d_tag(raw: &str) -> DTag { + DTag::parse(raw).expect("d tag") + } + + fn bin_id(raw: &str) -> InventoryBinId { + InventoryBinId::parse(raw).expect("bin id") + } + + fn listing() -> OperationalListing { + OperationalListing { + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), + published_at: None, + farm: FarmRef { + pubkey: SELLER.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + product: OperationalListingProduct { + key: "coffee".to_string(), + title: "Coffee".to_string(), + category: "coffee".to_string(), + summary: Some("Single origin coffee".to_string()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: bin_id("bin-1"), + bins: vec![OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: Some(Decimal::from(5u32)), + availability: Some(OperationalListingAvailability::Status { + status: OperationalListingStatus::Active, + }), + delivery_method: Some(OperationalListingDeliveryMethod::Pickup), + location: Some(OperationalListingPublicLocation { + primary: "Victoria".to_string(), + city: Some("Victoria".to_string()), + region: Some("British Columbia".to_string()), + country: Some("CA".to_string()), + geohash: "c287g".to_string(), + }), + images: None, + } + } + + fn seller_pubkey() -> PublicKey { + PublicKey::from_hex(SELLER).expect("seller public key") + } + + #[test] + fn draft_document_wraps_listing() { + let document = RadrootsOperationalListingEditDocumentV1::new(listing()); + + assert_eq!(document.listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); + assert_eq!(document.listing.product.title, "Coffee"); + } + + #[cfg(any())] + #[test] + fn draft_document_deserializes_as_untrusted_input() { + let json = serde_json::to_string(&RadrootsOperationalListingEditDocumentV1::new(listing())) + .expect("serialize document"); + + let document: RadrootsOperationalListingEditDocumentV1 = + serde_json::from_str(&json).expect("deserialize document"); + let canonical = canonicalize_operational_listing_edit(seller_pubkey(), document) + .expect("canonical draft"); + + assert_eq!(canonical.seller_pubkey().to_hex(), SELLER); + assert_eq!(canonical.listing().product.title, "Coffee"); + } + + #[test] + fn canonical_draft_carries_seller_listing_and_addresses() { + let seller_pubkey = PublicKey::from_hex(SELLER).expect("seller"); + let listing = listing(); + + let canonical = RadrootsOperationalListingCanonicalEdit::new(listing, seller_pubkey) + .expect("canonical"); + + assert_eq!(canonical.seller_pubkey(), &seller_pubkey); + assert_eq!( + canonical.public_listing_addr().as_str(), + format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!(canonical.listing().d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); + } + + #[test] + fn listing_edit_error_variants_are_precise() { + assert!(matches!( + RadrootsOperationalListingEditError::InvalidFarmPubkey( + PublicKey::from_hex("bad").unwrap_err() + ), + RadrootsOperationalListingEditError::InvalidFarmPubkey(_) + )); + assert!(matches!( + RadrootsOperationalListingEditError::InvalidClassifiedListingAddress( + ClassifiedListingAddress::parse("bad").unwrap_err() + ), + RadrootsOperationalListingEditError::InvalidClassifiedListingAddress(_) + )); + } + + #[test] + fn canonicalize_operational_listing_edit_fills_missing_farm_pubkey_and_derives_address() { + let mut listing = listing(); + listing.farm.pubkey.clear(); + let document = RadrootsOperationalListingEditDocumentV1::new(listing); + + let canonical = canonicalize_operational_listing_edit(seller_pubkey(), document) + .expect("canonical draft"); + + assert_eq!(canonical.seller_pubkey().to_hex(), SELLER); + assert_eq!( + canonical.public_listing_addr().as_str(), + format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!(canonical.listing().farm.pubkey, SELLER); + } + + #[test] + fn canonicalize_operational_listing_edit_rejects_mismatched_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey = OTHER.to_string(); + let document = RadrootsOperationalListingEditDocumentV1::new(listing); + + let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err(); + + assert!(matches!( + error, + RadrootsOperationalListingEditError::FarmPubkeyMismatch { .. } + )); + } + + #[test] + fn canonicalize_operational_listing_edit_rejects_invalid_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey = "bad".to_string(); + let document = RadrootsOperationalListingEditDocumentV1::new(listing); + + let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err(); + + assert!(matches!( + error, + RadrootsOperationalListingEditError::InvalidFarmPubkey(_) + )); + } + + #[test] + fn canonical_draft_new_rejects_mismatched_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey = OTHER.to_string(); + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert!(matches!( + error, + RadrootsOperationalListingEditError::FarmPubkeyMismatch { .. } + )); + } + + #[test] + fn canonical_draft_new_rejects_invalid_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey = "bad".to_string(); + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert!(matches!( + error, + RadrootsOperationalListingEditError::InvalidFarmPubkey(_) + )); + } + + #[test] + fn canonical_draft_new_rejects_empty_farm_pubkey() { + let mut listing = listing(); + listing.farm.pubkey.clear(); + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert!(matches!( + error, + RadrootsOperationalListingEditError::InvalidFarmPubkey(_) + )); + } + + #[test] + fn canonicalize_operational_listing_edit_rejects_missing_primary_bin() { + let mut listing = listing(); + listing.primary_bin_id = bin_id("bin-2"); + let document = RadrootsOperationalListingEditDocumentV1::new(listing); + + let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::MissingPrimaryBin { + primary_bin_id: bin_id("bin-2") + } + ); + } + + #[test] + fn canonical_draft_new_rejects_missing_primary_bin() { + let mut listing = listing(); + listing.primary_bin_id = bin_id("bin-2"); + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::MissingPrimaryBin { + primary_bin_id: bin_id("bin-2") + } + ); + } + + #[test] + fn canonicalize_operational_listing_edit_rejects_duplicate_bin_ids() { + let mut listing = listing(); + listing.bins.push(listing.bins[0].clone()); + let document = RadrootsOperationalListingEditDocumentV1::new(listing); + + let error = canonicalize_operational_listing_edit(seller_pubkey(), document).unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::DuplicateBinId { + bin_id: bin_id("bin-1") + } + ); + } + + #[test] + fn canonical_draft_new_rejects_invalid_model() { + let mut listing = listing(); + listing.inventory_available = None; + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::InvalidModel( + OperationalListingValidationError::MissingInventory + ) + ); + } + + #[test] + fn canonical_draft_new_rejects_invalid_secondary_bin() { + let mut invalid_quantity_listing = listing(); + let mut secondary_bin = invalid_quantity_listing.bins[0].clone(); + secondary_bin.bin_id = bin_id("bin-2"); + secondary_bin.quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(); + invalid_quantity_listing.bins.push(secondary_bin); + + let error = RadrootsOperationalListingCanonicalEdit::new( + invalid_quantity_listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::InvalidModel( + OperationalListingValidationError::InvalidBin + ) + ); + + let mut mismatched_unit_listing = listing(); + let mut secondary_bin = mismatched_unit_listing.bins[0].clone(); + secondary_bin.bin_id = bin_id("bin-2"); + secondary_bin.price_per_canonical_unit = QuantityPrice::try_new( + secondary_bin.price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(), + ) + .unwrap(); + mismatched_unit_listing.bins.push(secondary_bin); + + let error = RadrootsOperationalListingCanonicalEdit::new( + mismatched_unit_listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::InvalidModel( + OperationalListingValidationError::InvalidPrice + ) + ); + } + + #[test] + fn canonical_draft_new_rejects_duplicate_bin_ids() { + let mut listing = listing(); + listing.bins.push(listing.bins[0].clone()); + + let error = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .unwrap_err(); + + assert_eq!( + error, + RadrootsOperationalListingEditError::DuplicateBinId { + bin_id: bin_id("bin-1") + } + ); + } +} diff --git a/crates/sdk/src/listing/operational_listing/mod.rs b/crates/sdk/src/listing/operational_listing/mod.rs @@ -0,0 +1,253 @@ +pub mod draft; +pub mod model; +pub mod mutation; +pub mod price_ext; +pub mod validation; + +use radroots_event::{ + envelope::EventEnvelope, + id::{AddressableCoordinateParts, ClassifiedListingAddress, DTag, ParseError}, + listing::operational::{OperationalListing, OperationalListingParseError}, +}; +use radroots_event_codec::decode::operational_listing::operational_listing_from_nostr_event; +use radroots_identity::PublicKey; + +pub use self::draft::{ + RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1, + RadrootsOperationalListingEditError, canonicalize_operational_listing_edit, +}; +pub use self::model::{RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal}; +pub use self::mutation::build_operational_listing_mutation_plan; +pub use self::mutation::{ + RadrootsOperationalListingLifecycleState, RadrootsOperationalListingMutation, + RadrootsOperationalListingMutationError, +}; +pub use self::price_ext::BinPricingTryExt; +pub use self::validation::{ + RadrootsOperationalListingTradeProjection, validate_operational_listing_event, + validate_operational_listing_model, +}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsClassifiedListingAddressParts { + pub address: ClassifiedListingAddress, + pub kind: u32, + pub seller_pubkey: PublicKey, + pub listing_id: DTag, +} + +impl RadrootsClassifiedListingAddressParts { + pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> { + parse_classified_listing_address(value) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsPublicClassifiedListingAddress { + pub address: ClassifiedListingAddress, + pub kind: u32, + pub seller_pubkey: PublicKey, + pub listing_id: DTag, +} + +impl RadrootsPublicClassifiedListingAddress { + pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> { + parse_public_classified_listing_address(value) + } +} + +pub fn parse_classified_listing_address( + value: impl AsRef<str>, +) -> Result<RadrootsClassifiedListingAddressParts, ParseError> { + let value = value.as_ref(); + let address = ClassifiedListingAddress::parse(value)?; + let parts = AddressableCoordinateParts::parse(address.as_str()) + .expect("typed listing address must contain valid coordinate parts"); + Ok(RadrootsClassifiedListingAddressParts { + address, + kind: parts.kind, + seller_pubkey: parts.pubkey, + listing_id: parts.d_tag, + }) +} + +pub fn parse_public_classified_listing_address( + value: impl AsRef<str>, +) -> Result<RadrootsPublicClassifiedListingAddress, ParseError> { + let parts = parse_classified_listing_address(value)?; + Ok(RadrootsPublicClassifiedListingAddress { + address: parts.address, + kind: parts.kind, + seller_pubkey: parts.seller_pubkey, + listing_id: parts.listing_id, + }) +} + +pub fn parse_operational_listing_event( + event: &EventEnvelope, +) -> Result<OperationalListing, OperationalListingParseError> { + operational_listing_from_nostr_event(event) +} + +#[cfg(test)] +mod tests { + use super::{ + RadrootsClassifiedListingAddressParts, RadrootsPublicClassifiedListingAddress, + parse_classified_listing_address, parse_operational_listing_event, + parse_public_classified_listing_address, + }; + use radroots_event::{ + envelope::EventEnvelope, + envelope::EventEnvelopeParts, + envelope::kind::{KIND_CLASSIFIED_LISTING, KIND_PROFILE}, + id::ClassifiedListingAddress, + listing::operational::OperationalListingParseError, + }; + + const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + fn listing_event() -> EventEnvelope { + EventEnvelope::new(EventEnvelopeParts { + id: "9".repeat(64), + author: SELLER.to_string(), + created_at: 1, + kind: KIND_CLASSIFIED_LISTING, + tags: vec![ + vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()], + vec!["p".into(), SELLER.into()], + vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")], + vec!["key".into(), "coffee".into()], + vec!["title".into(), "Coffee".into()], + vec!["category".into(), "coffee".into()], + vec!["summary".into(), "Single origin".into()], + vec!["radroots:primary_bin".into(), "bin-1".into()], + vec![ + "radroots:bin".into(), + "bin-1".into(), + "1000".into(), + "g".into(), + ], + vec![ + "radroots:price".into(), + "bin-1".into(), + "20".into(), + "USD".into(), + "1".into(), + "g".into(), + ], + ], + content: String::new(), + sig: "f".repeat(128), + }) + .expect("listing event") + } + + #[test] + fn parse_operational_listing_event_rejects_non_listing_kind() { + let event = EventEnvelope::new(EventEnvelopeParts { + id: "8".repeat(64), + author: SELLER.to_string(), + created_at: 1, + kind: KIND_PROFILE, + tags: vec![], + content: String::new(), + sig: "f".repeat(128), + }) + .expect("profile event"); + + assert!(matches!( + parse_operational_listing_event(&event), + Err(OperationalListingParseError::InvalidKind(KIND_PROFILE)) + )); + } + + #[test] + fn parse_operational_listing_event_accepts_listing_kind() { + let listing = parse_operational_listing_event(&listing_event()).expect("listing"); + + assert_eq!(listing.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); + assert_eq!(listing.farm.pubkey, SELLER); + assert_eq!(listing.primary_bin_id.as_str(), "bin-1"); + } + + #[test] + fn listing_address_associated_parsers_delegate_to_public_parsers() { + let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1"); + + let listing = + RadrootsClassifiedListingAddressParts::parse(raw.clone()).expect("listing address"); + let public = RadrootsPublicClassifiedListingAddress::parse(&raw).expect("public address"); + let typed = parse_public_classified_listing_address( + ClassifiedListingAddress::parse(&raw).expect("typed addr"), + ) + .expect("typed public address"); + + assert_eq!(listing.address.as_str(), raw); + assert_eq!(public.address.as_str(), raw); + assert_eq!(typed.address.as_str(), raw); + assert_eq!(listing.seller_pubkey.to_hex(), SELLER); + assert_eq!(public.seller_pubkey.to_hex(), SELLER); + assert_eq!(typed.seller_pubkey.to_hex(), SELLER); + } + + #[test] + fn parse_public_classified_listing_address_accepts_public_listing_kind() { + let raw = format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:listing-1"); + let parsed = parse_public_classified_listing_address(&raw).expect("public listing address"); + + assert_eq!(parsed.address.as_str(), raw); + assert_eq!(parsed.kind, KIND_CLASSIFIED_LISTING); + assert_eq!(parsed.seller_pubkey.to_hex(), SELLER); + assert_eq!(parsed.listing_id.as_str(), "listing-1"); + } + + #[test] + fn parse_classified_listing_address_rejects_retired_listing_kind() { + let raw = format!("30403:{SELLER}:listing-1"); + + assert!(matches!( + parse_classified_listing_address(&raw), + Err(radroots_event::id::ParseError::UnexpectedKind { + expected: KIND_CLASSIFIED_LISTING, + actual: 30403, + }) + )); + assert!(matches!( + parse_public_classified_listing_address(&raw), + Err(radroots_event::id::ParseError::UnexpectedKind { + expected: KIND_CLASSIFIED_LISTING, + actual: 30403, + }) + )); + } + + #[test] + fn parse_public_classified_listing_address_maps_invalid_listing_addresses() { + assert!(matches!( + parse_public_classified_listing_address("not-an-address"), + Err(radroots_event::id::ParseError::InvalidFormat) + )); + + let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1"); + assert!(matches!( + parse_public_classified_listing_address(&raw), + Err(radroots_event::id::ParseError::UnexpectedKind { + expected: KIND_CLASSIFIED_LISTING, + actual: KIND_PROFILE, + }) + )); + assert!(ClassifiedListingAddress::parse(&raw).is_err()); + } + + #[test] + fn parse_classified_listing_address_rejects_non_listing_kind() { + let raw = format!("{KIND_PROFILE}:{SELLER}:listing-1"); + + assert!(matches!( + parse_classified_listing_address(&raw), + Err(radroots_event::id::ParseError::UnexpectedKind { + expected: KIND_CLASSIFIED_LISTING, + actual: KIND_PROFILE, + }) + )); + } +} diff --git a/crates/sdk/src/listing/operational_listing/model.rs b/crates/sdk/src/listing/operational_listing/model.rs @@ -0,0 +1,15 @@ +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOperationalListingSubtotal { + pub price_amount: radroots_core::Money, + pub price_currency: radroots_core::Currency, + pub quantity_amount: radroots_core::Decimal, + pub quantity_unit: radroots_core::Unit, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsOperationalListingTotal { + pub price_amount: radroots_core::Money, + pub price_currency: radroots_core::Currency, + pub quantity_amount: radroots_core::Decimal, + pub quantity_unit: radroots_core::Unit, +} diff --git a/crates/sdk/src/listing/operational_listing/mutation.rs b/crates/sdk/src/listing/operational_listing/mutation.rs @@ -0,0 +1,510 @@ +//! Mutation plan preparation for Radroots Listing v1. + +#![forbid(unsafe_code)] + +use core::fmt; + +use std::string::{String, ToString}; + +use radroots_event::id::ClassifiedListingAddress; +use radroots_event::{ + GenericEventDraft, draft::DraftError, envelope::kind::KIND_CLASSIFIED_LISTING, +}; +use radroots_event_codec::{ + authoring::AuthoredEventPlan, encode::operational_listing::to_wire_parts_with_kind, +}; + +use crate::listing::operational_listing::draft::RadrootsOperationalListingCanonicalEdit; + +/// Listing v1 mutation intent for draft preparation only. +/// +/// Publish and update target the public listing event, while local-only draft +/// persistence and archive are intentionally unsupported as wire events. +#[derive(Clone, Debug)] +pub enum RadrootsOperationalListingMutation { + Publish { + draft: RadrootsOperationalListingCanonicalEdit, + }, + Update { + draft: RadrootsOperationalListingCanonicalEdit, + }, + SaveDraft { + draft: RadrootsOperationalListingCanonicalEdit, + }, + Archive { + listing_addr: ClassifiedListingAddress, + }, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsOperationalListingLifecycleState { + Draft, + Published, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsOperationalListingMutationError { + UnsupportedMutation, + EncodeListing(String), + AuthoredPlan(DraftError), +} + +impl fmt::Display for RadrootsOperationalListingMutationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedMutation => f.write_str("listing mutation is not supported"), + Self::EncodeListing(error) => { + write!(f, "failed to encode listing mutation: {error}") + } + Self::AuthoredPlan(error) => { + write!(f, "failed to build listing authored plan: {error}") + } + } + } +} + +impl core::error::Error for RadrootsOperationalListingMutationError {} + +const OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID: &str = "radroots.operational_listing.published.v1"; + +impl RadrootsOperationalListingMutation { + pub fn publish(draft: RadrootsOperationalListingCanonicalEdit) -> Self { + Self::Publish { draft } + } + + pub fn update(draft: RadrootsOperationalListingCanonicalEdit) -> Self { + Self::Update { draft } + } + + pub fn save_draft(draft: RadrootsOperationalListingCanonicalEdit) -> Self { + Self::SaveDraft { draft } + } + + pub fn archive(listing_addr: ClassifiedListingAddress) -> Self { + Self::Archive { listing_addr } + } + + pub fn lifecycle_state( + &self, + ) -> Result<RadrootsOperationalListingLifecycleState, RadrootsOperationalListingMutationError> + { + match self { + Self::Publish { .. } | Self::Update { .. } => { + Ok(RadrootsOperationalListingLifecycleState::Published) + } + Self::SaveDraft { .. } => Ok(RadrootsOperationalListingLifecycleState::Draft), + Self::Archive { .. } => { + Err(RadrootsOperationalListingMutationError::UnsupportedMutation) + } + } + } + + pub fn canonical_draft( + &self, + ) -> Result<&RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingMutationError> + { + match self { + Self::Publish { draft } | Self::Update { draft } | Self::SaveDraft { draft } => { + Ok(draft) + } + Self::Archive { .. } => { + Err(RadrootsOperationalListingMutationError::UnsupportedMutation) + } + } + } + + pub fn listing_addr( + &self, + ) -> Result<&ClassifiedListingAddress, RadrootsOperationalListingMutationError> { + match self { + Self::Publish { draft } | Self::Update { draft } => Ok(draft.public_listing_addr()), + Self::SaveDraft { draft } => Ok(draft.public_listing_addr()), + Self::Archive { .. } => { + Err(RadrootsOperationalListingMutationError::UnsupportedMutation) + } + } + } +} + +pub fn build_operational_listing_mutation_plan( + mutation: &RadrootsOperationalListingMutation, + created_at: u64, +) -> Result<AuthoredEventPlan, RadrootsOperationalListingMutationError> { + let (draft, kind, contract_id) = match mutation { + RadrootsOperationalListingMutation::Publish { draft } + | RadrootsOperationalListingMutation::Update { draft } => ( + draft, + KIND_CLASSIFIED_LISTING, + OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID, + ), + RadrootsOperationalListingMutation::SaveDraft { .. } + | RadrootsOperationalListingMutation::Archive { .. } => { + return Err(RadrootsOperationalListingMutationError::UnsupportedMutation); + } + }; + let parts = to_wire_parts_with_kind(draft.listing(), kind).map_err(|error| { + RadrootsOperationalListingMutationError::EncodeListing(error.to_string()) + })?; + AuthoredEventPlan::from_generic( + GenericEventDraft::new( + contract_id, + parts.kind, + created_at, + parts.tags, + parts.content, + draft.seller_pubkey().to_hex(), + ) + .map_err(RadrootsOperationalListingMutationError::AuthoredPlan)?, + ) + .map_err(RadrootsOperationalListingMutationError::AuthoredPlan) +} + +#[cfg(all(test, feature = "local-signing"))] +mod tests { + const FIXTURE_ALICE_SECRET_KEY_HEX: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; + const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp}; + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::{ + contract::validate_event_contract_shape, + envelope::kind::KIND_CLASSIFIED_LISTING, + farm::FarmRef, + farm::resource_area::ResourceAreaRef, + id::{ClassifiedListingAddress, DTag, InventoryBinId}, + listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingProduct, + OperationalListingPublicLocation, OperationalListingStatus, + }, + wire::Nip01EventWire, + }; + use radroots_event_codec::{authoring::AuthoredEventPlan, verify::verify_nip01_event}; + use radroots_identity::PublicKey; + + use crate::listing::operational_listing::draft::RadrootsOperationalListingCanonicalEdit; + use crate::listing::operational_listing::validation::validate_operational_listing_event; + + use super::{ + OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID, RadrootsOperationalListingLifecycleState, + RadrootsOperationalListingMutation, RadrootsOperationalListingMutationError, + build_operational_listing_mutation_plan, + }; + + const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX; + + fn d_tag(raw: &str) -> DTag { + DTag::parse(raw).expect("d tag") + } + + fn bin_id(raw: &str) -> InventoryBinId { + InventoryBinId::parse(raw).expect("bin id") + } + + fn sign_plan(plan: &AuthoredEventPlan) -> radroots_event::envelope::EventEnvelope { + let keys = Keys::parse(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture signing key"); + assert_eq!(keys.public_key().to_hex(), plan.author().to_hex()); + let tags = plan + .body() + .tags() + .iter() + .cloned() + .map(|tag| Tag::parse(tag).expect("plan tag")) + .collect::<Vec<_>>(); + let event = EventBuilder::new( + Kind::Custom(u16::try_from(plan.body().kind()).expect("NIP-01 kind")), + plan.body().content(), + ) + .tags(tags) + .allow_self_tagging() + .custom_created_at(Timestamp::from_secs(plan.created_at())) + .sign_with_keys(&keys) + .expect("signed listing event"); + assert_eq!(event.id.to_hex(), plan.expected_event_id().to_hex()); + let raw_json = event.as_json(); + Nip01EventWire::parse_json(raw_json.as_str()) + .expect("canonical event wire") + .into_envelope() + .expect("event envelope") + } + + fn listing() -> OperationalListing { + OperationalListing { + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), + published_at: None, + farm: FarmRef { + pubkey: SELLER.to_string(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(), + }, + product: OperationalListingProduct { + key: "coffee".to_string(), + title: "Coffee".to_string(), + category: "coffee".to_string(), + summary: Some("Single origin coffee".to_string()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: bin_id("bin-1"), + bins: vec![OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: Some(Decimal::from(5u32)), + availability: Some(OperationalListingAvailability::Status { + status: OperationalListingStatus::Active, + }), + delivery_method: Some(OperationalListingDeliveryMethod::Pickup), + location: Some(OperationalListingPublicLocation { + primary: "Farm".to_string(), + city: Some("Town".to_string()), + region: Some("Region".to_string()), + country: Some("US".to_string()), + geohash: "9q8yy".to_string(), + }), + images: None, + } + } + + fn canonical_draft() -> RadrootsOperationalListingCanonicalEdit { + RadrootsOperationalListingCanonicalEdit::new( + listing(), + PublicKey::from_hex(SELLER).expect("seller"), + ) + .expect("canonical listing edit") + } + + #[test] + fn supported_mutations_report_lifecycle_states() { + assert_eq!( + RadrootsOperationalListingMutation::publish(canonical_draft()) + .lifecycle_state() + .expect("state"), + RadrootsOperationalListingLifecycleState::Published + ); + assert_eq!( + RadrootsOperationalListingMutation::update(canonical_draft()) + .lifecycle_state() + .expect("state"), + RadrootsOperationalListingLifecycleState::Published + ); + assert_eq!( + RadrootsOperationalListingMutation::save_draft(canonical_draft()) + .lifecycle_state() + .expect("state"), + RadrootsOperationalListingLifecycleState::Draft + ); + } + + #[test] + fn supported_mutations_expose_canonical_drafts() { + let publish = RadrootsOperationalListingMutation::publish(canonical_draft()); + let update = RadrootsOperationalListingMutation::update(canonical_draft()); + let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft()); + + assert_eq!( + publish + .canonical_draft() + .expect("draft") + .seller_pubkey() + .to_hex(), + SELLER + ); + assert_eq!( + update + .canonical_draft() + .expect("draft") + .seller_pubkey() + .to_hex(), + SELLER + ); + assert_eq!( + save_draft + .canonical_draft() + .expect("draft") + .seller_pubkey() + .to_hex(), + SELLER + ); + assert_eq!( + publish + .canonical_draft() + .expect("draft") + .listing() + .d_tag + .as_str(), + "AAAAAAAAAAAAAAAAAAAAAg" + ); + } + + #[test] + fn supported_mutations_report_listing_addresses() { + let publish = RadrootsOperationalListingMutation::publish(canonical_draft()); + let update = RadrootsOperationalListingMutation::update(canonical_draft()); + let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft()); + + assert_eq!( + publish.listing_addr().expect("address").as_str(), + format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!( + update.listing_addr().expect("address").as_str(), + format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + assert_eq!( + save_draft.listing_addr().expect("address").as_str(), + format!("{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg") + ); + } + + #[test] + fn archive_is_explicitly_unsupported() { + let archive = RadrootsOperationalListingMutation::archive( + ClassifiedListingAddress::parse(format!( + "{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" + )) + .expect("listing address"), + ); + + assert_eq!( + archive.lifecycle_state().unwrap_err(), + RadrootsOperationalListingMutationError::UnsupportedMutation + ); + assert_eq!( + archive.canonical_draft().unwrap_err(), + RadrootsOperationalListingMutationError::UnsupportedMutation + ); + assert_eq!( + archive.listing_addr().unwrap_err(), + RadrootsOperationalListingMutationError::UnsupportedMutation + ); + } + + #[test] + fn build_operational_listing_mutation_plan_maps_publish_and_update_to_published_listing() { + let publish = RadrootsOperationalListingMutation::publish(canonical_draft()); + let update = RadrootsOperationalListingMutation::update(canonical_draft()); + + let publish_draft = + build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft"); + let update_draft = + build_operational_listing_mutation_plan(&update, 1_700_000_000).expect("draft"); + + assert_eq!(publish_draft.body().kind(), KIND_CLASSIFIED_LISTING); + assert_eq!( + publish_draft.body().contract().contract_id().as_str(), + OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID + ); + assert_eq!(publish_draft.author().to_hex(), SELLER); + assert_eq!(publish_draft.created_at(), 1_700_000_000); + assert_eq!( + publish_draft.body().content(), + "# Coffee\n\nSingle origin coffee" + ); + assert_eq!(update_draft.body().kind(), KIND_CLASSIFIED_LISTING); + assert_eq!( + update_draft.body().contract().contract_id().as_str(), + OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID + ); + assert_eq!(update_draft.author().to_hex(), SELLER); + } + + #[test] + fn build_operational_listing_mutation_plan_rejects_save_draft() { + let save_draft = RadrootsOperationalListingMutation::save_draft(canonical_draft()); + + assert_eq!( + build_operational_listing_mutation_plan(&save_draft, 1_700_000_000).unwrap_err(), + RadrootsOperationalListingMutationError::UnsupportedMutation + ); + } + + #[test] + fn build_operational_listing_mutation_plan_rejects_archive() { + let archive = RadrootsOperationalListingMutation::archive( + ClassifiedListingAddress::parse(format!( + "{KIND_CLASSIFIED_LISTING}:{SELLER}:AAAAAAAAAAAAAAAAAAAAAg" + )) + .expect("listing address"), + ); + + assert_eq!( + build_operational_listing_mutation_plan(&archive, 1_700_000_000).unwrap_err(), + RadrootsOperationalListingMutationError::UnsupportedMutation + ); + } + + #[test] + fn build_operational_listing_mutation_plan_reports_encode_errors() { + let mut listing = listing(); + listing.resource_area = Some(ResourceAreaRef { + pubkey: SELLER.to_string(), + d_tag: "bad d tag".to_string(), + }); + let draft = RadrootsOperationalListingCanonicalEdit::new( + listing, + PublicKey::from_hex(SELLER).expect("seller"), + ) + .expect("canonical listing edit"); + let publish = RadrootsOperationalListingMutation::publish(draft); + + let err = build_operational_listing_mutation_plan(&publish, 1_700_000_000).unwrap_err(); + + assert!(matches!( + err, + RadrootsOperationalListingMutationError::EncodeListing(_) + )); + } + + #[test] + fn build_operational_listing_mutation_plan_event_id_is_stable_for_fixed_input() { + let publish = RadrootsOperationalListingMutation::publish(canonical_draft()); + + let first = + build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft"); + let second = + build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft"); + + assert_eq!(first.expected_event_id(), second.expected_event_id()); + assert_eq!(first.expected_event_id().to_hex().len(), 64); + assert_eq!(first.body().tags(), second.body().tags()); + assert_eq!(first.body().content(), second.body().content()); + } + + #[test] + fn build_operational_listing_mutation_plan_output_validates_as_operational_listing() { + let publish = RadrootsOperationalListingMutation::publish(canonical_draft()); + let draft = + build_operational_listing_mutation_plan(&publish, 1_700_000_000).expect("draft"); + + let signed = sign_plan(&draft); + validate_event_contract_shape(&signed, OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID) + .expect("operational listing contract"); + let verified = verify_nip01_event(signed).expect("verified listing"); + let validated = validate_operational_listing_event(&verified).expect("validated listing"); + + assert_eq!(validated.seller_pubkey, SELLER); + assert!( + validated + .listing_addr + .as_str() + .contains(&format!(":{SELLER}:")) + ); + } +} diff --git a/crates/sdk/src/listing/operational_listing/price_ext.rs b/crates/sdk/src/listing/operational_listing/price_ext.rs @@ -0,0 +1,176 @@ +use crate::listing::operational_listing::model::{ + RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal, +}; +use radroots_core::pricing::{Error as PricingError, QuantityPriceOps}; +use radroots_core::{Decimal, Quantity}; +use radroots_event::listing::operational::OperationalListingBin; + +pub trait BinPricingTryExt { + fn try_subtotal_for_count( + &self, + bin_count: u32, + ) -> Result<RadrootsOperationalListingSubtotal, PricingError>; + fn try_total_for_count( + &self, + bin_count: u32, + ) -> Result<RadrootsOperationalListingTotal, PricingError>; +} + +#[inline] +fn effective_quantity( + bin: &OperationalListingBin, + bin_count: u32, +) -> Result<Quantity, PricingError> { + let amount = bin + .quantity + .amount() + .checked_mul(Decimal::from(bin_count)) + .map_err(|_| PricingError::ArithmeticOverflow)?; + Quantity::try_new(amount, bin.quantity.unit()).map_err(|_| PricingError::ArithmeticOverflow) +} + +impl BinPricingTryExt for OperationalListingBin { + fn try_subtotal_for_count( + &self, + bin_count: u32, + ) -> Result<RadrootsOperationalListingSubtotal, PricingError> { + let effective_qty = effective_quantity(self, bin_count)?; + let money = self + .price_per_canonical_unit + .try_cost_for_rounded(&effective_qty)?; + let currency = money.currency(); + + Ok(RadrootsOperationalListingSubtotal { + price_amount: money, + price_currency: currency, + quantity_amount: effective_qty.amount(), + quantity_unit: effective_qty.unit(), + }) + } + + fn try_total_for_count( + &self, + bin_count: u32, + ) -> Result<RadrootsOperationalListingTotal, PricingError> { + let sub = self.try_subtotal_for_count(bin_count)?; + Ok(RadrootsOperationalListingTotal { + price_amount: sub.price_amount, + price_currency: sub.price_currency, + quantity_amount: sub.quantity_amount, + quantity_unit: sub.quantity_unit, + }) + } +} + +#[cfg(test)] +mod tests { + use super::BinPricingTryExt; + use radroots_core::pricing::Error as PricingError; + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::id::InventoryBinId; + use radroots_event::listing::operational::OperationalListingBin; + + fn bin_id(raw: &str) -> InventoryBinId { + InventoryBinId::parse(raw).expect("bin id") + } + + fn valid_bin() -> OperationalListingBin { + OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(2u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(5u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + } + } + + #[test] + fn try_subtotal_for_rejects_unit_mismatch() { + let bin = OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }; + + let err = bin.try_subtotal_for_count(1).unwrap_err(); + assert_eq!( + err, + PricingError::UnitMismatch { + have: Unit::MassG, + want: Unit::Each, + } + ); + } + + #[test] + fn subtotal_and_total_for_count_follow_effective_quantity() { + let bin = valid_bin(); + let subtotal = bin.try_subtotal_for_count(3).unwrap(); + let total = bin.try_total_for_count(3).unwrap(); + + assert_eq!(subtotal.quantity_amount, Decimal::from(6u32)); + assert_eq!(subtotal.quantity_unit, Unit::MassG); + assert_eq!(subtotal.price_amount.amount(), Decimal::from(30u32)); + assert_eq!(subtotal.price_currency, Currency::USD); + + assert_eq!(total.quantity_amount, subtotal.quantity_amount); + assert_eq!(total.quantity_unit, subtotal.quantity_unit); + assert_eq!(total.price_amount, subtotal.price_amount); + assert_eq!(total.price_currency, subtotal.price_currency); + } + + #[test] + fn try_subtotal_and_try_total_match() { + let bin = valid_bin(); + let subtotal = bin.try_subtotal_for_count(4).expect("subtotal"); + let total = bin.try_total_for_count(4).expect("total"); + + assert_eq!(subtotal.quantity_amount, Decimal::from(8u32)); + assert_eq!(subtotal.price_amount.amount(), Decimal::from(40u32)); + assert_eq!(total.quantity_amount, subtotal.quantity_amount); + assert_eq!(total.price_amount, subtotal.price_amount); + } + + #[test] + fn try_total_for_count_propagates_subtotal_errors() { + let bin = OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }; + + let err = bin.try_total_for_count(1).unwrap_err(); + assert_eq!( + err, + PricingError::UnitMismatch { + have: Unit::MassG, + want: Unit::Each, + } + ); + } +} diff --git a/crates/sdk/src/listing/operational_listing/validation.rs b/crates/sdk/src/listing/operational_listing/validation.rs @@ -0,0 +1,988 @@ +#![forbid(unsafe_code)] + +use std::{ + format, + string::{String, ToString}, +}; + +use radroots_core::{Decimal, Money, Quantity, Unit}; +use radroots_event::{ + envelope::kind::{KIND_CLASSIFIED_LISTING, is_classified_listing_kind}, + farm::location::{has_textual_locality, is_public_geohash5}, + id::ClassifiedListingAddress, + listing::classified::{ClassifiedListingPartition, classify_classified_listing_tags}, + listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingPublicLocation, + }, + trade::validation::OperationalListingValidationError, +}; +use radroots_identity::PublicKey; + +use radroots_event_codec::{ + decode::operational_listing::operational_listing_from_nostr_event, + verify::RadrootsSignatureVerifiedEvent, +}; + +#[derive(Clone, Debug)] +pub struct RadrootsOperationalListingTradeProjection { + pub listing_id: String, + pub listing_addr: ClassifiedListingAddress, + pub seller_pubkey: String, + pub title: String, + pub description: String, + pub product_type: String, + pub primary_bin_id: String, + pub bin_quantity: Quantity, + pub unit: Unit, + pub unit_price: Money, + pub inventory_available: Decimal, + pub availability: OperationalListingAvailability, + pub location: OperationalListingPublicLocation, + pub delivery_method: OperationalListingDeliveryMethod, + pub listing: OperationalListing, +} + +/// Validates a signature-verified Operational Listing event. +/// +/// A plain envelope cannot cross this boundary: +/// +/// ```compile_fail +/// use radroots_event::envelope::EventEnvelope; +/// use radroots_sdk::listing::validate_operational_listing_event; +/// +/// fn validate_unverified(event: &EventEnvelope) { +/// let _ = validate_operational_listing_event(event); +/// } +/// ``` +pub fn validate_operational_listing_event( + verified_event: &RadrootsSignatureVerifiedEvent, +) -> Result<RadrootsOperationalListingTradeProjection, OperationalListingValidationError> { + let event = verified_event.event(); + if !is_classified_listing_kind(event.kind_u32()) { + return Err(OperationalListingValidationError::InvalidKind { + kind: event.kind_u32(), + }); + } + if classify_classified_listing_tags(event.tags()) + != ClassifiedListingPartition::OperationalListing + { + return Err(OperationalListingValidationError::InvalidProfile); + } + + let listing = operational_listing_from_nostr_event(event) + .map_err(|error| OperationalListingValidationError::ParseError { error })?; + validate_operational_listing_model(listing, event.author()) +} + +/// Validates the trade semantics of an unsigned Operational Listing model. +/// +/// The seller is typed independently from the model because it is the +/// authority against which the listing's farm identity is checked. This +/// function does not perform event-kind, profile, decoding, or signature +/// checks; callers handling Nostr events must use +/// [`validate_operational_listing_event`] instead. +pub fn validate_operational_listing_model( + listing: OperationalListing, + seller_pubkey: &PublicKey, +) -> Result<RadrootsOperationalListingTradeProjection, OperationalListingValidationError> { + let listing_id = listing.d_tag.as_str().trim().to_string(); + + if listing.farm.pubkey != seller_pubkey.to_hex() { + return Err(OperationalListingValidationError::InvalidSeller); + } + let listing_addr_raw = format!("{KIND_CLASSIFIED_LISTING}:{}:{listing_id}", seller_pubkey); + let listing_addr = ClassifiedListingAddress::parse(&listing_addr_raw) + .expect("validated listing identity must form a listing address"); + + let title = listing.product.title.trim().to_string(); + if title.is_empty() { + return Err(OperationalListingValidationError::MissingTitle); + } + + let description = listing + .product + .summary + .as_ref() + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + if description.is_empty() { + return Err(OperationalListingValidationError::MissingDescription); + } + + let product_type = if !listing.product.category.trim().is_empty() { + listing.product.category.trim().to_string() + } else { + listing.product.key.trim().to_string() + }; + if product_type.is_empty() { + return Err(OperationalListingValidationError::MissingProductType); + } + + if listing.bins.is_empty() { + return Err(OperationalListingValidationError::MissingBins); + } + let primary_bin_id = listing.primary_bin_id.as_str().trim().to_string(); + let primary_bin_index = listing + .bins + .iter() + .position(|bin| bin.bin_id.as_str() == primary_bin_id) + .ok_or(OperationalListingValidationError::MissingPrimaryBin)?; + for (index, bin) in listing.bins.iter().enumerate() { + if listing.bins[..index] + .iter() + .any(|seen| seen.bin_id == bin.bin_id) + { + return Err(OperationalListingValidationError::InvalidBin); + } + } + let primary_bin = &listing.bins[primary_bin_index]; + + validate_listing_bin(primary_bin)?; + for (index, bin) in listing.bins.iter().enumerate() { + if index != primary_bin_index { + validate_listing_bin(bin)?; + } + } + + let inventory_available = listing + .inventory_available + .ok_or(OperationalListingValidationError::MissingInventory)?; + if inventory_available.is_sign_negative() { + return Err(OperationalListingValidationError::InvalidInventory); + } + + let availability = listing + .availability + .clone() + .ok_or(OperationalListingValidationError::MissingAvailability)?; + let location = listing + .location + .clone() + .ok_or(OperationalListingValidationError::MissingLocation)?; + if !has_textual_locality( + &location.primary, + location.city.as_deref(), + location.region.as_deref(), + location.country.as_deref(), + ) { + return Err(OperationalListingValidationError::MissingLocationLocality); + } + validate_listing_location_geohash(&location.geohash)?; + let delivery_method = listing + .delivery_method + .clone() + .ok_or(OperationalListingValidationError::MissingDeliveryMethod)?; + + Ok(RadrootsOperationalListingTradeProjection { + listing_id, + listing_addr, + seller_pubkey: seller_pubkey.to_hex(), + title, + description, + product_type, + primary_bin_id: primary_bin_id.clone(), + bin_quantity: primary_bin.quantity.clone(), + unit: primary_bin.quantity.unit(), + unit_price: primary_bin.price_per_canonical_unit.amount().clone(), + inventory_available, + availability, + location, + delivery_method, + listing, + }) +} + +fn validate_listing_bin( + bin: &OperationalListingBin, +) -> Result<(), OperationalListingValidationError> { + if bin.quantity.amount().is_sign_negative() || !bin.quantity.is_canonical() { + return Err(OperationalListingValidationError::InvalidBin); + } + if !bin.price_per_canonical_unit.is_price_per_canonical_unit() + || bin + .price_per_canonical_unit + .amount() + .amount() + .is_sign_negative() + || bin.price_per_canonical_unit.quantity().unit() != bin.quantity.unit() + { + return Err(OperationalListingValidationError::InvalidPrice); + } + Ok(()) +} + +fn validate_listing_location_geohash( + geohash: &str, +) -> Result<(), OperationalListingValidationError> { + if geohash.trim().is_empty() { + return Err(OperationalListingValidationError::MissingLocationGeohash); + } + if !is_public_geohash5(geohash) { + return Err(OperationalListingValidationError::InvalidLocationGeohash); + } + Ok(()) +} + +#[cfg(all(test, feature = "local-signing"))] +mod tests { + const FIXTURE_ALICE_SECRET_KEY_HEX: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; + const FIXTURE_ALICE_PUBLIC_KEY_HEX: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + const FIXTURE_BOB_SECRET_KEY_HEX: &str = + "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8"; + const FIXTURE_BOB_PUBLIC_KEY_HEX: &str = + "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; + use super::{ + OperationalListingValidationError, validate_operational_listing_event, + validate_operational_listing_model, + }; + use nostr::{EventBuilder, JsonUtil, Keys, Kind, Tag, Timestamp}; + use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; + use radroots_event::{ + envelope::EventEnvelope, + envelope::EventEnvelopeParts, + envelope::kind::KIND_CLASSIFIED_LISTING, + farm::FarmRef, + id::{DTag, InventoryBinId}, + listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingProduct, + OperationalListingPublicLocation, + }, + wire::Nip01EventWire, + }; + use radroots_event_codec::verify::{RadrootsSignatureVerifiedEvent, verify_nip01_event}; + use radroots_identity::PublicKey; + + const SELLER: &str = FIXTURE_ALICE_PUBLIC_KEY_HEX; + const OTHER_SELLER: &str = FIXTURE_BOB_PUBLIC_KEY_HEX; + + fn d_tag(raw: &str) -> DTag { + DTag::parse(raw).expect("d tag") + } + + fn bin_id(raw: &str) -> InventoryBinId { + InventoryBinId::parse(raw).expect("bin id") + } + + fn seller_pubkey() -> PublicKey { + PublicKey::from_hex(SELLER).expect("seller pubkey") + } + + fn other_seller_pubkey() -> PublicKey { + PublicKey::from_hex(OTHER_SELLER).expect("other seller pubkey") + } + + fn base_listing() -> OperationalListing { + OperationalListing { + d_tag: d_tag("AAAAAAAAAAAAAAAAAAAAAg"), + published_at: None, + farm: FarmRef { + pubkey: SELLER.into(), + d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), + }, + product: OperationalListingProduct { + key: "coffee".into(), + title: "Coffee".into(), + category: "coffee".into(), + summary: Some("Single origin coffee".into()), + process: None, + lot: None, + location: None, + profile: None, + year: None, + }, + primary_bin_id: bin_id("bin-1"), + bins: vec![OperationalListingBin { + bin_id: bin_id("bin-1"), + quantity: Quantity::try_new(Decimal::from(1000u32), Unit::MassG).unwrap(), + price_per_canonical_unit: QuantityPrice::try_new( + Money::try_new(Decimal::from(20u32), Currency::USD).unwrap(), + Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), + ) + .unwrap(), + display_amount: None, + display_unit: None, + display_label: None, + display_price: None, + display_price_unit: None, + }], + resource_area: None, + plot: None, + discounts: None, + inventory_available: Some(Decimal::from(5u32)), + availability: Some(OperationalListingAvailability::Status { + status: radroots_event::listing::operational::OperationalListingStatus::Active, + }), + delivery_method: Some(OperationalListingDeliveryMethod::Pickup), + location: Some(OperationalListingPublicLocation { + primary: "Farm".into(), + city: Some("Town".into()), + region: Some("Region".into()), + country: Some("US".into()), + geohash: "9q8yy".into(), + }), + images: None, + } + } + + fn base_event(listing: &OperationalListing) -> RadrootsSignatureVerifiedEvent { + let mut tags = vec![ + vec!["d".into(), listing.d_tag.to_string()], + vec!["p".into(), listing.farm.pubkey.clone()], + vec![ + "a".into(), + format!("30340:{}:{}", listing.farm.pubkey, listing.farm.d_tag), + ], + vec!["key".into(), listing.product.key.clone()], + vec!["title".into(), listing.product.title.clone()], + vec!["category".into(), listing.product.category.clone()], + vec![ + "summary".into(), + listing.product.summary.clone().unwrap_or_default(), + ], + vec![ + "radroots:primary_bin".into(), + listing.primary_bin_id.to_string(), + ], + ]; + for bin in &listing.bins { + tags.push(vec![ + "radroots:bin".into(), + bin.bin_id.to_string(), + bin.quantity.amount().to_string(), + bin.quantity.unit().code().to_string(), + ]); + tags.push(vec![ + "radroots:price".into(), + bin.bin_id.to_string(), + bin.price_per_canonical_unit.amount().amount().to_string(), + bin.price_per_canonical_unit + .amount() + .currency() + .as_str() + .to_string(), + bin.price_per_canonical_unit.quantity().amount().to_string(), + bin.price_per_canonical_unit + .quantity() + .unit() + .code() + .to_string(), + ]); + } + if let Some(inventory) = listing.inventory_available { + tags.push(vec!["inventory".into(), inventory.to_string()]); + } + if let Some(availability) = &listing.availability { + match availability { + OperationalListingAvailability::Status { status } => tags.push(vec![ + "status".into(), + match status { + radroots_event::listing::operational::OperationalListingStatus::Active => { + "active".into() + } + radroots_event::listing::operational::OperationalListingStatus::Sold => { + "sold".into() + } + radroots_event::listing::operational::OperationalListingStatus::Other { + value, + } => value.clone(), + }, + ]), + OperationalListingAvailability::Window { start, end } => { + if let Some(start) = start { + tags.push(vec![ + "radroots:availability_start".into(), + start.to_string(), + ]); + } + if let Some(end) = end { + tags.push(vec!["expires_at".into(), end.to_string()]); + } + } + } + } + if let Some(delivery) = &listing.delivery_method { + let mut tag = vec!["delivery".into()]; + match delivery { + OperationalListingDeliveryMethod::Pickup => tag.push("pickup".into()), + OperationalListingDeliveryMethod::LocalDelivery => { + tag.push("local_delivery".into()) + } + OperationalListingDeliveryMethod::Shipping => tag.push("shipping".into()), + OperationalListingDeliveryMethod::Other { method } => { + tag.push("other".into()); + tag.push(method.clone()); + } + } + tags.push(tag); + } + if let Some(location) = &listing.location { + tags.push(vec![ + "location".into(), + location.primary.clone(), + location.city.clone().unwrap_or_default(), + location.region.clone().unwrap_or_default(), + location.country.clone().unwrap_or_default(), + ]); + tags.push(vec!["g".into(), location.geohash.clone()]); + } + + event_with_parts(SELLER, KIND_CLASSIFIED_LISTING, tags, String::new()) + } + + fn event_with_parts( + author: &str, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsSignatureVerifiedEvent { + let secret = match author { + SELLER => FIXTURE_ALICE_SECRET_KEY_HEX, + OTHER_SELLER => FIXTURE_BOB_SECRET_KEY_HEX, + _ => panic!("test author must be an approved fixture identity"), + }; + let keys = Keys::parse(secret).expect("fixture signing key"); + let tags = tags + .into_iter() + .map(|tag| Tag::parse(tag).expect("test tag")) + .collect::<Vec<_>>(); + let event = EventBuilder::new( + Kind::Custom(u16::try_from(kind).expect("test kind")), + content, + ) + .tags(tags) + .allow_self_tagging() + .custom_created_at(Timestamp::from_secs(1)) + .sign_with_keys(&keys) + .expect("signed test event"); + let raw_json = event.as_json(); + let envelope = Nip01EventWire::parse_json(raw_json.as_str()) + .expect("canonical event wire") + .into_envelope() + .expect("event envelope"); + verify_nip01_event(envelope).expect("verified test event") + } + + fn assert_validation_err( + listing: OperationalListing, + expected: OperationalListingValidationError, + ) { + let event = base_event(&listing); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!(format!("{err}"), format!("{expected}")); + } + + fn assert_secondary_bin_model_error( + update: impl FnOnce(&mut OperationalListingBin), + expected: OperationalListingValidationError, + ) { + let mut listing = listing_with_secondary_bin(); + update(&mut listing.bins[1]); + + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("invalid secondary bin"), + expected + ); + } + + fn listing_with_secondary_bin() -> OperationalListing { + let mut listing = base_listing(); + let mut secondary_bin = listing.bins[0].clone(); + secondary_bin.bin_id = bin_id("bin-2"); + listing.bins.push(secondary_bin); + listing + } + + #[test] + fn validate_listing_ok() { + let listing = base_listing(); + let event = base_event(&listing); + assert!(validate_operational_listing_event(&event).is_ok()); + } + + #[test] + #[cfg(any())] + fn model_and_verified_event_validation_return_the_same_projection() { + let listing = listing_with_secondary_bin(); + let event_projection = + validate_operational_listing_event(&base_event(&listing)).expect("event projection"); + let model_projection = validate_operational_listing_model(listing, &seller_pubkey()) + .expect("model projection"); + + assert_eq!( + serde_json::to_value(model_projection).expect("model projection JSON"), + serde_json::to_value(event_projection).expect("event projection JSON") + ); + } + + #[test] + fn model_and_verified_event_validation_return_the_same_semantic_errors() { + let mut listing = base_listing(); + listing.inventory_available = None; + let event_error = validate_operational_listing_event(&base_event(&listing)) + .expect_err("event inventory error"); + let model_error = validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("model inventory error"); + assert_eq!(model_error, event_error); + + let listing = base_listing(); + let event = event_with_parts( + OTHER_SELLER, + KIND_CLASSIFIED_LISTING, + base_event(&listing).event().tags_as_vec(), + String::new(), + ); + let event_error = + validate_operational_listing_event(&event).expect_err("event seller error"); + let model_error = validate_operational_listing_model(listing, &other_seller_pubkey()) + .expect_err("model seller error"); + assert_eq!(model_error, event_error); + } + + #[test] + fn model_validation_reports_errors_before_event_encoding() { + let mut listing = base_listing(); + listing.bins.clear(); + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("missing bins"), + OperationalListingValidationError::MissingBins + ); + + let mut listing = base_listing(); + listing.primary_bin_id = bin_id("missing"); + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("missing primary bin"), + OperationalListingValidationError::MissingPrimaryBin + ); + + let mut listing = base_listing(); + listing.bins.push(listing.bins[0].clone()); + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("duplicate bin ID"), + OperationalListingValidationError::InvalidBin + ); + + let mut listing = base_listing(); + listing.location.as_mut().expect("location").geohash = " ".into(); + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("missing geohash"), + OperationalListingValidationError::MissingLocationGeohash + ); + + let mut listing = base_listing(); + listing.location.as_mut().expect("location").geohash = "9q8yyz".into(); + assert_eq!( + validate_operational_listing_model(listing, &seller_pubkey()) + .expect_err("invalid geohash"), + OperationalListingValidationError::InvalidLocationGeohash + ); + } + + #[test] + fn model_validation_rejects_invalid_secondary_bin_quantities() { + assert_secondary_bin_model_error( + |bin| bin.quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(), + OperationalListingValidationError::InvalidBin, + ); + } + + #[test] + fn model_validation_rejects_invalid_secondary_bin_prices() { + assert_secondary_bin_model_error( + |bin| { + bin.price_per_canonical_unit = QuantityPrice::try_new( + bin.price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::from(2u32), Unit::MassG).unwrap(), + ) + .unwrap(); + }, + OperationalListingValidationError::InvalidPrice, + ); + assert_secondary_bin_model_error( + |bin| { + bin.price_per_canonical_unit = QuantityPrice::try_new( + bin.price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(), + ) + .unwrap(); + }, + OperationalListingValidationError::InvalidPrice, + ); + assert_secondary_bin_model_error( + |bin| { + bin.price_per_canonical_unit = QuantityPrice::try_new( + bin.price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(), + ) + .unwrap(); + }, + OperationalListingValidationError::InvalidPrice, + ); + } + + #[test] + fn validate_listing_rejects_retired_kind() { + let listing = base_listing(); + let event = event_with_parts( + SELLER, + 30403, + base_event(&listing).event().tags_as_vec(), + String::new(), + ); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!( + err, + OperationalListingValidationError::InvalidKind { kind: 30403 } + ); + } + + #[test] + fn validate_listing_rejects_missing_d_tag() { + let event = event_with_parts(SELLER, KIND_CLASSIFIED_LISTING, Vec::new(), String::new()); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!(err, OperationalListingValidationError::InvalidProfile); + } + + #[test] + fn validate_listing_rejects_invalid_currency() { + let event = event_with_parts( + SELLER, + KIND_CLASSIFIED_LISTING, + vec![ + vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()], + vec!["p".into(), SELLER.into()], + vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")], + vec!["key".into(), "coffee".into()], + vec!["title".into(), "Coffee".into()], + vec!["category".into(), "coffee".into()], + vec!["summary".into(), "Single origin".into()], + vec!["radroots:primary_bin".into(), "bin-1".into()], + vec![ + "quantity".into(), + "1".into(), + "lb".into(), + "bag".into(), + "5".into(), + ], + vec![ + "price".into(), + "20".into(), + "US".into(), + "1".into(), + "lb".into(), + ], + vec![ + "location".into(), + "Farm".into(), + "Town".into(), + "Region".into(), + ], + vec!["status".into(), "active".into()], + vec!["delivery".into(), "pickup".into()], + ], + String::new(), + ); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert!(format!("{err:?}").starts_with("ParseError")); + } + + #[test] + fn validate_listing_rejects_mismatched_seller() { + let listing = base_listing(); + let event = event_with_parts( + OTHER_SELLER, + KIND_CLASSIFIED_LISTING, + base_event(&listing).event().tags_as_vec(), + String::new(), + ); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!(err, OperationalListingValidationError::InvalidSeller); + } + + #[test] + fn validate_listing_rejects_missing_inventory() { + let mut listing = base_listing(); + listing.inventory_available = None; + let event = base_event(&listing); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!(err, OperationalListingValidationError::MissingInventory); + } + + #[test] + fn validate_listing_rejects_invalid_kind() { + let listing = base_listing(); + let event = event_with_parts( + SELLER, + 0, + base_event(&listing).event().tags_as_vec(), + String::new(), + ); + let err = validate_operational_listing_event(&event).unwrap_err(); + assert_eq!( + err, + OperationalListingValidationError::InvalidKind { kind: 0 } + ); + } + + #[test] + fn validate_listing_rejects_missing_title() { + let mut listing = base_listing(); + listing.product.title = " ".into(); + assert_validation_err(listing, OperationalListingValidationError::MissingTitle); + } + + #[test] + fn tampered_envelope_cannot_reach_operational_validation() { + let verified = base_event(&base_listing()); + let event = verified.into_event(); + let tampered = EventEnvelope::new(EventEnvelopeParts { + id: event.id_hex(), + author: event.author().to_hex().to_owned(), + created_at: event.created_at_u64(), + kind: event.kind_u32(), + tags: event.tags_as_vec(), + content: "tampered".to_owned(), + sig: event.signature_hex(), + }) + .expect("well-shaped tampered envelope"); + + assert!(verify_nip01_event(tampered).is_err()); + } + + #[test] + fn validate_listing_rejects_missing_description() { + let mut listing = base_listing(); + listing.product.summary = Some(" ".into()); + assert_validation_err( + listing, + OperationalListingValidationError::MissingDescription, + ); + } + + #[test] + fn validate_listing_rejects_missing_product_type() { + let mut listing = base_listing(); + listing.product.category = " ".into(); + listing.product.key = " ".into(); + assert_validation_err( + listing, + OperationalListingValidationError::MissingProductType, + ); + } + + #[test] + fn validate_listing_rejects_missing_bins() { + let mut listing = base_listing(); + listing.bins.clear(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "radroots:primary_bin".into(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_missing_primary_bin_id() { + assert!(InventoryBinId::parse(" ").is_err()); + } + + #[test] + fn validate_listing_rejects_primary_bin_not_found() { + let mut listing = base_listing(); + listing.primary_bin_id = bin_id("missing"); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "radroots:primary_bin".into(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_non_canonical_quantity() { + let mut listing = base_listing(); + listing.bins[0].quantity = Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "radroots:bin".into(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_non_canonical_price_quantity() { + let mut listing = base_listing(); + listing.bins[0].price_per_canonical_unit = QuantityPrice::try_new( + listing.bins[0].price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::ONE, Unit::MassKg).unwrap(), + ) + .unwrap(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "radroots:price".into(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_price_unit_mismatch() { + let mut listing = base_listing(); + listing.bins[0].price_per_canonical_unit = QuantityPrice::try_new( + listing.bins[0].price_per_canonical_unit.amount().clone(), + Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(), + ) + .unwrap(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "radroots:price".into(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_negative_inventory() { + let mut listing = base_listing(); + listing.inventory_available = Some("-1".parse().unwrap()); + assert_validation_err(listing, OperationalListingValidationError::InvalidInventory); + } + + #[test] + fn validate_listing_rejects_missing_availability() { + let mut listing = base_listing(); + listing.availability = None; + assert_validation_err( + listing, + OperationalListingValidationError::MissingAvailability, + ); + } + + #[test] + fn validate_listing_rejects_missing_location() { + let mut listing = base_listing(); + listing.location = None; + assert_validation_err(listing, OperationalListingValidationError::MissingLocation); + } + + #[test] + fn validate_listing_rejects_missing_location_locality() { + let mut listing = base_listing(); + let location = listing.location.as_mut().expect("location"); + location.city = None; + location.region = None; + location.country = None; + assert_validation_err( + listing, + OperationalListingValidationError::MissingLocationLocality, + ); + } + + #[test] + fn validate_listing_rejects_missing_location_geohash() { + let mut listing = base_listing(); + listing.location.as_mut().expect("location").geohash = " ".into(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "g".to_string(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_invalid_location_geohash() { + let mut listing = base_listing(); + listing.location.as_mut().expect("location").geohash = "9q8yyz".into(); + assert_validation_err( + listing, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "g".to_string(), + ), + }, + ); + } + + #[test] + fn validate_listing_rejects_missing_delivery_method() { + let mut listing = base_listing(); + listing.delivery_method = None; + assert_validation_err( + listing, + OperationalListingValidationError::MissingDeliveryMethod, + ); + } + + #[test] + fn validation_error_display_covers_all_variants() { + let errors = vec![ + OperationalListingValidationError::InvalidKind { kind: 9 }, + OperationalListingValidationError::InvalidProfile, + OperationalListingValidationError::MissingListingId, + OperationalListingValidationError::ListingEventNotFound { + listing_addr: "addr".into(), + }, + OperationalListingValidationError::ListingEventFetchFailed { + listing_addr: "addr".into(), + }, + OperationalListingValidationError::ParseError { + error: + radroots_event::listing::operational::OperationalListingParseError::InvalidTag( + "d".into(), + ), + }, + OperationalListingValidationError::InvalidSeller, + OperationalListingValidationError::MissingFarmProfile, + OperationalListingValidationError::MissingFarmRecord, + OperationalListingValidationError::MissingTitle, + OperationalListingValidationError::MissingDescription, + OperationalListingValidationError::MissingProductType, + OperationalListingValidationError::MissingBins, + OperationalListingValidationError::MissingPrimaryBin, + OperationalListingValidationError::InvalidBin, + OperationalListingValidationError::MissingPrice, + OperationalListingValidationError::InvalidPrice, + OperationalListingValidationError::MissingInventory, + OperationalListingValidationError::InvalidInventory, + OperationalListingValidationError::MissingAvailability, + OperationalListingValidationError::MissingLocation, + OperationalListingValidationError::MissingLocationLocality, + OperationalListingValidationError::MissingLocationGeohash, + OperationalListingValidationError::InvalidLocationGeohash, + OperationalListingValidationError::MissingDeliveryMethod, + ]; + for error in errors { + assert!(!error.to_string().trim().is_empty()); + } + } +} diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs @@ -0,0 +1,532 @@ +//! Generic signer composition without protocol or relay ownership. + +use std::sync::Arc; +#[cfg(feature = "local-signing")] +use std::sync::RwLock; + +use radroots_signing::{SignReceipt, SignRequest, Signer, SignerStatus}; + +/// Host-visible signer composition mode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Mode { + /// A concrete local Nostr adapter owns opaque key material. + Local, + /// A host-provided signer drives NIP-46 protocol and relay execution. + Nip46, + /// Another host-provided implementation of the generic signer SPI. + Host, +} + +/// Cloneable SDK composition wrapper around the canonical signer SPI. +#[derive(Clone)] +pub struct Provider { + mode: Mode, + signer: Arc<dyn Signer>, + #[cfg(feature = "local-signing")] + slot: Option<Slot>, +} + +impl Provider { + /// Wraps any host-provided canonical signer implementation. + #[must_use] + pub fn host(signer: Arc<dyn Signer>) -> Self { + Self { + mode: Mode::Host, + signer, + #[cfg(feature = "local-signing")] + slot: None, + } + } + + /// Wraps the concrete local Nostr adapter without exposing its key. + #[cfg(feature = "local-signing")] + #[must_use] + pub fn local(signer: radroots_nostr::signing::LocalSigner) -> Self { + Self { + mode: Mode::Local, + signer: Arc::new(signer), + slot: None, + } + } + + /// Wraps a host-controlled local signer slot. + /// + /// The slot starts inert and can be populated or cleared without rebuilding + /// the client. Secret persistence remains entirely host-owned. + #[cfg(feature = "local-signing")] + #[must_use] + pub fn slot(slot: Slot) -> Self { + Self { + mode: Mode::Local, + signer: Arc::new(slot.clone()), + slot: Some(slot), + } + } + + /// Marks a host-provided canonical signer as a NIP-46 composition. + /// + /// `radroots_nostr_connect` owns protocol state and its transport SPI. The + /// injected implementation owns that client plus explicit relay execution; + /// this wrapper does not contact a relay, persist a session, or start work. + #[cfg(feature = "nip46")] + #[must_use] + pub fn nip46(signer: Arc<dyn Signer>) -> Self { + Self { + mode: Mode::Nip46, + signer, + #[cfg(feature = "local-signing")] + slot: None, + } + } + + /// Returns the explicit composition mode. + #[must_use] + pub const fn mode(&self) -> Mode { + self.mode + } + + /// Borrows the canonical signer SPI. + #[must_use] + pub fn as_signer(&self) -> &dyn Signer { + self.signer.as_ref() + } + + /// Reports canonical status without initiating a signing request. + pub async fn status(&self) -> Result<SignerStatus, radroots_signing::Error> { + self.signer.status().await + } + + /// Delegates one already-authorized request to the canonical SPI. + pub async fn sign(&self, request: SignRequest) -> Result<SignReceipt, radroots_signing::Error> { + self.signer.sign(request).await + } + + #[cfg(feature = "local-signing")] + pub(crate) fn into_parts(self) -> (Arc<dyn Signer>, Option<Slot>) { + (self.signer, self.slot) + } + + #[cfg(not(feature = "local-signing"))] + pub(crate) fn into_signer(self) -> Arc<dyn Signer> { + self.signer + } +} + +/// Public identity controlled by an installed local signer. +#[cfg(feature = "local-signing")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LocalIdentity { + public_key: radroots_identity::PublicKey, + npub: String, +} + +#[cfg(feature = "local-signing")] +impl LocalIdentity { + fn from_public_key( + public_key: radroots_identity::PublicKey, + ) -> Result<Self, radroots_nostr::Error> { + Ok(Self { + public_key, + npub: radroots_nostr::key::public_key_to_npub(public_key)?, + }) + } + + /// Returns the canonical lowercase public-key hexadecimal form. + #[must_use] + pub fn public_key_hex(&self) -> String { + self.public_key.to_hex() + } + + /// Returns the canonical NIP-19 public identity. + #[must_use] + pub fn npub(&self) -> &str { + self.npub.as_str() + } + + #[cfg(any(test, all(feature = "sync", feature = "nostr")))] + pub(crate) const fn public_key(&self) -> radroots_identity::PublicKey { + self.public_key + } +} + +/// Mutable, client-shareable local signer selected by the host. +/// +/// The slot never persists key material and its debug output never observes +/// the installed signer. Installing and clearing are explicit host actions. +#[cfg(feature = "local-signing")] +#[derive(Clone, Default)] +pub struct Slot { + state: Arc<RwLock<Option<SlotState>>>, +} + +#[cfg(feature = "local-signing")] +struct SlotState { + signer: Arc<dyn Signer>, + identity: LocalIdentity, +} + +#[cfg(feature = "local-signing")] +impl Slot { + /// Creates an empty signer slot. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Validates and installs one host-supplied hexadecimal or `nsec` secret. + pub fn install(&self, encoded: &str) -> Result<LocalIdentity, radroots_nostr::Error> { + let secret = radroots_nostr::key::SecretKey::parse(encoded)?; + self.install_secret(secret) + } + + /// Generates, installs, and returns one secret for immediate host custody. + /// + /// The SDK retains only the opaque signer. The returned `nsec` is the sole + /// persistence handoff and must be moved into host secure storage. + pub fn generate(&self) -> Result<(String, LocalIdentity), radroots_nostr::Error> { + let secret = radroots_nostr::key::SecretKey::generate(); + let encoded = radroots_nostr::key::secret_key_to_nsec(&secret); + let identity = self.install_secret(secret)?; + Ok((encoded, identity)) + } + + /// Removes the active signer from this process. + pub fn clear(&self) { + if let Ok(mut state) = self.state.write() { + *state = None; + } + } + + /// Returns the currently installed public identity. + #[must_use] + pub fn identity(&self) -> Option<LocalIdentity> { + self.state + .read() + .ok() + .and_then(|state| state.as_ref().map(|state| state.identity.clone())) + } + + fn install_secret( + &self, + secret: radroots_nostr::key::SecretKey, + ) -> Result<LocalIdentity, radroots_nostr::Error> { + let public_key = secret.public_key()?; + let identity = LocalIdentity::from_public_key(public_key)?; + let signer: Arc<dyn Signer> = Arc::new(radroots_nostr::signing::LocalSigner::new(secret)?); + if let Ok(mut state) = self.state.write() { + *state = Some(SlotState { + signer, + identity: identity.clone(), + }); + } + Ok(identity) + } + + fn signer(&self) -> Result<Arc<dyn Signer>, radroots_signing::Error> { + self.state + .read() + .map_err(|source| { + radroots_signing::Error::with_source( + radroots_signing::error::Kind::InternalError, + LockFailure(source.to_string()), + ) + })? + .as_ref() + .map(|state| Arc::clone(&state.signer)) + .ok_or_else(|| { + radroots_signing::Error::new(radroots_signing::error::Kind::SignerUnavailable) + }) + } +} + +#[cfg(feature = "local-signing")] +impl Signer for Slot { + fn status( + &self, + ) -> radroots_signing::signer::BoxFuture<'_, Result<SignerStatus, radroots_signing::Error>> + { + Box::pin(async move { + match self.signer() { + Ok(signer) => signer.status().await, + Err(error) if error.kind() == radroots_signing::error::Kind::SignerUnavailable => { + Ok(SignerStatus::unavailable()) + } + Err(error) => Err(error), + } + }) + } + + fn sign( + &self, + request: SignRequest, + ) -> radroots_signing::signer::BoxFuture<'_, Result<SignReceipt, radroots_signing::Error>> { + Box::pin(async move { self.signer()?.sign(request).await }) + } +} + +#[cfg(feature = "local-signing")] +impl std::fmt::Debug for Slot { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Slot") + .field("installed", &self.identity().is_some()) + .finish() + } +} + +#[cfg(feature = "local-signing")] +#[derive(Debug)] +struct LockFailure(String); + +#[cfg(feature = "local-signing")] +impl std::fmt::Display for LockFailure { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(self.0.as_str()) + } +} + +#[cfg(feature = "local-signing")] +impl std::error::Error for LockFailure {} + +impl std::fmt::Debug for Provider { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Provider") + .field("mode", &self.mode) + .field("signer", &"<opaque>") + .finish() + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use radroots_event::{GenericEventDraft, contract::AuthorRole}; + use radroots_event_codec::authoring::AuthoredEventPlan; + use radroots_identity::PublicKey; + use radroots_protocol::runtime::v1::OperationId; + use radroots_signing::{ + Actor, AuthoredArtifactId, Error, SignReceipt, SignRequest, SignerStatus, SigningIntentId, + SigningOperationId, + actor::ActorSource, + error::Kind, + request::{CancellationPolicy, SignPolicy}, + signer::BoxFuture, + }; + #[cfg(any(feature = "local-signing", feature = "nip46"))] + use radroots_signing::{capability::SignerKind, status::SignerAvailability}; + #[cfg(feature = "nip46")] + use radroots_signing::{ + capability::{CancellationSupport, SignerCapability}, + recovery::ReplayCapability, + status::{AuthChallenge, SignProgress}, + }; + + use super::*; + + const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + struct ScriptedSigner { + status: SignerStatus, + result: Kind, + polls: Arc<AtomicUsize>, + } + + impl Signer for ScriptedSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, Error>> { + let status = self.status.clone(); + Box::pin(async move { Ok(status) }) + } + + fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> { + let result = self.result; + let polls = Arc::clone(&self.polls); + Box::pin(async move { + polls.fetch_add(1, Ordering::Relaxed); + Err(Error::new(result)) + }) + } + } + + fn request() -> SignRequest { + let actor = Actor::new( + PublicKey::from_hex(PUBLIC_KEY).expect("public key"), + ActorSource::ExplicitPublicKey, + [AuthorRole::Any], + ) + .expect("actor"); + let plan = AuthoredEventPlan::from_generic( + GenericEventDraft::new( + "radroots.social.geochat.v1", + 20_000, + 1_700_000_000, + Vec::new(), + "frozen-content", + PUBLIC_KEY, + ) + .expect("draft"), + ) + .expect("authored plan"); + SignRequest::new( + OperationId::SyncPush, + SigningIntentId::new( + SigningOperationId::new([1; 16]).expect("operation id"), + AuthoredArtifactId::new([2; 16]).expect("artifact id"), + ), + actor, + plan, + SignPolicy::new(1_700_000_100, CancellationPolicy::PreservePublishedRequest) + .expect("policy"), + ) + .expect("request") + } + + #[cfg(feature = "nip46")] + fn remote_status(progress: Option<SignProgress>) -> SignerStatus { + SignerStatus::new( + SignerAvailability::AwaitingAuthentication, + vec![SignerCapability::new( + SignerKind::Remote, + ReplayCapability::ExactReplayByRequestId, + CancellationSupport::BeforeAndAfterPublication, + true, + true, + )], + progress, + ) + } + + #[cfg(feature = "local-signing")] + #[tokio::test] + async fn local_provider_uses_the_concrete_lower_adapter() { + let local = radroots_nostr::signing::LocalSigner::generate().expect("local signer"); + let provider = Provider::local(local); + let status = provider.status().await.expect("status"); + assert_eq!(provider.mode(), Mode::Local); + assert_eq!(status.availability(), SignerAvailability::Ready); + assert_eq!(status.capabilities()[0].kind(), SignerKind::Local); + } + + #[cfg(feature = "local-signing")] + #[tokio::test] + async fn local_slot_hands_secret_to_host_and_supports_lock_restore() { + let slot = Slot::new(); + assert!(slot.identity().is_none()); + assert_eq!( + slot.status().await.expect("empty status").availability(), + SignerAvailability::Unavailable + ); + + let (secret, generated) = slot.generate().expect("generated identity"); + assert!(secret.starts_with("nsec1")); + assert_eq!(slot.identity().expect("installed"), generated); + assert!(!format!("{slot:?}").contains(secret.as_str())); + + slot.clear(); + assert!(slot.identity().is_none()); + let restored = slot.install(secret.as_str()).expect("restored identity"); + assert_eq!(restored, generated); + assert_eq!(restored.public_key_hex(), restored.public_key().to_hex()); + assert!(restored.npub().starts_with("npub1")); + + let provider = Provider::slot(slot.clone()); + assert_eq!(provider.mode(), Mode::Local); + assert!(format!("{provider:?}").contains("<opaque>")); + let (_signer, provider_slot) = provider.into_parts(); + assert!(provider_slot.is_some()); + + slot.clear(); + assert_eq!( + slot.sign(request()).await.expect_err("empty slot").kind(), + Kind::SignerUnavailable + ); + } + + #[cfg(feature = "local-signing")] + #[tokio::test] + async fn poisoned_local_slot_fails_closed_without_exposing_key_state() { + let slot = Slot::new(); + let state = Arc::clone(&slot.state); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _guard = state.write().expect("write lock"); + panic!("poison signer slot"); + })); + + slot.clear(); + assert!(slot.identity().is_none()); + let (_secret, identity) = slot.generate().expect("secret remains host-owned"); + assert!(!identity.public_key_hex().is_empty()); + assert_eq!( + slot.sign(request()) + .await + .expect_err("poisoned slot") + .kind(), + Kind::InternalError + ); + } + + #[cfg(feature = "nip46")] + #[tokio::test] + async fn nip46_provider_preserves_auth_challenge_and_capabilities() { + let challenge = AuthChallenge::new( + "https://signer.example/approve", + 1_700_000_000, + Some(1_700_000_100), + ) + .expect("challenge"); + let signer = ScriptedSigner { + status: remote_status(Some(SignProgress::authentication(challenge))), + result: Kind::SignerRejected, + polls: Arc::new(AtomicUsize::new(0)), + }; + let provider = Provider::nip46(Arc::new(signer)); + let status = provider.status().await.expect("status"); + assert_eq!(provider.mode(), Mode::Nip46); + assert_eq!( + status.availability(), + SignerAvailability::AwaitingAuthentication + ); + assert!(status.progress().expect("progress").challenge().is_some()); + assert!(status.capabilities()[0].may_require_authentication()); + } + + #[tokio::test] + async fn canonical_errors_preserve_timeout_drift_and_cancellation() { + for expected in [ + Kind::SignerTimeout, + Kind::SignerOutputInvalid, + Kind::SignerCancelled, + ] { + let provider = Provider::host(Arc::new(ScriptedSigner { + status: SignerStatus::unavailable(), + result: expected, + polls: Arc::new(AtomicUsize::new(0)), + })); + assert_eq!( + provider.sign(request()).await.expect_err("failure").kind(), + expected + ); + assert_eq!( + provider.as_signer().status().await.expect("status"), + SignerStatus::unavailable() + ); + } + } + + #[test] + fn dropping_unpolled_signing_future_has_no_effect() { + let polls = Arc::new(AtomicUsize::new(0)); + let provider = Provider::host(Arc::new(ScriptedSigner { + status: SignerStatus::unavailable(), + result: Kind::SignerCancelled, + polls: Arc::clone(&polls), + })); + drop(provider.sign(request())); + assert_eq!(polls.load(Ordering::Relaxed), 0); + } +} diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs @@ -0,0 +1,259 @@ +//! Canonical storage capability composition. + +/// Backend-neutral storage status owned by `radroots_storage`. +pub type Status = radroots_storage::StorageStatus; + +/// Backend-neutral integrity status owned by `radroots_storage`. +pub type IntegrityStatus = radroots_storage::status::IntegrityStatus; + +/// Validated SQLite open configuration; this contains no connection or pool. +#[cfg(feature = "sqlite")] +pub type SqliteOptions = radroots_storage_sqlite::OpenOptions; + +/// Explicit SQLite lifecycle mode. +#[cfg(feature = "sqlite")] +pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode; + +/// Validated SQLite-owned paths; this contains no backend handle. +#[cfg(feature = "sqlite")] +pub type SqlitePaths = radroots_storage_sqlite::Paths; + +use radroots_storage::backup::{ + BackupId, BackupOperation, BackupPlan, BackupTransition, ReliabilityRevision, RestoreOperation, + RestorePlan, RestoreTransition, StorageReliability, +}; + +/// Borrowed reliability operations over the canonical backend-neutral SPI. +#[derive(Clone, Copy)] +pub struct Operations<'a> { + storage: &'a dyn radroots_storage::Storage, +} + +impl<'a> Operations<'a> { + pub(crate) const fn new(storage: &'a dyn radroots_storage::Storage) -> Self { + Self { storage } + } + + /// Begins or resumes one idempotent backup plan. + pub async fn begin_backup( + &self, + plan: BackupPlan, + ) -> Result<BackupOperation, radroots_storage::Error> { + StorageReliability::begin_backup(self.storage, plan).await + } + + /// Applies one optimistic backup transition. + pub async fn transition_backup( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: BackupTransition, + at_unix_ms: u64, + ) -> Result<BackupOperation, radroots_storage::Error> { + StorageReliability::transition_backup( + self.storage, + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + .await + } + + /// Begins or resumes one staged restore plan. + pub async fn begin_restore( + &self, + plan: RestorePlan, + ) -> Result<RestoreOperation, radroots_storage::Error> { + StorageReliability::begin_restore(self.storage, plan).await + } + + /// Applies one optimistic staged restore transition. + pub async fn transition_restore( + &self, + backup_id: BackupId, + expected_revision: ReliabilityRevision, + transition: RestoreTransition, + at_unix_ms: u64, + ) -> Result<RestoreOperation, radroots_storage::Error> { + StorageReliability::transition_restore( + self.storage, + backup_id, + expected_revision, + transition, + at_unix_ms, + ) + .await + } + + /// Returns passive backend status without initiating recovery work. + pub async fn status(&self) -> Result<Status, radroots_storage::Error> { + StorageReliability::status(self.storage).await + } + + /// Runs backend-owned integrity inspection. + pub async fn integrity(&self) -> Result<IntegrityStatus, radroots_storage::Error> { + StorageReliability::integrity(self.storage).await + } +} + +impl std::fmt::Debug for Operations<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Operations") + .field("storage", &"<borrowed canonical storage>") + .finish() + } +} + +#[cfg(all(test, feature = "memory"))] +mod tests { + use std::sync::Arc; + + use radroots_storage::{ + backup::{ + BackupFormatVersion, BackupManifest, BackupMember, BackupMemberKind, + BackupSecretPolicy, BackupStage, MemberDigest, MemberVerification, RestoreMemberStatus, + RestoreStage, + }, + event::SourceGeneration, + memory::MemoryStorage, + status::IntegrityHealth, + }; + + use crate::ClientBuilder; + + use super::*; + + fn manifest(backup_id: BackupId) -> BackupManifest { + BackupManifest::new( + BackupFormatVersion::V1, + backup_id, + 1_800_000_000_100, + BackupSecretPolicy::ExcludeProtectedStorage, + vec![ + BackupMember::new( + "runtime/events.bin", + BackupMemberKind::Runtime, + 16, + MemberDigest::new([3; 32]), + ) + .expect("member"), + ], + ) + .expect("manifest") + } + + #[tokio::test] + async fn memory_reliability_preserves_staging_interruption_integrity_and_native_states() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([8; 32]).expect("generation"), + )); + let client = ClientBuilder::new() + .storage(storage) + .build() + .expect("client"); + let operations = client.storage_operations().expect("operations"); + let backup_id = BackupId::new([9; 16]).expect("backup id"); + let plan = BackupPlan::new( + backup_id, + BackupFormatVersion::V1, + BackupSecretPolicy::ExcludeProtectedStorage, + 1_800_000_000_000, + ) + .expect("plan"); + + drop(operations.begin_backup(plan.clone())); + let planned = operations.begin_backup(plan).await.expect("planned"); + assert_eq!(planned.stage(), BackupStage::Planned); + let captured = operations + .transition_backup( + backup_id, + planned.revision(), + BackupTransition::Captured(manifest(backup_id)), + 1_800_000_000_200, + ) + .await + .expect("captured"); + let verified = operations + .transition_backup( + backup_id, + captured.revision(), + BackupTransition::Verified, + 1_800_000_000_300, + ) + .await + .expect("verified"); + let finalized = operations + .transition_backup( + backup_id, + verified.revision(), + BackupTransition::Finalize, + 1_800_000_000_400, + ) + .await + .expect("finalized"); + assert_eq!(finalized.stage(), BackupStage::Finalized); + + let restore_plan = RestorePlan::new( + manifest(backup_id), + BackupSecretPolicy::ExcludeProtectedStorage, + 1_800_000_001_000, + ) + .expect("restore plan"); + let staging = operations + .begin_restore(restore_plan.clone()) + .await + .expect("staging"); + assert_eq!(staging.stage(), RestoreStage::Staging); + let replayed = operations + .begin_restore(restore_plan) + .await + .expect("resume staging"); + assert_eq!(replayed, staging); + let verifying = operations + .transition_restore( + backup_id, + staging.revision(), + RestoreTransition::Staged, + 1_800_000_001_100, + ) + .await + .expect("verifying"); + let finalizing = operations + .transition_restore( + backup_id, + verifying.revision(), + RestoreTransition::Verified(vec![ + RestoreMemberStatus::new("runtime/events.bin", MemberVerification::Verified) + .expect("member status"), + ]), + 1_800_000_001_200, + ) + .await + .expect("finalizing"); + let restored = operations + .transition_restore( + backup_id, + finalizing.revision(), + RestoreTransition::Finalize, + 1_800_000_001_300, + ) + .await + .expect("restored"); + assert_eq!(restored.stage(), RestoreStage::Finalized); + assert_eq!( + operations.integrity().await.expect("integrity").health(), + IntegrityHealth::Healthy + ); + assert_eq!( + operations + .status() + .await + .expect("status") + .integrity() + .health(), + IntegrityHealth::Healthy + ); + } +} diff --git a/crates/sdk/src/sync.rs b/crates/sdk/src/sync.rs @@ -0,0 +1,520 @@ +//! Client-scoped access to the canonical synchronization engine. + +#[cfg(feature = "sync")] +use std::sync::Arc; + +#[cfg(feature = "sync")] +use radroots_storage::{authored_delivery::AuthoredDeliveryPlan, projection::ProjectionId}; +#[cfg(feature = "sync")] +use radroots_sync::{ + Engine, PullReceipt, PullRequest, PushRequest, SyncStatus, + ingest::{AdmissionPolicy, IngestBatchReceipt, IngestReceipt}, + policy::Error, + projection::{Reducer, RefreshReceipt, RefreshRequest}, + push::{ + AdmissionRunReceipt, DeliveryExecutionReceipt, PushPreparation, PushStatus, + SigningRunReceipt, + }, +}; +#[cfg(feature = "sync")] +use radroots_transport::source::ObservedEvent; + +/// Explicit host policy for SDK-composed synchronization. +/// +/// Selecting this policy opts into the system clock and operating-system +/// randomness for operation IDs. It creates no executor, timer, or worker. +#[cfg(feature = "sync")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct HostPolicy { + deadlines: radroots_sync::policy::DeadlinePolicy, +} + +#[cfg(feature = "sync")] +impl HostPolicy { + /// Creates a bounded policy for pull, signing, and delivery calls. + pub fn new( + pull_timeout_ms: u64, + sign_timeout_ms: u64, + delivery_timeout_ms: u64, + ) -> Result<Self, radroots_sync::policy::Error> { + Ok(Self { + deadlines: radroots_sync::policy::DeadlinePolicy::new( + pull_timeout_ms, + sign_timeout_ms, + delivery_timeout_ms, + )?, + }) + } + + /// Returns the ordinary bounded native-host policy. + #[must_use] + pub fn standard() -> Self { + Self::new(30_000, 30_000, 30_000).expect("static host deadlines are valid") + } + + pub(crate) fn composition( + self, + ) -> ( + Arc<dyn radroots_sync::policy::Clock>, + Arc<dyn radroots_sync::policy::IdSource>, + radroots_sync::policy::DeadlinePolicy, + ) { + (Arc::new(SystemClock), Arc::new(RandomIds), self.deadlines) + } +} + +#[cfg(feature = "sync")] +impl Default for HostPolicy { + fn default() -> Self { + Self::standard() + } +} + +#[cfg(feature = "sync")] +struct SystemClock; + +#[cfg(feature = "sync")] +impl radroots_sync::policy::Clock for SystemClock { + fn now_unix_ms(&self) -> Result<u64, radroots_sync::policy::Error> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|duration| u64::try_from(duration.as_millis()).ok()) + .filter(|value| *value != 0) + .ok_or(radroots_sync::policy::Error::ClockUnavailable) + } +} + +#[cfg(feature = "sync")] +struct RandomIds; + +#[cfg(feature = "sync")] +impl radroots_sync::policy::IdSource for RandomIds { + fn next_id( + &self, + _operation: radroots_sync::policy::OperationKind, + ) -> Result<radroots_sync::policy::SyncId, radroots_sync::policy::Error> { + radroots_sync::policy::SyncId::new(*uuid::Uuid::new_v4().as_bytes()) + } +} + +/// Borrowed client operations over one explicitly composed sync engine. +/// +/// This type owns no scheduling, retries, status strings, outbox state, or +/// projection state. Every method delegates once to the canonical engine and +/// returns its native receipt or error. +#[cfg(feature = "sync")] +#[derive(Clone, Copy)] +pub struct Operations<'a> { + engine: &'a Engine, +} + +#[cfg(feature = "sync")] +impl<'a> Operations<'a> { + pub(crate) const fn new(engine: &'a Engine) -> Self { + Self { engine } + } + + /// Runs one caller-bounded pull and canonical ingest sequence. + pub async fn pull( + &self, + request: PullRequest, + admission: &dyn AdmissionPolicy, + ) -> Result<PullReceipt, Error> { + self.engine.pull(request, admission).await + } + + /// Verifies and atomically ingests one observed event. + pub async fn ingest( + &self, + observed: ObservedEvent, + admission: &dyn AdmissionPolicy, + ) -> Result<IngestReceipt, Error> { + self.engine.ingest(observed, admission).await + } + + /// Ingests a bounded caller-owned batch while preserving partial outcomes. + pub async fn ingest_batch( + &self, + observed: Vec<ObservedEvent>, + admission: &dyn AdmissionPolicy, + ) -> IngestBatchReceipt { + self.engine.ingest_batch(observed, admission).await + } + + /// Runs one bounded projection refresh through its owning reducer. + pub async fn refresh_projection( + &self, + request: RefreshRequest, + reducer: &dyn Reducer, + ) -> Result<RefreshReceipt, Error> { + self.engine.refresh_projection(request, reducer).await + } + + /// Atomically persists one complete authored operation before any side effect. + pub async fn prepare_push(&self, request: PushRequest) -> Result<PushPreparation, Error> { + self.engine.prepare_push(request).await + } + + /// Returns the complete durable state of one prepared push operation. + pub async fn push_status( + &self, + operation_id: radroots_sync::policy::SyncId, + ) -> Result<Option<PushStatus>, Error> { + self.engine.push_status(operation_id).await + } + + /// Runs one bounded signing phase for an exactly prepared operation. + pub async fn sign_prepared(&self, request: PushRequest) -> Result<SigningRunReceipt, Error> { + self.engine.sign_prepared(request).await + } + + /// Runs one bounded local-admission phase for a durably signed artifact. + pub async fn admit_signed( + &self, + operation_id: radroots_sync::policy::SyncId, + ) -> Result<AdmissionRunReceipt, Error> { + self.engine.admit_signed(operation_id).await + } + + /// Prepares, signs, and locally admits one authored operation. + /// + /// Preparation commits the complete recoverable intent before the signer + /// can be invoked. Delivery remains an explicit caller-driven phase. + pub async fn submit_push(&self, request: PushRequest) -> Result<PushStatus, Error> { + let operation_id = request.operation_id(); + self.sign_prepared(request).await?; + self.admit_signed(operation_id).await?; + self.push_status(operation_id) + .await? + .ok_or(Error::StorageFailed) + } + + /// Runs one bounded delivery attempt for one durable authored plan. + pub async fn deliver_push( + &self, + operation_id: radroots_sync::policy::SyncId, + ) -> Result<DeliveryExecutionReceipt, Error> { + self.engine.deliver_push(operation_id).await + } + + /// Returns the native passive sync status without starting recovery work. + pub async fn status(&self, projections: &[ProjectionId]) -> Result<SyncStatus, Error> { + self.engine.status(projections).await + } + + /// Returns the native host scheduling decision for one durable plan. + pub fn retry_decision( + &self, + plan: &AuthoredDeliveryPlan, + now_unix_ms: u64, + ) -> Result<radroots_protocol::runtime::v1::SyncRetryDecision, Error> { + self.engine.retry_decision(plan, now_unix_ms) + } +} + +#[cfg(feature = "sync")] +impl std::fmt::Debug for Operations<'_> { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Operations") + .field("engine", &"<borrowed canonical engine>") + .finish() + } +} + +#[cfg(all(test, feature = "sync", feature = "memory"))] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }; + + use radroots_event::{ + GenericEventDraft, SignedEvent, contract::AuthorRole, wire::Nip01EventWire, + }; + use radroots_event_codec::authoring::AuthoredEventPlan; + use radroots_identity::PublicKey; + use radroots_protocol::runtime::v1::SyncCapabilityState; + use radroots_signing::{Actor, actor::ActorSource, request::CancellationPolicy}; + use radroots_storage::{ + event::{SourceGeneration, StoredVisibleEvent}, + journal::IdempotencyKey, + memory::MemoryStorage, + projection::{ + ProjectionGeneration, ProjectionId, RawSourceDigest, RebuildFailure, RebuildTicketId, + }, + }; + use radroots_sync::{ + Engine, PullRequest, PushRequest, + ingest::RegistryPolicy, + policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage}, + projection::{Reducer, ReducerError, RefreshRequest, RefreshState}, + pull::PullTermination, + }; + use radroots_transport::{ + Error as TransportError, EventSource, FetchPage, FetchRequest, SourceStatus, Target, + TargetSet, TransportId, + capability::{Availability, Maturity, SourceCapabilities}, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, + source::{EventProvenance, NextPage, ObservedEvent}, + }; + + use crate::{ClientBuilder, error::ErrorKind}; + + const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + + struct FixedClock; + struct SequenceIds(AtomicU8); + struct CancelledSource; + + impl Clock for FixedClock { + fn now_unix_ms(&self) -> Result<u64, Error> { + Ok(1_700_000_000_000) + } + } + + impl IdSource for SequenceIds { + fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> { + let next = self.0.fetch_add(1, Ordering::Relaxed); + SyncId::new([next; 16]) + } + } + + impl EventSource for CancelledSource { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, TransportError>> { + Box::pin(async { + Ok(SourceStatus::new( + TransportId::NOSTR, + true, + Maturity::Stable, + Availability::Available, + SourceCapabilities::FETCH, + "ready", + )) + }) + } + + fn fetch( + &self, + request: FetchRequest, + ) -> radroots_transport::BoxFuture<'_, Result<FetchPage, TransportError>> { + Box::pin(async move { + FetchPage::for_request( + &request, + Vec::new(), + Vec::new(), + NextPage::Cancelled { resume_from: None }, + ) + }) + } + } + + struct EmptyReducer { + id: ProjectionId, + generation: ProjectionGeneration, + } + + impl Reducer for EmptyReducer { + fn projection_id(&self) -> &ProjectionId { + &self.id + } + + fn generation(&self) -> ProjectionGeneration { + self.generation + } + + fn begin_rebuild( + &self, + _ticket_id: RebuildTicketId, + _source_generation: SourceGeneration, + _source_digest: RawSourceDigest, + ) -> Result<(), ReducerError> { + Ok(()) + } + + fn reduce( + &self, + events: &[StoredVisibleEvent], + prior_projected_rows: u64, + _rebuild_ticket: Option<RebuildTicketId>, + ) -> Result<u64, ReducerError> { + assert!(events.is_empty()); + Ok(prior_projected_rows) + } + + fn abort_rebuild( + &self, + _ticket_id: RebuildTicketId, + _failure: RebuildFailure, + ) -> Result<(), ReducerError> { + Ok(()) + } + } + + fn target() -> Target { + Target::nostr_relay("wss://sync.example").expect("target") + } + + fn engine(storage: Arc<MemoryStorage>) -> Engine { + let capability: Arc<dyn SyncStorage> = storage; + Engine::builder( + capability, + Arc::new(FixedClock), + Arc::new(SequenceIds(AtomicU8::new(1))), + DeadlinePolicy::new(1_000, 1_000, 1_000).expect("deadlines"), + ) + .source(Arc::new(CancelledSource)) + .build() + .expect("engine") + } + + fn invalid_observation() -> ObservedEvent { + let mut wire = Nip01EventWire { + id: "0".repeat(64), + pubkey: PUBLIC_KEY.to_owned(), + created_at: 1_800_000_100, + kind: 0, + tags: vec![], + content: "invalid signature fixture".to_owned(), + sig: "42".repeat(64), + extra: Default::default(), + }; + wire.id = wire.computed_event_id().expect("event id").to_hex(); + let raw = format!( + "{{\"id\":\"{}\",\"pubkey\":\"{}\",\"created_at\":{},\"kind\":0,\"tags\":[],\"content\":\"invalid signature fixture\",\"sig\":\"{}\"}}", + wire.id, wire.pubkey, wire.created_at, wire.sig + ); + let event = SignedEvent::from_wire_verified_id(wire, raw).expect("signed event"); + let target = target(); + let provenance = EventProvenance::new( + TransportId::NOSTR, + target.fingerprint().clone(), + 1_700_000_000_000, + ) + .expect("provenance"); + ObservedEvent::new(event, provenance) + } + + fn push_request() -> PushRequest { + let actor = Actor::new( + PublicKey::from_hex(PUBLIC_KEY).expect("public key"), + ActorSource::ExplicitPublicKey, + [AuthorRole::Any], + ) + .expect("actor"); + let plan = AuthoredEventPlan::from_generic( + GenericEventDraft::new( + "radroots.social.geochat.v1", + 20_000, + 1_700_000_000, + Vec::new(), + "content", + PUBLIC_KEY, + ) + .expect("draft"), + ) + .expect("authored plan"); + PushRequest::new( + SyncId::new([8; 16]).expect("operation id"), + IdempotencyKey::parse("sdk-sync-wrapper").expect("idempotency key"), + actor, + plan, + TargetSet::new(vec![target()]).expect("targets"), + SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()), + 1_700_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ) + .expect("push request") + } + + #[tokio::test] + async fn operations_delegate_native_pull_ingest_projection_push_delivery_and_status() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([3; 32]).expect("generation"), + )); + let client = ClientBuilder::new() + .storage(storage.clone()) + .sync_engine(engine(storage)) + .build() + .expect("client"); + let operations = client.sync().expect("open client").expect("sync"); + let policy = RegistryPolicy::verified(); + + let pull = operations + .pull( + PullRequest::new(TargetSet::new(vec![target()]).expect("targets"), 10, 1) + .expect("pull request"), + &policy, + ) + .await + .expect("pull"); + assert_eq!(pull.termination(), PullTermination::Cancelled); + + let ingest = operations + .ingest_batch(vec![invalid_observation(), invalid_observation()], &policy) + .await; + assert_eq!(ingest.accepted(), 0); + assert_eq!(ingest.rejected(), 2); + assert!( + ingest + .outcomes() + .iter() + .all(|outcome| matches!(outcome, Err(Error::VerificationFailed))) + ); + + let projection_id = ProjectionId::parse("sdk.sync.test").expect("projection id"); + let generation = ProjectionGeneration::new([5; 32]).expect("generation"); + let projection = operations + .refresh_projection( + RefreshRequest::new(projection_id.clone(), generation, 10, 1) + .expect("refresh request"), + &EmptyReducer { + id: projection_id.clone(), + generation, + }, + ) + .await + .expect("projection"); + assert_eq!(projection.state(), RefreshState::Complete); + + let push = push_request(); + let operation_id = push.operation_id(); + let preparation = operations + .prepare_push(push.clone()) + .await + .expect("durable preparation"); + assert!(!preparation.is_replay()); + assert_eq!( + operations.sign_prepared(push).await, + Err(Error::MissingSigner) + ); + assert!( + operations + .push_status(operation_id) + .await + .expect("push status") + .is_some() + ); + assert_eq!( + operations.deliver_push(operation_id).await, + Err(Error::MissingSink) + ); + + let status = operations + .status(std::slice::from_ref(&projection_id)) + .await + .expect("status"); + assert_eq!(status.source().state(), SyncCapabilityState::Available); + assert_eq!(status.sink().state(), SyncCapabilityState::Unsupported); + assert_eq!(status.projections().len(), 1); + + client.close().await.expect("close"); + assert_eq!( + client.sync().expect_err("closed").kind(), + ErrorKind::ClientClosed + ); + } +} diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs @@ -0,0 +1,868 @@ +//! Canonical trade planning, commit, query, and private-evidence operations. + +use std::{error, fmt}; + +use radroots_event::{ + GenericEventDraft, + contract::AuthorRole, + trade::{TradeMutationEnvelopeV1, TradeProtocolError, canonical_trade_mutation_content}, +}; +use radroots_event_codec::{ + authoring::AuthoredEventPlan, encode::EventEncodeError, + encode::trade::trade_mutation_event_build, +}; +use radroots_signing::Actor; +use radroots_trade::{Projection, ReductionInput, WorkflowPlan, reducer::reduce_trade_records}; + +/// Pure inputs for one frozen trade command. +#[derive(Clone, Debug)] +pub struct PrepareRequest { + actor: Actor, + mutation: TradeMutationEnvelopeV1, +} + +impl PrepareRequest { + /// Creates explicit inputs for any canonical proposal, revision, decision, + /// cancellation, or resumable mutation. + #[must_use] + pub const fn new(actor: Actor, mutation: TradeMutationEnvelopeV1) -> Self { + Self { actor, mutation } + } +} + +/// Frozen, replay-stable trade workflow plan. +#[derive(Clone, Debug)] +pub struct Plan { + actor: Actor, + workflow: WorkflowPlan, + authored_event: AuthoredEventPlan, +} + +impl Plan { + /// Returns the exact authorized actor carried into signing. + #[must_use] + pub const fn actor(&self) -> &Actor { + &self.actor + } + + /// Returns the lower-owned validated workflow and required host actions. + pub const fn workflow(&self) -> &WorkflowPlan { + &self.workflow + } + + /// Returns the immutable canonical authored event plan. + #[must_use] + pub const fn authored_event(&self) -> &AuthoredEventPlan { + &self.authored_event + } +} + +/// Trade planning failure stage. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PrepareErrorKind { + /// The actor identity or role cannot author the supplied mutation. + UnauthorizedActor, + /// Event-domain canonicalization rejected the mutation. + CanonicalMutation, + /// The lower trade workflow rejected the canonical mutation. + Workflow, + /// The canonical event codec rejected the mutation. + Encode, + /// The canonical event draft rejected the encoded mutation. + Draft, +} + +/// One secret-safe trade planning failure retaining its lower source. +pub struct PrepareError { + kind: PrepareErrorKind, + source: Option<Box<dyn error::Error + Send + Sync>>, +} + +impl PrepareError { + /// Returns the stable client-level planning stage. + #[must_use] + pub const fn kind(&self) -> PrepareErrorKind { + self.kind + } + + fn unauthorized_actor() -> Self { + Self { + kind: PrepareErrorKind::UnauthorizedActor, + source: None, + } + } + + fn canonical(source: TradeProtocolError) -> Self { + Self::with_source(PrepareErrorKind::CanonicalMutation, source) + } + + fn workflow(source: radroots_trade::Error) -> Self { + Self::with_source(PrepareErrorKind::Workflow, source) + } + + fn encode(source: EventEncodeError) -> Self { + Self::with_source(PrepareErrorKind::Encode, source) + } + + fn draft(source: radroots_event::draft::DraftError) -> Self { + Self::with_source(PrepareErrorKind::Draft, source) + } + + fn with_source( + kind: PrepareErrorKind, + source: impl error::Error + Send + Sync + 'static, + ) -> Self { + Self { + kind, + source: Some(Box::new(source)), + } + } +} + +impl fmt::Display for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self.kind { + PrepareErrorKind::UnauthorizedActor => "trade actor is not authorized", + PrepareErrorKind::CanonicalMutation => "trade mutation is not canonical", + PrepareErrorKind::Workflow => "trade workflow is invalid", + PrepareErrorKind::Encode => "trade event encoding failed", + PrepareErrorKind::Draft => "trade event draft is invalid", + }) + } +} + +impl fmt::Debug for PrepareError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PrepareError") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +impl error::Error for PrepareError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + self.source + .as_deref() + .map(|source| source as &(dyn error::Error + 'static)) + } +} + +/// Canonicalizes, authorizes, validates, and freezes one trade command. +/// +/// This operation performs no signing, persistence, private-artifact access, +/// scheduling, or delivery. Every proposal, revision, decision, cancellation, +/// and resumed command uses the same lower-owned `TradeId` and workflow law. +pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> { + let canonical = canonical_trade_mutation_content(request.mutation) + .map_err(PrepareError::canonical)? + .envelope; + let required_role = match ( + canonical.author_pubkey == canonical.buyer_pubkey, + canonical.author_pubkey == canonical.seller_pubkey, + ) { + (true, _) => AuthorRole::Buyer, + (false, true) => AuthorRole::Seller, + (false, false) => return Err(PrepareError::unauthorized_actor()), + }; + if ( + request.actor.public_key() == canonical.author_pubkey, + request.actor.satisfies(required_role), + ) != (true, true) + { + return Err(PrepareError::unauthorized_actor()); + } + let workflow = WorkflowPlan::prepare(canonical.clone()).map_err(PrepareError::workflow)?; + let parts = trade_mutation_event_build(canonical.clone()).map_err(PrepareError::encode)?; + let authored_event = AuthoredEventPlan::from_generic( + GenericEventDraft::new( + canonical.contract_id.clone(), + parts.kind, + canonical.authored_at_unix_s, + parts.tags, + parts.content, + canonical.author_pubkey.to_hex(), + ) + .map_err(PrepareError::draft)?, + ) + .map_err(PrepareError::draft)?; + Ok(Plan { + actor: request.actor, + workflow, + authored_event, + }) +} + +/// Deterministically reduces caller-supplied canonical evidence. +#[must_use] +pub fn project(input: ReductionInput) -> Projection { + reduce_trade_records(input) +} + +#[cfg(feature = "sync")] +use radroots_signing::request::CancellationPolicy; +#[cfg(feature = "sync")] +use radroots_storage::{ + event::{EventPage, EventQuery, StoredVisibleEvent}, + journal::IdempotencyKey, + private_artifact::{PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStage}, +}; +#[cfg(feature = "sync")] +use radroots_sync::{ + policy::{Error as SyncError, SyncId}, + push::PushStatus, +}; + +/// Explicit commit inputs for one prepared trade command. +#[cfg(feature = "sync")] +#[derive(Clone, Debug)] +pub struct EnqueueRequest { + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, +} + +#[cfg(feature = "sync")] +impl EnqueueRequest { + /// Creates a command request whose transport selection has no fallback. + #[must_use] + pub const fn new( + operation_id: SyncId, + idempotency_key: IdempotencyKey, + plan: Plan, + profile: crate::transport::Profile, + delivery_deadline_unix_ms: u64, + cancellation: CancellationPolicy, + ) -> Self { + Self { + operation_id, + idempotency_key, + plan, + profile, + delivery_deadline_unix_ms, + cancellation, + } + } +} + +/// Private-term metadata verification failure. +#[cfg(feature = "sync")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PrivateTermsError { + /// Canonical storage failed to inspect the requested metadata. + Storage, + /// The workflow does not require private terms. + NotRequired, + /// Metadata is absent, inactive, or does not match the public commitment. + EvidenceMismatch, +} + +/// Borrowed trade operations over canonical storage and sync capabilities. +#[cfg(feature = "sync")] +#[derive(Clone, Copy)] +pub struct Operations<'a> { + storage: &'a dyn radroots_storage::Storage, + sync: crate::sync::Operations<'a>, +} + +#[cfg(feature = "sync")] +impl fmt::Debug for Operations<'_> { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Operations") + .field("storage", &"<borrowed canonical storage>") + .field("sync", &self.sync) + .finish() + } +} + +#[cfg(feature = "sync")] +impl<'a> Operations<'a> { + pub(crate) const fn new( + storage: &'a dyn radroots_storage::Storage, + sync: crate::sync::Operations<'a>, + ) -> Self { + Self { storage, sync } + } + + /// Durably prepares, signs, and locally admits a command. Reusing the same + /// idempotency input is the canonical resume/replay operation. + pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushStatus, SyncError> { + let targets = request + .profile + .targets() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + let satisfaction = request + .profile + .satisfaction() + .cloned() + .ok_or(SyncError::InvalidPushRequest)?; + self.sync + .submit_push(radroots_sync::PushRequest::new( + request.operation_id, + request.idempotency_key, + request.plan.actor, + request.plan.authored_event, + targets, + satisfaction, + request.delivery_deadline_unix_ms, + request.cancellation, + )?) + .await + } + + /// Returns one native, bounded, generation-bound page of visible evidence. + pub async fn query_visible( + &self, + query: EventQuery, + ) -> Result<EventPage<StoredVisibleEvent>, radroots_storage::Error> { + radroots_storage::event::EventStore::query_visible(self.storage, query).await + } + + /// Returns native private-artifact metadata without reading secret material. + pub async fn private_artifact( + &self, + artifact_id: PrivateArtifactId, + ) -> Result<Option<PrivateArtifactMetadata>, radroots_storage::Error> { + radroots_storage::private_artifact::PrivateArtifactStore::metadata( + self.storage, + artifact_id, + ) + .await + } + + /// Verifies that canonical active metadata matches a plan's public schema + /// and ciphertext commitment. Plaintext, ciphertext, and keys never cross + /// the SDK boundary. + pub async fn verify_private_terms( + &self, + plan: &Plan, + artifact_id: PrivateArtifactId, + ) -> Result<PrivateArtifactMetadata, PrivateTermsError> { + let expected = plan + .workflow + .private_terms() + .ok_or(PrivateTermsError::NotRequired)?; + let metadata = self + .private_artifact(artifact_id) + .await + .map_err(|_| PrivateTermsError::Storage)? + .ok_or(PrivateTermsError::EvidenceMismatch)?; + let commitment = hex_lower(metadata.commitment().as_bytes()); + let evidence_matches = [ + metadata.stage() == PrivateArtifactStage::Active, + metadata.schema_id().as_str() == expected.schema_id(), + commitment == expected.ciphertext_commitment(), + ]; + if evidence_matches != [true; 3] { + return Err(PrivateTermsError::EvidenceMismatch); + } + Ok(metadata) + } +} + +#[cfg(feature = "sync")] +fn hex_lower(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut encoded = String::with_capacity(bytes.len() * 2); + for byte in bytes { + encoded.push(char::from(HEX[usize::from(byte >> 4)])); + encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + encoded +} + +#[cfg(test)] +mod tests { + use radroots_event::{ + id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, MutationId, TradeId}, + trade::{ + FulfillmentProfileV1, RADROOTS_TRADE_CANCELLATION_CONTRACT_ID, + RADROOTS_TRADE_DECISION_CONTRACT_ID, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, + RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID, + RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION, + TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1, + TradeDecisionV1, TradeEconomicAdjustmentV1, TradeEconomicsProfileV1, + TradeLineTombstoneV1, TradeMutationBodyV1, TradeMutationKindV1, TradePrivateTermsRefV1, + }, + }; + use radroots_identity::PublicKey; + use radroots_signing::actor::ActorSource; + use radroots_trade::workflow::WorkflowAction; + + use super::*; + + const BUYER: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + const SELLER: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; + + fn pubkey(value: &str) -> PublicKey { + PublicKey::from_hex(value).expect("public key") + } + + fn actor(public_key: &str, role: AuthorRole) -> Actor { + Actor::from_public_key_hex(public_key, ActorSource::ExplicitPublicKey, [role]) + .expect("actor") + } + + fn mutation_id(marker: char) -> MutationId { + MutationId::parse(std::iter::repeat_n(marker, 64).collect::<String>()).expect("mutation id") + } + + fn candidate(suffix: &str) -> TradeCandidateTermsV1 { + TradeCandidateTermsV1 { + candidate_id: None, + schema_version: RADROOTS_TRADE_SCHEMA_VERSION, + base_candidate_id: None, + supersession_intent: None, + buyer_pubkey: pubkey(BUYER), + seller_pubkey: pubkey(SELLER), + farm_id: DTag::parse("farm-1").expect("farm id"), + lines: vec![TradeCandidateLineV1 { + line_id: DTag::parse(format!("line-{suffix}")).expect("line id"), + listing_addr: ClassifiedListingAddress::parse(format!( + "30402:{SELLER}:listing-{suffix}" + )) + .expect("listing address"), + listing_event_id: EventId::parse("cc".repeat(32)).expect("event id"), + listing_snapshot_sha256: "dd".repeat(32), + product_id: format!("carrots-{suffix}"), + option_id: None, + bin_id: InventoryBinId::parse(format!("bin-{suffix}")).expect("bin id"), + quantity_mantissa: "2".into(), + quantity_scale: 0, + unit_code: "count".into(), + unit_profile: "mvp-count".into(), + unit_price_mantissa: "500".into(), + currency_code: "USD".into(), + line_subtotal_mantissa: "1000".into(), + replaces_line_id: None, + }], + line_tombstones: Vec::<TradeLineTombstoneV1>::new(), + economics: TradeEconomicsProfileV1 { + profile_id: "mvp-fixed".into(), + currency_code: "USD".into(), + currency_exponent: 2, + rounding_profile: "half-even".into(), + subtotal_mantissa: "1000".into(), + discount_total_mantissa: "0".into(), + adjustment_total_mantissa: "0".into(), + total_mantissa: "1000".into(), + adjustments: Vec::<TradeEconomicAdjustmentV1>::new(), + }, + fulfillment: FulfillmentProfileV1 { + profile_id: "market-pickup".into(), + method: "pickup".into(), + starts_at_unix_s: 1_800_000_000, + ends_at_unix_s: 1_800_003_600, + timezone: "America/New_York".into(), + utc_offset_seconds: -18_000, + fold: 0, + location_class: "farmstand".into(), + requires_private_terms: true, + }, + cancellation: TradeCancellationProfileV1 { + profile_id: "buyer-pre-agreement".into(), + buyer_pre_agreement: true, + post_agreement_cutoff_unix_s: None, + }, + private_terms: Some(TradePrivateTermsRefV1 { + artifact_id: "artifact-1".into(), + schema_id: "radroots.private.fulfillment.v1".into(), + ciphertext_commitment: "ee".repeat(32), + required_acknowledgement: true, + }), + proposal_expires_at_unix_s: 1_800_010_000, + } + } + + fn envelope(contract_id: &str, body: TradeMutationBodyV1) -> TradeMutationEnvelopeV1 { + let initial = body.mutation_kind() == TradeMutationKindV1::Proposal; + TradeMutationEnvelopeV1 { + mutation_id: None, + contract_id: contract_id.into(), + schema_version: RADROOTS_TRADE_SCHEMA_VERSION, + trade_id: TradeId::parse("11".repeat(16)).expect("trade id"), + root_mutation_id: (!initial).then(|| mutation_id('1')), + buyer_pubkey: pubkey(BUYER), + seller_pubkey: pubkey(SELLER), + farm_id: DTag::parse("farm-1").expect("farm id"), + parent_mutation_ids: if initial { + vec![] + } else { + vec![mutation_id('1')] + }, + author_pubkey: pubkey(BUYER), + counterparty_pubkey: pubkey(SELLER), + authored_at_unix_s: 1_800_000_000, + body, + } + } + + fn all_commands() -> Vec<TradeMutationEnvelopeV1> { + let proposal = canonical_trade_mutation_content(envelope( + RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, + TradeMutationBodyV1::Proposal { + candidate: candidate("1"), + }, + )) + .expect("proposal") + .envelope; + let proposal_id = proposal.mutation_id.expect("proposal id"); + let candidate_id = match &proposal.body { + TradeMutationBodyV1::Proposal { candidate } => { + candidate.candidate_id.expect("candidate id") + } + _ => unreachable!(), + }; + vec![ + proposal, + envelope( + RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID, + TradeMutationBodyV1::RevisionProposal { + candidate: candidate("2"), + }, + ), + envelope( + RADROOTS_TRADE_DECISION_CONTRACT_ID, + TradeMutationBodyV1::Decision { + proposal_mutation_id: proposal_id, + candidate_id, + decision: TradeDecisionV1::Declined { + reason: "unavailable".into(), + }, + }, + ), + envelope( + RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID, + TradeMutationBodyV1::RevisionDecision { + proposal_mutation_id: mutation_id('2'), + candidate_id, + decision: TradeDecisionV1::Declined { + reason: "unavailable".into(), + }, + }, + ), + envelope( + RADROOTS_TRADE_CANCELLATION_CONTRACT_ID, + TradeMutationBodyV1::Cancellation { + target_candidate_id: Some(candidate_id), + target_claim_mutation_id: None, + reason: "cancelled".into(), + }, + ), + ] + } + + #[test] + fn prepare_covers_every_command_with_one_trade_identity_and_private_plan() { + let plans = all_commands() + .into_iter() + .map(|mutation| { + prepare(PrepareRequest::new( + actor(BUYER, AuthorRole::Buyer), + mutation, + )) + }) + .collect::<Result<Vec<_>, _>>() + .expect("plans"); + + assert_eq!( + plans + .iter() + .map(|plan| plan.workflow().kind()) + .collect::<Vec<_>>(), + [ + TradeMutationKindV1::Proposal, + TradeMutationKindV1::RevisionProposal, + TradeMutationKindV1::Decision, + TradeMutationKindV1::RevisionDecision, + TradeMutationKindV1::Cancellation, + ] + ); + assert!( + plans + .iter() + .all(|plan| plan.workflow().trade_id() == plans[0].workflow().trade_id()) + ); + assert_eq!( + plans[0].workflow().required_actions()[0], + WorkflowAction::VerifyPrivateTerms + ); + assert_eq!( + plans[0] + .workflow() + .private_terms() + .expect("private terms") + .artifact_id(), + "artifact-1" + ); + for plan in plans { + assert_eq!( + plan.authored_event() + .body() + .contract() + .contract_id() + .as_str(), + plan.workflow().kind().contract_id() + ); + assert_eq!( + plan.authored_event().body().kind(), + plan.workflow().kind().nostr_kind() + ); + } + } + + #[test] + fn prepare_rejects_wrong_identity_role_and_invalid_protocol_once() { + let mutation = all_commands().remove(0); + let wrong_role = prepare(PrepareRequest::new( + actor(BUYER, AuthorRole::Seller), + mutation.clone(), + )) + .expect_err("wrong role"); + assert_eq!(wrong_role.kind(), PrepareErrorKind::UnauthorizedActor); + assert!(std::error::Error::source(&wrong_role).is_none()); + + let wrong_identity = prepare(PrepareRequest::new( + actor(SELLER, AuthorRole::Buyer), + mutation, + )) + .expect_err("wrong identity"); + assert_eq!(wrong_identity.kind(), PrepareErrorKind::UnauthorizedActor); + + let mut invalid = all_commands().remove(0); + invalid.contract_id = "radroots.trade.cancellation.v1".into(); + let canonical = prepare(PrepareRequest::new( + actor(BUYER, AuthorRole::Buyer), + invalid, + )) + .expect_err("invalid contract"); + assert_eq!(canonical.kind(), PrepareErrorKind::CanonicalMutation); + assert!(std::error::Error::source(&canonical).is_some()); + assert!(!format!("{canonical:?}").contains("artifact-1")); + + let mut seller_authored = all_commands().remove(0); + seller_authored.mutation_id = None; + seller_authored.author_pubkey = pubkey(SELLER); + seller_authored.counterparty_pubkey = pubkey(BUYER); + let seller_plan = prepare(PrepareRequest::new( + actor(SELLER, AuthorRole::Seller), + seller_authored, + )) + .expect("seller-authored command"); + assert_eq!( + seller_plan.workflow().trade_id(), + &TradeId::parse("11".repeat(16)).unwrap() + ); + + let outsider = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let mut unauthorized = all_commands().remove(0); + unauthorized.mutation_id = None; + unauthorized.author_pubkey = pubkey(outsider); + assert_eq!( + prepare(PrepareRequest::new( + actor(outsider, AuthorRole::Any), + unauthorized, + )) + .expect_err("author must be a governed party") + .kind(), + PrepareErrorKind::UnauthorizedActor + ); + } + + #[test] + fn query_projection_returns_the_lower_projection_and_conflict_evidence_types() { + let trade_id = TradeId::parse("22".repeat(16)).expect("trade id"); + let projection = project(ReductionInput::new(trade_id)); + assert_eq!(projection.trade_id(), &trade_id); + assert!(projection.candidate_heads().is_empty()); + assert!(!projection.projection_digest().is_empty()); + } + + #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))] + mod operations { + use std::sync::{ + Arc, + atomic::{AtomicU8, Ordering}, + }; + + use radroots_nostr::key::SecretKey; + use radroots_signing::request::CancellationPolicy; + use radroots_storage::{ + Outbox, + event::{EventQuery, EventQueryBounds, SourceGeneration}, + journal::IdempotencyKey, + memory::MemoryStorage, + private_artifact::{ + ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DurableSecretReference, + PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactStore, RetentionPolicy, + }, + }; + use radroots_sync::{ + Engine, + policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage}, + }; + use radroots_transport::{ + DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkFailure, + SinkStatus, Target, TargetSet, TransportId, + capability::{Availability, Maturity, SinkCapabilities}, + outcome::Retryability, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, + }; + + use super::*; + use crate::{ClientBuilder, transport::Profile}; + + const BUYER_SECRET: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; + + struct HostClock; + struct SequenceIds(AtomicU8); + struct NoopSink; + + impl Clock for HostClock { + fn now_unix_ms(&self) -> Result<u64, Error> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .ok() + .and_then(|duration| u64::try_from(duration.as_millis()).ok()) + .filter(|value| *value != 0) + .ok_or(Error::ClockUnavailable) + } + } + impl IdSource for SequenceIds { + fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> { + SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16]) + } + } + impl EventSink for NoopSink { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> { + Box::pin(async { + Ok(SinkStatus::new( + TransportId::NOSTR, + true, + Maturity::Stable, + Availability::Available, + SinkCapabilities::DELIVER, + "ready", + )) + }) + } + fn deliver( + &self, + request: DeliveryRequest, + ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, SinkFailure>> + { + Box::pin(async move { + Err(SinkFailure::for_request( + &request, + "test_sink_unavailable", + Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("test sink failure")) + }) + } + } + + #[tokio::test] + async fn operations_cover_private_evidence_pagination_commit_replay_and_cancellation() { + let storage = Arc::new(MemoryStorage::new( + SourceGeneration::new([6; 32]).expect("generation"), + )); + let artifact_id = PrivateArtifactId::new([7; 16]).expect("artifact id"); + let metadata = PrivateArtifactMetadata::new( + artifact_id, + ArtifactKind::parse("trade.private_terms").expect("kind"), + ArtifactSchemaId::parse("radroots.private.fulfillment.v1").expect("schema"), + ArtifactCommitment::new([0xee; 32]), + 64, + DurableSecretReference::new("memory", "trade-artifact-1", 1).expect("reference"), + RetentionPolicy::indefinite(), + 1_800_000_000_000, + ) + .expect("metadata"); + PrivateArtifactStore::put_metadata(storage.as_ref(), metadata) + .await + .expect("store metadata"); + + let signer = Arc::new( + radroots_nostr::signing::LocalSigner::new( + SecretKey::parse(BUYER_SECRET).expect("secret"), + ) + .expect("signer"), + ); + let capability: Arc<dyn SyncStorage> = storage.clone(); + let engine = Engine::builder( + capability, + Arc::new(HostClock), + Arc::new(SequenceIds(AtomicU8::new(1))), + DeadlinePolicy::new(30_000, 30_000, 30_000).expect("deadlines"), + ) + .sink(Arc::new(NoopSink)) + .signer(signer) + .build() + .expect("engine"); + let client = ClientBuilder::new() + .storage(storage.clone()) + .sync_engine(engine) + .build() + .expect("client"); + let operations = client.trade().expect("open").expect("trade operations"); + let plan = prepare(PrepareRequest::new( + actor(BUYER, AuthorRole::Buyer), + all_commands().remove(0), + )) + .expect("plan"); + + let verified = operations + .verify_private_terms(&plan, artifact_id) + .await + .expect("private evidence"); + assert_eq!(verified.artifact_id(), artifact_id); + let page = operations + .query_visible(EventQuery::all(EventQueryBounds::first(1).expect("bounds"))) + .await + .expect("page"); + assert!(page.items().is_empty()); + assert!(page.next_cursor().is_none()); + + let targets = TargetSet::new(vec![ + Target::nostr_relay("wss://trade.example").expect("target"), + ]) + .expect("targets"); + let satisfaction = + SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all()); + let request = EnqueueRequest::new( + SyncId::new([11; 16]).expect("operation id"), + IdempotencyKey::parse("trade-proposal-a").expect("idempotency"), + plan, + Profile::delivery(targets.clone(), satisfaction.clone()).expect("profile"), + 2_000_000_001_000, + CancellationPolicy::PreservePublishedRequest, + ); + drop(operations.enqueue(request.clone())); + assert_eq!( + Outbox::status(storage.as_ref()) + .await + .expect("status") + .pending, + 0 + ); + let committed = operations.enqueue(request.clone()).await.expect("commit"); + assert_eq!(committed.delivery_plan().intent().target_set(), &targets); + let replay = operations.enqueue(request).await.expect("resume replay"); + assert_eq!(replay, committed); + } + } +} diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -0,0 +1,742 @@ +//! Explicit user-facing transport profile composition. +//! +//! Profiles retain canonical `radroots_transport` identities, targets, +//! policies, and statuses. They select no adapter implicitly and never replace +//! an unavailable selection with another transport. + +use radroots_transport::{ + Error, SinkStatus, SourceStatus, TargetSet, TransportId, + capability::{Availability, Maturity, SinkCapabilities, SourceCapabilities}, + policy::SatisfactionPolicy, +}; +#[cfg(feature = "nostr")] +use std::sync::{Arc, RwLock}; + +#[cfg(feature = "nostr")] +pub use radroots_transport_nostr::RelayUrlPolicy; + +const PREVIEW_UNAVAILABLE_MESSAGE: &str = "preview transport is unavailable in this SDK release"; + +/// A side-effect-free transport selection for a client operation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Profile { + selection: Selection, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum Selection { + LocalOnly, + Delivery { + targets: TargetSet, + satisfaction: SatisfactionPolicy, + }, + UnavailablePreview { + source: SourceStatus, + sink: SinkStatus, + }, +} + +impl Profile { + /// Selects local persistence only, with no transport target or fallback. + #[must_use] + pub const fn local_only() -> Self { + Self { + selection: Selection::LocalOnly, + } + } + + /// Selects an exact bounded target set and canonical satisfaction policy. + /// + /// Impossible quorum and required-target policies are rejected here by the + /// owning transport contract. Construction performs no network operation. + pub fn delivery(targets: TargetSet, satisfaction: SatisfactionPolicy) -> Result<Self, Error> { + satisfaction.validate_for(&targets)?; + Ok(Self { + selection: Selection::Delivery { + targets, + satisfaction, + }, + }) + } + + /// Describes a preview transport that is intentionally not selectable. + /// + /// Both canonical capability directions remain explicitly unconfigured + /// and unavailable. The profile has no targets and therefore cannot fall + /// back to local, Nostr, daemon, or another transport. + #[must_use] + pub fn unavailable_preview(transport_id: TransportId) -> Self { + Self { + selection: Selection::UnavailablePreview { + source: SourceStatus::new( + transport_id, + false, + Maturity::Preview, + Availability::Unavailable, + SourceCapabilities::NONE, + PREVIEW_UNAVAILABLE_MESSAGE, + ), + sink: SinkStatus::new( + transport_id, + false, + Maturity::Preview, + Availability::Unavailable, + SinkCapabilities::NONE, + PREVIEW_UNAVAILABLE_MESSAGE, + ), + }, + } + } + + /// Returns whether this profile authorizes no transport operation. + #[must_use] + pub const fn is_local_only(&self) -> bool { + matches!(self.selection, Selection::LocalOnly) + } + + /// Returns the exact selected targets, if delivery is authorized. + #[must_use] + pub const fn targets(&self) -> Option<&TargetSet> { + match &self.selection { + Selection::Delivery { targets, .. } => Some(targets), + Selection::LocalOnly | Selection::UnavailablePreview { .. } => None, + } + } + + /// Returns the exact selected satisfaction policy, if delivery is authorized. + #[must_use] + pub const fn satisfaction(&self) -> Option<&SatisfactionPolicy> { + match &self.selection { + Selection::Delivery { satisfaction, .. } => Some(satisfaction), + Selection::LocalOnly | Selection::UnavailablePreview { .. } => None, + } + } + + /// Returns canonical source status for an unavailable preview. + #[must_use] + pub const fn source_status(&self) -> Option<&SourceStatus> { + match &self.selection { + Selection::UnavailablePreview { source, .. } => Some(source), + Selection::LocalOnly | Selection::Delivery { .. } => None, + } + } + + /// Returns canonical sink status for an unavailable preview. + #[must_use] + pub const fn sink_status(&self) -> Option<&SinkStatus> { + match &self.selection { + Selection::UnavailablePreview { sink, .. } => Some(sink), + Selection::LocalOnly | Selection::Delivery { .. } => None, + } + } +} + +impl Default for Profile { + fn default() -> Self { + Self::local_only() + } +} + +/// Host-configured, client-shareable Nostr transport slot. +/// +/// Reconfiguration validates the complete relay set before atomically +/// replacing the active adapter. Construction, clearing, and target +/// inspection perform no network I/O. +#[cfg(feature = "nostr")] +#[derive(Clone)] +pub struct NostrSlot { + policy: RelayUrlPolicy, + state: Arc<RwLock<Option<NostrState>>>, +} + +#[cfg(feature = "nostr")] +#[derive(Clone)] +struct NostrState { + transport: Arc<radroots_transport_nostr::NostrTransport>, + targets: TargetSet, +} + +#[cfg(feature = "nostr")] +impl NostrSlot { + /// Creates an inert slot with an explicit destination policy. + #[must_use] + pub fn new(policy: RelayUrlPolicy) -> Self { + Self { + policy, + state: Arc::new(RwLock::new(None)), + } + } + + /// Validates and atomically installs the complete relay selection. + pub fn configure<I, S>(&self, relays: I) -> crate::Result<()> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + let config = radroots_transport_nostr::Config::new(self.policy, relays) + .map_err(crate::Error::invalid_host_configuration)?; + let targets = TargetSet::new( + config + .relays() + .iter() + .map(radroots_transport_nostr::RelayUrl::to_target) + .collect::<Result<Vec<_>, _>>() + .map_err(crate::Error::invalid_host_configuration)?, + ) + .map_err(|_| crate::Error::invalid_host_configuration_without_source())?; + let state = NostrState { + transport: Arc::new(radroots_transport_nostr::NostrTransport::new(config)), + targets, + }; + let mut current = self + .state + .write() + .map_err(|_| crate::Error::shared_operation_unavailable())?; + *current = Some(state); + Ok(()) + } + + /// Removes the active adapter without starting or stopping background work. + pub fn clear(&self) { + if let Ok(mut state) = self.state.write() { + *state = None; + } + } + + /// Returns the currently selected canonical targets. + #[must_use] + pub fn targets(&self) -> Option<TargetSet> { + self.snapshot().map(|state| state.targets) + } + + fn snapshot(&self) -> Option<NostrState> { + self.state.read().ok().and_then(|state| state.clone()) + } +} + +#[cfg(feature = "nostr")] +impl radroots_transport::EventSource for NostrSlot { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SourceStatus, radroots_transport::Error>> { + Box::pin(async move { + match self.snapshot() { + Some(state) => { + radroots_transport::EventSource::status(state.transport.as_ref()).await + } + None => Ok(SourceStatus::new( + TransportId::NOSTR, + false, + Maturity::Stable, + Availability::Unavailable, + SourceCapabilities::FETCH, + "Nostr transport is not configured", + )), + } + }) + } + + fn fetch( + &self, + request: radroots_transport::FetchRequest, + ) -> radroots_transport::BoxFuture< + '_, + Result<radroots_transport::FetchPage, radroots_transport::Error>, + > { + Box::pin(async move { + let state = self + .snapshot() + .ok_or(radroots_transport::Error::UnsupportedOperation)?; + radroots_transport::EventSource::fetch(state.transport.as_ref(), request).await + }) + } +} + +#[cfg(feature = "nostr")] +impl radroots_transport::EventSink for NostrSlot { + fn status( + &self, + ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, radroots_transport::Error>> { + Box::pin(async move { + match self.snapshot() { + Some(state) => { + radroots_transport::EventSink::status(state.transport.as_ref()).await + } + None => Ok(SinkStatus::new( + TransportId::NOSTR, + false, + Maturity::Stable, + Availability::Unavailable, + SinkCapabilities::DELIVER, + "Nostr transport is not configured", + )), + } + }) + } + + fn deliver( + &self, + request: radroots_transport::DeliveryRequest, + ) -> radroots_transport::BoxFuture< + '_, + Result<radroots_transport::DeliveryReceipt, radroots_transport::SinkFailure>, + > { + Box::pin(async move { + let Some(state) = self.snapshot() else { + return Err(radroots_transport::SinkFailure::for_request( + &request, + "nostr_transport_not_configured", + radroots_transport::outcome::Retryability::Terminal, + None, + None, + Vec::new(), + ) + .expect("static unconfigured sink failure is valid")); + }; + radroots_transport::EventSink::deliver(state.transport.as_ref(), request).await + }) + } +} + +#[cfg(feature = "nostr")] +impl std::fmt::Debug for NostrSlot { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("NostrSlot") + .field("policy", &self.policy) + .field("configured", &self.targets().is_some()) + .finish() + } +} + +/// Explicit daemon adapter authentication configuration. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum DaemonAuth { + /// Sends no authorization header. + None, + /// Sends the supplied bearer credential only when delivery is invoked. + BearerToken(String), +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Debug for DaemonAuth { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => formatter.write_str("None"), + Self::BearerToken(_) => formatter.write_str("BearerToken(<redacted>)"), + } + } +} + +/// Explicit daemon endpoint and request deadline configuration. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DaemonConfig { + endpoint: String, + auth: DaemonAuth, + timeout: core::time::Duration, +} + +#[cfg(feature = "radrootsd")] +impl DaemonConfig { + /// Creates inert configuration; no client is built and no request is sent. + #[must_use] + pub fn new(endpoint: impl Into<String>) -> Self { + Self { + endpoint: endpoint.into(), + auth: DaemonAuth::None, + timeout: core::time::Duration::from_secs(10), + } + } + + /// Selects explicit authentication for later invocation. + #[must_use] + pub fn with_auth(mut self, auth: DaemonAuth) -> Self { + self.auth = auth; + self + } + + /// Selects the complete HTTP/RPC request deadline. + #[must_use] + pub const fn with_timeout(mut self, timeout: core::time::Duration) -> Self { + self.timeout = timeout; + self + } +} + +/// Stable secret-safe daemon execution failure class. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum DaemonErrorKind { + /// The explicit authentication value cannot be represented safely. + Authentication, + /// The versioned protocol rejected the request. + InvalidRequest, + /// HTTP transport or timeout failed. + Transport, + /// The daemon returned a JSON-RPC error. + Rpc, + /// The response was malformed or did not match the request. + InvalidResponse, +} + +/// One redacted daemon failure retaining a private source chain. +#[cfg(feature = "radrootsd")] +pub struct DaemonError { + kind: DaemonErrorKind, + source: crate::adapters::radrootsd::RadrootsdError, +} + +#[cfg(feature = "radrootsd")] +impl DaemonError { + /// Returns the stable failure class. + #[must_use] + pub const fn kind(&self) -> DaemonErrorKind { + self.kind + } + + fn from_private(source: crate::adapters::radrootsd::RadrootsdError) -> Self { + use crate::adapters::radrootsd::RadrootsdError; + let kind = match &source { + RadrootsdError::InvalidAuthHeader(_) => DaemonErrorKind::Authentication, + RadrootsdError::InvalidRequest(_) => DaemonErrorKind::InvalidRequest, + RadrootsdError::Http(_) => DaemonErrorKind::Transport, + RadrootsdError::JsonRpc { .. } => DaemonErrorKind::Rpc, + RadrootsdError::MalformedResponse(_) => DaemonErrorKind::InvalidResponse, + }; + Self { kind, source } + } +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Display for DaemonError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self.kind { + DaemonErrorKind::Authentication => "daemon authentication configuration is invalid", + DaemonErrorKind::InvalidRequest => "daemon delivery request is invalid", + DaemonErrorKind::Transport => "daemon transport failed", + DaemonErrorKind::Rpc => "daemon RPC failed", + DaemonErrorKind::InvalidResponse => "daemon response is invalid", + }) + } +} + +#[cfg(feature = "radrootsd")] +impl std::fmt::Debug for DaemonError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("DaemonError") + .field("kind", &self.kind) + .finish_non_exhaustive() + } +} + +#[cfg(feature = "radrootsd")] +impl std::error::Error for DaemonError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.source) + } +} + +/// Explicitly configured daemon execution adapter. +/// +/// Construction is inert. Network contact occurs only in [`Self::deliver`]. +#[cfg(feature = "radrootsd")] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DaemonDelivery { + adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter, +} + +#[cfg(feature = "radrootsd")] +impl DaemonDelivery { + /// Creates an inert adapter from explicit host configuration. + #[must_use] + pub fn new(config: DaemonConfig) -> Self { + let auth = match config.auth { + DaemonAuth::None => crate::adapters::radrootsd::RadrootsdAuth::None, + DaemonAuth::BearerToken(token) => { + crate::adapters::radrootsd::RadrootsdAuth::BearerToken(token) + } + }; + Self { + adapter: crate::adapters::radrootsd::RadrootsdPublishAdapter::new( + crate::adapters::radrootsd::RadrootsdPublishConfig::new(config.endpoint) + .with_auth(auth) + .with_timeout(config.timeout), + ), + } + } + + /// Invokes the generation-5 daemon transport-publish contract. + pub async fn deliver( + &self, + signed_event: radroots_event::SignedEvent, + target_policy: radroots_protocol::radrootsd::transport_publish::v5::TargetPolicy, + delivery_policy: radroots_protocol::radrootsd::transport_publish::v5::DeliveryPolicy, + idempotency_key: Option<String>, + timeout_ms: Option<u64>, + ) -> Result<radroots_protocol::radrootsd::transport_publish::v5::EventResponse, DaemonError> + { + self.adapter + .publish_signed_event(crate::adapters::radrootsd::RadrootsdPublishRequest { + signed_event, + target_policy, + delivery_policy, + idempotency_key, + timeout_ms, + }) + .await + .map_err(DaemonError::from_private) + } +} + +#[cfg(test)] +mod tests { + use radroots_transport::{ + Error, TARGET_SET_MAX_ITEMS, Target, + capability::{Availability, Maturity}, + policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy}, + target::TargetFingerprint, + }; + + use super::*; + + fn target(index: usize) -> Target { + Target::nostr_relay(format!("wss://relay-{index}.example")).expect("target") + } + + #[cfg(feature = "nostr")] + fn signed_event() -> radroots_event::SignedEvent { + let raw = r#"{"id":"56bfc78223bb2221bad82b539efdec1ade0f56d0eb0e1f592fd387df4b2ceee0","pubkey":"585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df","created_at":1700000001,"kind":0,"tags":[],"content":"{}","sig":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"}"#; + let wire = radroots_event::wire::v1::Nip01EventWire::parse_json(raw).expect("wire event"); + radroots_event::SignedEvent::from_wire_verified_id(wire, raw).expect("signed event") + } + + #[test] + fn delivery_profile_preserves_canonical_targets_and_policy() { + let targets = TargetSet::new(vec![target(1), target(2)]).expect("target set"); + let policy = SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::all()); + let profile = Profile::delivery(targets.clone(), policy.clone()).expect("profile"); + + assert_eq!(profile.targets(), Some(&targets)); + assert_eq!(profile.satisfaction(), Some(&policy)); + assert!(!profile.is_local_only()); + assert!(profile.source_status().is_none()); + assert!(profile.sink_status().is_none()); + } + + #[test] + fn canonical_target_and_policy_bounds_fail_during_profile_construction() { + assert_eq!(TargetSet::new(Vec::new()), Err(Error::EmptyTargetSet)); + assert_eq!( + TargetSet::new((0..=TARGET_SET_MAX_ITEMS).map(target).collect()), + Err(Error::TargetSetTooLarge) + ); + + let targets = TargetSet::new(vec![target(1)]).expect("target set"); + let quorum = SatisfactionPolicy::new( + SatisfactionClass::Delivered, + TargetPolicy::quorum(2).expect("non-zero quorum"), + ); + assert_eq!( + Profile::delivery(targets.clone(), quorum), + Err(Error::InvalidSatisfactionPolicy) + ); + + let missing = + TargetFingerprint::from_target(target(2).kind(), target(2).uri(), target(2).scope()); + let required = SatisfactionPolicy::new( + SatisfactionClass::Accepted, + TargetPolicy::required(vec![missing]).expect("required policy"), + ); + assert_eq!( + Profile::delivery(targets, required), + Err(Error::RequiredTargetNotRequested) + ); + } + + #[test] + fn preview_transport_is_explicitly_unavailable_and_unselectable() { + let profile = Profile::unavailable_preview(TransportId::RETICULUM); + let source = profile.source_status().expect("source status"); + let sink = profile.sink_status().expect("sink status"); + + assert_eq!(source.transport_id(), TransportId::RETICULUM); + assert_eq!(sink.transport_id(), TransportId::RETICULUM); + assert!(!source.is_configured()); + assert!(!sink.is_configured()); + assert_eq!(source.maturity(), Maturity::Preview); + assert_eq!(sink.maturity(), Maturity::Preview); + assert_eq!(source.availability(), Availability::Unavailable); + assert_eq!(sink.availability(), Availability::Unavailable); + assert!(!source.capabilities().can_fetch()); + assert!(!sink.capabilities().can_deliver()); + assert!(profile.targets().is_none()); + assert!(profile.satisfaction().is_none()); + } + + #[test] + fn local_and_preview_profiles_never_substitute_fallback_targets() { + let local = Profile::local_only(); + let preview = Profile::unavailable_preview(TransportId::RETICULUM); + assert!(local.is_local_only()); + assert!(local.targets().is_none()); + assert!(preview.targets().is_none()); + + let selected = TargetSet::new(vec![target(7)]).expect("selected targets"); + let profile = Profile::delivery( + selected.clone(), + SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), + ) + .expect("profile"); + assert_eq!(profile.targets(), Some(&selected)); + assert!( + profile + .targets() + .expect("targets") + .targets() + .iter() + .all(|target| *target.kind() == TransportId::NOSTR) + ); + } + + #[test] + fn default_profile_is_local_only() { + assert_eq!(Profile::default(), Profile::local_only()); + } + + #[cfg(feature = "radrootsd")] + #[test] + fn daemon_configuration_is_inert_explicit_and_redacted() { + let config = DaemonConfig::new("http://127.0.0.1:1/rpc") + .with_auth(DaemonAuth::BearerToken("secret-token".to_owned())) + .with_timeout(core::time::Duration::from_millis(5)); + let adapter = DaemonDelivery::new(config); + + let debug = format!("{adapter:?}"); + assert!(!debug.contains("secret-token")); + assert!(!debug.contains("reqwest")); + assert_eq!(format!("{:?}", DaemonAuth::None), "None"); + assert_eq!( + format!("{:?}", DaemonAuth::BearerToken("private".to_owned())), + "BearerToken(<redacted>)" + ); + } + + #[cfg(feature = "radrootsd")] + #[test] + fn daemon_errors_are_stably_classified_and_redacted() { + use std::error::Error as _; + + use crate::adapters::radrootsd::RadrootsdError; + + let cases = [ + ( + RadrootsdError::InvalidAuthHeader("private".to_owned()), + DaemonErrorKind::Authentication, + "daemon authentication configuration is invalid", + ), + ( + RadrootsdError::InvalidRequest("private".to_owned()), + DaemonErrorKind::InvalidRequest, + "daemon delivery request is invalid", + ), + ( + RadrootsdError::Http("private".to_owned()), + DaemonErrorKind::Transport, + "daemon transport failed", + ), + ( + RadrootsdError::JsonRpc { + code: -1, + message: "private".to_owned(), + }, + DaemonErrorKind::Rpc, + "daemon RPC failed", + ), + ( + RadrootsdError::MalformedResponse("private".to_owned()), + DaemonErrorKind::InvalidResponse, + "daemon response is invalid", + ), + ]; + for (private, kind, display) in cases { + let error = DaemonError::from_private(private); + assert_eq!(error.kind(), kind); + assert_eq!(error.to_string(), display); + assert!(error.source().is_some()); + assert!(!format!("{error:?}").contains("private")); + } + } + + #[cfg(feature = "nostr")] + #[test] + fn nostr_slot_reconfiguration_is_validated_atomic_and_inert() { + let slot = NostrSlot::new(RelayUrlPolicy::Local); + assert!(slot.targets().is_none()); + assert!(slot.configure(["ws://127.0.0.1:7447"]).is_ok()); + let original = slot.targets().expect("configured targets"); + assert!(slot.configure(Vec::<String>::new()).is_err()); + assert_eq!(slot.targets(), Some(original)); + slot.clear(); + assert!(slot.targets().is_none()); + assert!(format!("{slot:?}").contains("configured: false")); + } + + #[cfg(feature = "nostr")] + #[test] + fn poisoned_nostr_slot_fails_closed_for_every_host_operation() { + let slot = NostrSlot::new(RelayUrlPolicy::Local); + let state = Arc::clone(&slot.state); + let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _guard = state.write().expect("write lock"); + panic!("poison transport slot"); + })); + + slot.clear(); + assert!(slot.targets().is_none()); + assert!(slot.configure(["ws://127.0.0.1:7447"]).is_err()); + } + + #[cfg(feature = "nostr")] + #[tokio::test] + async fn empty_nostr_slot_reports_unavailable_and_rejects_operations() { + use radroots_transport::{ + DeliveryRequest, EventSink as _, EventSource as _, FetchRequest, sink::DeliveryPayload, + source::FetchBounds, + }; + + let slot = NostrSlot::new(RelayUrlPolicy::Local); + let source = radroots_transport::EventSource::status(&slot) + .await + .expect("source status"); + assert_eq!(source.availability(), Availability::Unavailable); + + let targets = TargetSet::new(vec![target(1)]).expect("targets"); + let fetch = FetchRequest::new( + "fetch", + targets.clone(), + FetchBounds::new(1, 1).expect("bounds"), + ) + .expect("fetch"); + assert_eq!(slot.fetch(fetch).await, Err(Error::UnsupportedOperation)); + + let sink = radroots_transport::EventSink::status(&slot) + .await + .expect("sink status"); + assert_eq!(sink.availability(), Availability::Unavailable); + let deliver = DeliveryRequest::new( + "deliver", + DeliveryPayload::new(signed_event()), + targets, + SatisfactionPolicy::new(SatisfactionClass::Accepted, TargetPolicy::any()), + 1, + ) + .expect("delivery"); + let failure = slot.deliver(deliver).await.expect_err("unconfigured sink"); + assert_eq!(failure.code(), "nostr_transport_not_configured"); + } +} diff --git a/crates/sdk/tests/fixtures/publication.toml b/crates/sdk/tests/fixtures/publication.toml @@ -0,0 +1,67 @@ +[publication] +frozen = false +registry = "crates-io" +final_enablement_step = 305 +spec_id = "radroots.crates.release.v1" +approved_packages = [ + "radroots_core", + "radroots_identity", + "radroots_blossom", + "radroots_protocol", + "radroots_event", + "radroots_event_codec", + "radroots_trade", + "radroots_signing", + "radroots_transport", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_secrets", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_transport_nostr", + "radroots_sync", + "radroots_geonames", + "radroots_sdk", + "radroots", +] +local_packages = ["radroots_sdk", "radroots"] +external_packages = [ + "radroots_core", + "radroots_identity", + "radroots_blossom", + "radroots_protocol", + "radroots_event", + "radroots_event_codec", + "radroots_trade", + "radroots_signing", + "radroots_transport", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_secrets", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_transport_nostr", + "radroots_sync", + "radroots_geonames", +] + +[workspace_classification] +private = ["radroots_sdk_ffi"] +build_codegen = [ + "radroots_core_bindings", + "radroots_event_bindings", + "radroots_event_codec_wasm", + "radroots_identity_bindings", + "radroots_replica_schema_bindings", + "radroots_replica_store_wasm", + "radroots_replica_sync_wasm", + "radroots_sdk_sql_wasm_runtime", + "radroots_sdk_xtask", + "radroots_trade_bindings", +] +test_support = [] +preview = [] +retired = [] + +[publish_order] +crates = ["radroots_sdk", "radroots"] diff --git a/crates/sdk/tests/lifecycle.rs b/crates/sdk/tests/lifecycle.rs @@ -0,0 +1,156 @@ +#[cfg(feature = "memory")] +use radroots_sdk::{ClientBuilder, capability::CapabilityId, error::ErrorKind}; + +const CLIENT_SOURCE: &str = include_str!("../src/client.rs"); +const MANIFEST: &str = include_str!("../Cargo.toml"); + +#[test] +fn clean_default_path_contains_no_implicit_resource_or_worker_authority() { + let production = CLIENT_SOURCE + .split_once("#[cfg(all(test, feature = \"memory\"))]") + .expect("production client boundary") + .0; + for forbidden in [ + "keyring", + "reqwest", + "radrootsd", + "std::fs", + "tokio::fs", + "tokio::spawn", + "std::thread::spawn", + "Runtime::new", + "File::create", + "impl Drop for Client", + ] { + assert!( + !production.contains(forbidden), + "clean client path contains implicit authority `{forbidden}`" + ); + } + assert!(MANIFEST.contains("default = [\"memory\"]")); + assert!(!MANIFEST.contains("default = [\"native\"]")); + assert!(!MANIFEST.contains("default = [\"full\"]")); +} + +#[cfg(feature = "memory")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn memory_client_is_passive_clone_shared_and_explicitly_closed() { + let generation = + radroots_storage::event::SourceGeneration::new([41; 32]).expect("non-zero generation"); + let client = ClientBuilder::memory(generation).build().expect("client"); + + let first_report = client.capabilities(); + let second_report = client.capabilities(); + assert_eq!(first_report, second_report); + let storage = first_report + .get(CapabilityId::CANONICAL_STORAGE) + .expect("canonical storage status"); + assert!(storage.is_compiled()); + assert!(storage.is_configured()); + + let unpolled_close = client.close(); + drop(unpolled_close); + assert!(client.storage().is_ok()); + + let first = client.clone(); + let second = client.clone(); + let first_close = tokio::spawn(async move { first.close().await }); + let second_close = tokio::spawn(async move { second.close().await }); + let outcomes = [ + first_close.await.expect("first close task"), + second_close.await.expect("second close task"), + ]; + assert!(outcomes.iter().all(|outcome| { + outcome.is_ok() + || outcome + .as_ref() + .is_err_and(|error| error.kind() == ErrorKind::CloseInProgress) + })); + if !client.is_closed() { + client.close().await.expect("complete close"); + } + + assert!(client.is_closed()); + assert_eq!( + client + .storage() + .err() + .expect("closed client rejects storage") + .kind(), + ErrorKind::ClientClosed + ); + assert_eq!( + client + .capabilities() + .get(CapabilityId::CANONICAL_STORAGE) + .expect("closed storage status") + .availability(), + radroots_sdk::capability::Availability::Unavailable + ); +} + +#[cfg(all(feature = "memory", feature = "sqlite"))] +#[tokio::test] +async fn sqlite_resources_exist_only_after_explicit_open_and_close_across_clones() { + use radroots_sdk::storage::{SqliteOpenMode, SqliteOptions, SqlitePaths}; + use radroots_storage::status::{ShutdownState, StorageBackend}; + + let directory = tempfile::tempdir().expect("temporary directory"); + let empty_builder = ClientBuilder::new(); + assert!( + directory + .path() + .read_dir() + .expect("read tempdir") + .next() + .is_none() + ); + drop(empty_builder); + + let paths = SqlitePaths::from_directory(directory.path()).expect("SQLite paths"); + let generation = + radroots_storage::event::SourceGeneration::new([42; 32]).expect("non-zero generation"); + let options = SqliteOptions::new(paths, SqliteOpenMode::Create) + .with_source_generation(generation, 1) + .expect("source generation"); + assert!( + directory + .path() + .read_dir() + .expect("read tempdir") + .next() + .is_none() + ); + + let client = ClientBuilder::sqlite(options) + .await + .expect("explicit SQLite open") + .build() + .expect("client"); + assert!( + directory + .path() + .read_dir() + .expect("read tempdir") + .next() + .is_some() + ); + let status = client.storage_status().await.expect("storage status"); + assert_eq!(status.backend(), StorageBackend::Sqlite); + assert_eq!(status.shutdown(), ShutdownState::Open); + + let clone = client.clone(); + let unpolled_close = clone.close(); + drop(unpolled_close); + assert!(client.storage().is_ok()); + client.close().await.expect("explicit close"); + assert!(clone.is_closed()); + assert_eq!( + clone + .storage_status() + .await + .expect_err("closed clone rejects inspection") + .kind(), + ErrorKind::ClientClosed + ); +} diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs @@ -0,0 +1,554 @@ +use std::collections::BTreeSet; + +const MANIFEST: &str = include_str!("../Cargo.toml"); +const ROOT: &str = include_str!("../src/lib.rs"); +const CLIENT: &str = include_str!("../src/client.rs"); +const FARM: &str = include_str!("../src/farm.rs"); +const LISTING: &str = include_str!("../src/listing.rs"); +const TRADE: &str = include_str!("../src/trade.rs"); +const STORAGE: &str = include_str!("../src/storage.rs"); +const DIAGNOSTICS: &str = include_str!("../src/diagnostics.rs"); +const ADAPTERS: &str = include_str!("../src/adapters/mod.rs"); +const RADROOTSD: &str = include_str!("../src/adapters/radrootsd.rs"); +const SYNC: &str = include_str!("../src/sync.rs"); +const TRANSPORT: &str = include_str!("../src/transport.rs"); +const PUBLICATION: &str = include_str!("fixtures/publication.toml"); + +#[test] +fn manifest_has_final_identity_and_dependency_boundary() { + assert!(MANIFEST.contains("name = \"radroots_sdk\"")); + assert!(MANIFEST.contains("publish = [\"crates-io\"]")); + assert!(MANIFEST.contains("version.workspace = true")); + assert!(MANIFEST.contains("name = \"package_boundary\"")); + + let dependencies = dependency_names(MANIFEST); + let expected = BTreeSet::from([ + "radroots_core", + "radroots_event", + "radroots_event_codec", + "radroots_geonames", + "radroots_identity", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_protocol", + "radroots_secrets", + "radroots_signing", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_sync", + "radroots_trade", + "radroots_transport", + "radroots_transport_nostr", + ]); + assert_eq!(dependencies, expected); + for forbidden in [ + "radroots_event_store", + "radroots_nostr_signer", + "radroots_outbox", + "radroots_protected_store", + "radroots_runtime_paths", + "radroots_secret_vault", + "radroots_transport_reticulum", + ] { + assert!( + !dependencies.contains(forbidden), + "SDK depends on private or superseded package `{forbidden}`" + ); + } +} + +#[test] +fn manifest_has_exact_feature_vocabulary_and_explicit_optional_activation() { + let features = MANIFEST + .split_once("[features]") + .expect("feature section") + .1 + .split_once("[dependencies]") + .expect("dependency section") + .0 + .lines() + .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim())) + .collect::<BTreeSet<_>>(); + assert_eq!( + features, + BTreeSet::from([ + "default", + "full", + "geonames", + "knowledge", + "local-signing", + "memory", + "native", + "nip46", + "nostr", + "radrootsd", + "sqlite", + "sync", + ]) + ); + for activation in [ + "dep:radroots_storage_sqlite", + "dep:radroots_sync", + "dep:radroots_nostr", + "dep:radroots_transport_nostr", + "dep:radroots_nostr_connect", + "dep:radroots_secrets", + "dep:reqwest", + "dep:serde", + "dep:serde_json", + "dep:radroots_geonames", + ] { + assert!( + MANIFEST.contains(activation), + "missing activation `{activation}`" + ); + } + for retired in [ + "runtime =", + "local-runtime", + "signer-adapters", + "transport-nostr-runtime", + "transport-nostr-client", + "fixtures =", + ] { + assert!(!MANIFEST.contains(retired), "retired feature `{retired}`"); + } +} + +#[test] +fn root_declares_exact_final_module_skeleton() { + let actual = ROOT + .lines() + .filter_map(|line| line.trim().strip_prefix("pub mod ")) + .map(|module| module.trim_end_matches(';')) + .collect::<BTreeSet<_>>(); + let expected = BTreeSet::from([ + "capability", + "client", + "diagnostics", + "error", + "farm", + "listing", + "signing", + "storage", + "sync", + "trade", + "transport", + ]); + assert_eq!(actual, expected); + assert!(!ROOT.contains("no_std")); + assert_eq!(ROOT.matches("pub use crate::").count(), 2); + assert!(ROOT.contains("pub use crate::client::{Client, ClientBuilder};")); + assert!(ROOT.contains("pub use crate::error::{Error, Result};")); + assert!(!ROOT.contains("pub use radroots_")); +} + +#[test] +fn package_contains_only_reachable_sources_and_registered_targets() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + assert_eq!( + rust_files(&root.join("src")), + BTreeSet::from([ + "adapters/mod.rs".to_owned(), + "adapters/radrootsd.rs".to_owned(), + "capability.rs".to_owned(), + "client.rs".to_owned(), + "diagnostics.rs".to_owned(), + "error.rs".to_owned(), + "farm.rs".to_owned(), + "lib.rs".to_owned(), + "listing.rs".to_owned(), + "listing/operational_listing/draft.rs".to_owned(), + "listing/operational_listing/mod.rs".to_owned(), + "listing/operational_listing/model.rs".to_owned(), + "listing/operational_listing/mutation.rs".to_owned(), + "listing/operational_listing/price_ext.rs".to_owned(), + "listing/operational_listing/validation.rs".to_owned(), + "signing.rs".to_owned(), + "storage.rs".to_owned(), + "sync.rs".to_owned(), + "trade.rs".to_owned(), + "transport.rs".to_owned(), + ]) + ); + assert_eq!( + rust_files(&root.join("tests")), + BTreeSet::from([ + "lifecycle.rs".to_owned(), + "package_boundary.rs".to_owned(), + "public_api.rs".to_owned(), + "unit/adapters_radrootsd_tests.rs".to_owned(), + ]) + ); + assert_eq!( + rust_files(&root.join("examples")), + BTreeSet::from([ + "safe_memory_client.rs".to_owned(), + "transport_profile.rs".to_owned(), + ]) + ); +} + +#[test] +fn compatibility_packages_are_removed() { + assert!(PUBLICATION.contains("retired = []")); + let approved = PUBLICATION + .split_once("approved_packages = [") + .expect("approved package list") + .1 + .split_once("\n]") + .expect("approved package list terminator") + .0; + assert!(!approved.contains("radroots_runtime_contract_v1")); + let workspace = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(std::path::Path::parent) + .expect("workspace root"); + assert!(!workspace.join("crates/runtime_contract_v1").exists()); + assert!(!workspace.join("crates/event_index_bindings").exists()); +} + +#[test] +fn root_types_have_the_required_std_contracts() { + fn assert_client<T: Clone + Send + Sync>() {} + fn assert_builder<T: Send + Sync>() {} + fn assert_error<T: std::error::Error + Send + Sync + 'static>() {} + + assert_client::<radroots_sdk::Client>(); + assert_builder::<radroots_sdk::ClientBuilder>(); + assert_error::<radroots_sdk::Error>(); + let result: radroots_sdk::Result<()> = Ok(()); + assert!(result.is_ok()); +} + +#[cfg(feature = "sqlite")] +#[test] +fn sqlite_backend_implements_the_canonical_storage_capability() { + fn assert_storage<T: radroots_storage::Storage>() {} + assert_storage::<radroots_storage_sqlite::SqliteStorage>(); +} + +#[cfg(feature = "nostr")] +#[test] +fn nostr_adapter_implements_independent_source_and_sink_capabilities() { + fn assert_source<T: radroots_transport::EventSource>() {} + fn assert_sink<T: radroots_transport::EventSink>() {} + assert_source::<radroots_transport_nostr::NostrTransport>(); + assert_sink::<radroots_transport_nostr::NostrTransport>(); +} + +#[test] +fn lifecycle_source_owns_no_hidden_worker_runtime_or_blocking_drop() { + for forbidden in [ + "tokio::spawn", + "std::thread::spawn", + "Runtime::new", + "block_on(self.close", + "impl Drop for Client", + ] { + assert!( + !CLIENT.contains(forbidden), + "forbidden lifecycle source `{forbidden}`" + ); + } + assert!(CLIENT.contains("pub async fn close(&self)")); + assert!(CLIENT.contains("impl Drop for CloseAttempt")); +} + +#[test] +fn sdk_source_contains_no_studio_storage_surface() { + let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut pending = vec![source_root]; + while let Some(path) = pending.pop() { + for entry in std::fs::read_dir(path).expect("read SDK source") { + let entry = entry.expect("source entry"); + let path = entry.path(); + if path.is_dir() { + pending.push(path); + } else if path.extension().and_then(|value| value.to_str()) == Some("rs") { + let source = std::fs::read_to_string(&path).expect("read source file"); + for forbidden in [ + "studio.sqlite", + "SdkStudioStore", + "sdk_studio_state", + "studio_store", + ] { + assert!( + !source.contains(forbidden), + "{} contains retired Studio storage marker {forbidden}", + path.display() + ); + } + } + } + } +} + +#[test] +fn transport_profiles_reuse_canonical_types_and_forbid_fallback() { + assert!(TRANSPORT.contains("use radroots_transport::{")); + assert!(TRANSPORT.contains("satisfaction.validate_for(&targets)?")); + assert!(TRANSPORT.contains("Selection::UnavailablePreview")); + for duplicate in [ + "pub struct TargetSet", + "pub enum TargetPolicy", + "pub enum SatisfactionPolicy", + "pub struct SourceStatus", + "pub struct SinkStatus", + "DefaultProfile", + ] { + assert!( + !TRANSPORT.contains(duplicate), + "SDK transport source contains forbidden duplicate or fallback `{duplicate}`" + ); + } +} + +#[test] +fn sync_operations_only_delegate_to_the_canonical_engine() { + for delegation in [ + "self.engine.pull(request, admission).await", + "self.engine.ingest(observed, admission).await", + "self.engine.ingest_batch(observed, admission).await", + "self.engine.refresh_projection(request, reducer).await", + "self.engine.prepare_push(request).await", + "self.engine.push_status(operation_id).await", + "self.engine.sign_prepared(request).await", + "self.engine.admit_signed(operation_id).await", + "self.engine.deliver_push(operation_id).await", + "self.engine.status(projections).await", + "self.engine.retry_decision(plan, now_unix_ms)", + ] { + assert!( + SYNC.contains(delegation), + "missing sync delegation `{delegation}`" + ); + } + for duplicate in [ + "pub struct SyncTransport", + "pub struct SyncOutbox", + "pub struct SyncProjection", + "pub struct SyncStatus", + "pub struct PushOutbox", + ] { + assert!( + !SYNC.contains(duplicate), + "SDK sync source contains duplicate lower model `{duplicate}`" + ); + } + assert!( + !std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/sync_runtime.rs") + .exists() + ); +} + +#[test] +fn authored_submission_is_one_protocol_neutral_boundary_for_all_typed_contracts() { + use radroots_event_codec::authoring::REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS; + + assert_eq!(REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS.len(), 8); + assert!(SYNC.contains("pub async fn submit_push")); + assert!(SYNC.contains("request: PushRequest")); + assert!(SYNC.contains("Result<PushStatus, Error>")); + for contract_id in REGISTRY_V7_TYPED_AUTHORING_CONTRACT_IDS { + assert!( + !SYNC.contains(contract_id), + "SDK submission must not branch on typed contract `{contract_id}`" + ); + } +} + +#[test] +fn farm_operations_preserve_pure_planning_commit_and_privacy_boundaries() { + for required in [ + "encode::farm::to_wire_parts", + "AddressableCoordinate", + "GenericEventDraft", + "AuthoredEventPlan", + "radroots_sync::PushRequest::new", + "self.sync", + ".submit_push(", + "PrivateArtifactStore", + ] { + assert!( + FARM.contains(required), + "missing farm boundary `{required}`" + ); + } + for forbidden in [ + "SdkExactLocation", + "SdkPublicLocality", + "latitude:", + "longitude:", + "enqueue_signed_workflow", + "local_event_seq", + "outbox_event_id", + ] { + assert!( + !FARM.contains(forbidden), + "farm source contains retired SDK or private representation `{forbidden}`" + ); + } + let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + assert!(!source_root.join("farms_runtime.rs").exists()); +} + +#[test] +fn listing_operations_reuse_trade_event_sync_and_privacy_boundaries() { + for required in [ + "canonicalize_operational_listing_edit", + "RadrootsOperationalListingMutation", + "RadrootsOperationalListingLifecycleState", + "build_operational_listing_mutation_plan", + "radroots_sync::PushRequest::new", + "PrivateArtifactStore", + ] { + assert!( + LISTING.contains(required), + "missing listing boundary `{required}`" + ); + } + for forbidden in [ + "SdkMutationState", + "ListingEnqueueReceipt", + "enqueue_signed_workflow", + "local_event_seq", + "outbox_event_id", + "latitude:", + "longitude:", + ] { + assert!( + !LISTING.contains(forbidden), + "listing source contains retired duplicate or private representation `{forbidden}`" + ); + } + assert!( + !std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/listings_runtime.rs") + .exists() + ); +} + +#[test] +fn trade_operations_use_canonical_workflow_storage_sync_and_projection_types() { + for required in [ + "WorkflowPlan", + "TradeMutationEnvelopeV1", + "ReductionInput", + "Projection", + "reduce_trade_records", + "EventQuery", + "EventPage<StoredVisibleEvent>", + "PrivateArtifactMetadata", + "radroots_sync::PushRequest::new", + ] { + assert!( + TRADE.contains(required), + "missing trade boundary `{required}`" + ); + } + for forbidden in [ + "sqlx::", + "QueryBuilder", + "TradeStatusView", + "SdkPrivateTradeArtifact", + "SdkMutationState", + "TradeCommandReceipt", + "struct Page", + "struct TradeId", + ] { + assert!( + !TRADE.contains(forbidden), + "trade source contains retired duplicate `{forbidden}`" + ); + } + assert!( + !std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/trade_runtime.rs") + .exists() + ); +} + +#[test] +fn reliability_and_diagnostics_return_canonical_storage_contracts() { + for required in [ + "StorageReliability::begin_backup", + "StorageReliability::transition_backup", + "StorageReliability::begin_restore", + "StorageReliability::transition_restore", + "StorageReliability::integrity", + "StorageReliability::status", + ] { + assert!( + STORAGE.contains(required), + "missing storage delegation `{required}`" + ); + } + for forbidden in [ + "struct BackupManifest", + "struct IntegrityStatus", + "Studio", + "sqlx::", + ] { + assert!( + !STORAGE.contains(forbidden), + "storage source duplicates `{forbidden}`" + ); + } + assert!(DIAGNOSTICS.contains("radroots_storage::StorageStatus")); + for forbidden in ["path:", "SqlitePool", "private_artifact"] { + assert!( + !DIAGNOSTICS.contains(forbidden), + "diagnostics leaks `{forbidden}`" + ); + } +} + +#[test] +fn radrootsd_adapter_is_private_explicit_versioned_and_redacted() { + assert!(ROOT.contains("mod adapters;")); + assert!(!ROOT.contains("pub mod adapters")); + assert!(ADAPTERS.contains("cfg(feature = \"radrootsd\")")); + assert!(ADAPTERS.contains("pub(crate) mod radrootsd")); + assert!(RADROOTSD.contains("transport_publish::v5")); + assert!(RADROOTSD.contains("reqwest::Client::builder")); + assert!(RADROOTSD.contains("BearerToken(<redacted>)")); + assert!(!RADROOTSD.contains("tokio::spawn")); +} + +fn dependency_names(manifest: &str) -> BTreeSet<&str> { + let dependencies = manifest + .split_once("[dependencies]") + .expect("dependency section") + .1 + .split_once("[[test]]") + .expect("test section") + .0; + dependencies + .lines() + .filter_map(|line| line.split_once(" = ").map(|(name, _)| name.trim())) + .filter(|name| name.starts_with("radroots_")) + .collect() +} + +fn rust_files(root: &std::path::Path) -> BTreeSet<String> { + let mut files = BTreeSet::new(); + let mut pending = vec![root.to_path_buf()]; + while let Some(directory) = pending.pop() { + for entry in std::fs::read_dir(&directory).expect("read package source directory") { + let path = entry.expect("read package source entry").path(); + if path.is_dir() { + pending.push(path); + } else if path.extension().and_then(|value| value.to_str()) == Some("rs") { + files.insert( + path.strip_prefix(root) + .expect("source remains below root") + .to_string_lossy() + .replace(std::path::MAIN_SEPARATOR, "/"), + ); + } + } + } + files +} diff --git a/crates/sdk/tests/public_api.rs b/crates/sdk/tests/public_api.rs @@ -0,0 +1,172 @@ +use std::{any::type_name, collections::BTreeSet}; + +const ACTIVE_MODULES: &[(&str, &str)] = &[ + ("capability", include_str!("../src/capability.rs")), + ("client", include_str!("../src/client.rs")), + ("diagnostics", include_str!("../src/diagnostics.rs")), + ("error", include_str!("../src/error.rs")), + ("farm", include_str!("../src/farm.rs")), + ("listing", include_str!("../src/listing.rs")), + ("signing", include_str!("../src/signing.rs")), + ("storage", include_str!("../src/storage.rs")), + ("sync", include_str!("../src/sync.rs")), + ("trade", include_str!("../src/trade.rs")), + ("transport", include_str!("../src/transport.rs")), +]; + +#[test] +fn public_native_type_snapshot_uses_contextual_names() { + let actual = BTreeSet::from([ + type_name::<radroots_sdk::Client>(), + type_name::<radroots_sdk::ClientBuilder>(), + type_name::<radroots_sdk::capability::Availability>(), + type_name::<radroots_sdk::capability::CapabilityId>(), + type_name::<radroots_sdk::capability::CapabilityReport>(), + type_name::<radroots_sdk::capability::CapabilityStatus>(), + type_name::<radroots_sdk::capability::Maturity>(), + type_name::<radroots_sdk::diagnostics::Report>(), + type_name::<radroots_sdk::error::Error>(), + type_name::<radroots_sdk::error::ErrorDescriptor>(), + type_name::<radroots_sdk::error::ErrorKind>(), + type_name::<radroots_sdk::farm::Plan>(), + type_name::<radroots_sdk::farm::PrepareError>(), + type_name::<radroots_sdk::farm::PrepareErrorKind>(), + type_name::<radroots_sdk::farm::PrepareRequest>(), + type_name::<radroots_sdk::listing::Action>(), + type_name::<radroots_sdk::listing::Plan>(), + type_name::<radroots_sdk::listing::PrepareError>(), + type_name::<radroots_sdk::listing::PrepareErrorKind>(), + type_name::<radroots_sdk::listing::PrepareRequest>(), + type_name::<radroots_sdk::signing::Mode>(), + type_name::<radroots_sdk::signing::Provider>(), + type_name::<radroots_sdk::storage::Operations<'static>>(), + type_name::<radroots_sdk::trade::Plan>(), + type_name::<radroots_sdk::trade::PrepareError>(), + type_name::<radroots_sdk::trade::PrepareErrorKind>(), + type_name::<radroots_sdk::trade::PrepareRequest>(), + type_name::<radroots_sdk::transport::Profile>(), + ]); + #[cfg(feature = "sync")] + let actual = actual + .into_iter() + .chain([ + type_name::<radroots_sdk::farm::EnqueueRequest>(), + type_name::<radroots_sdk::farm::Operations<'static>>(), + type_name::<radroots_sdk::listing::EnqueueRequest>(), + type_name::<radroots_sdk::listing::Operations<'static>>(), + type_name::<radroots_sdk::sync::Operations<'static>>(), + type_name::<radroots_sdk::sync::HostPolicy>(), + type_name::<radroots_sdk::trade::EnqueueRequest>(), + type_name::<radroots_sdk::trade::Operations<'static>>(), + type_name::<radroots_sdk::trade::PrivateTermsError>(), + ]) + .collect::<BTreeSet<_>>(); + #[cfg(feature = "local-signing")] + let actual = actual + .into_iter() + .chain([ + type_name::<radroots_sdk::signing::LocalIdentity>(), + type_name::<radroots_sdk::signing::Slot>(), + ]) + .collect::<BTreeSet<_>>(); + #[cfg(feature = "nostr")] + let actual = actual + .into_iter() + .chain([type_name::<radroots_sdk::transport::NostrSlot>()]) + .collect::<BTreeSet<_>>(); + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + let actual = actual + .into_iter() + .chain([ + type_name::<radroots_sdk::client::PostEvent>(), + type_name::<radroots_sdk::client::ProfileDraft>(), + type_name::<radroots_sdk::client::ProfileEvent>(), + type_name::<radroots_sdk::client::PublishReceipt>(), + type_name::<radroots_sdk::client::SocialOperations<'static>>(), + type_name::<radroots_sdk::client::TransportHealth>(), + ]) + .collect::<BTreeSet<_>>(); + #[cfg(feature = "radrootsd")] + let actual = actual + .into_iter() + .chain([ + type_name::<radroots_sdk::transport::DaemonAuth>(), + type_name::<radroots_sdk::transport::DaemonConfig>(), + type_name::<radroots_sdk::transport::DaemonDelivery>(), + type_name::<radroots_sdk::transport::DaemonError>(), + type_name::<radroots_sdk::transport::DaemonErrorKind>(), + ]) + .collect::<BTreeSet<_>>(); + + assert!(actual.iter().all(|name| !name.contains("RadrootsSdk"))); + assert!(actual.iter().all(|name| { + name.rsplit("::") + .next() + .is_some_and(|item| !item.starts_with("Sdk")) + })); + assert_eq!(actual.len(), expected_public_type_count()); +} + +#[test] +fn active_native_api_has_no_sdk_owned_traits_or_public_field_layout() { + for (module, source) in ACTIVE_MODULES { + for line in source.lines() { + let line = line.trim_start(); + assert!( + !line.starts_with("pub trait "), + "{module} must reuse lower host SPIs instead of exposing an SDK-owned trait" + ); + for declaration in ["pub struct ", "pub enum ", "pub trait ", "pub type "] { + if let Some(item) = line.strip_prefix(declaration) { + let item = item + .split(|character: char| { + character == '<' + || character == '(' + || character == '{' + || character == ';' + || character.is_whitespace() + }) + .next() + .expect("public item name"); + assert!( + !item.starts_with("RadrootsSdk") && !item.starts_with("Sdk"), + "{module} exposes representation-prefixed item {item}" + ); + } + } + + let public_field = line.starts_with("pub ") + && line.contains(':') + && ![ + "pub async fn ", + "pub const ", + "pub enum ", + "pub fn ", + "pub mod ", + "pub static ", + "pub struct ", + "pub trait ", + "pub type ", + "pub use ", + ] + .iter() + .any(|prefix| line.starts_with(prefix)); + assert!(!public_field, "{module} exposes native field `{line}`"); + } + } +} + +const fn expected_public_type_count() -> usize { + let count = 28; + #[cfg(feature = "sync")] + let count = count + 9; + #[cfg(feature = "local-signing")] + let count = count + 2; + #[cfg(feature = "nostr")] + let count = count + 1; + #[cfg(all(feature = "sync", feature = "nostr", feature = "local-signing"))] + let count = count + 6; + #[cfg(feature = "radrootsd")] + let count = count + 5; + count +} diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs @@ -0,0 +1,940 @@ +use super::*; +use radroots_event::wire::Nip01EventWire; +use radroots_protocol::radrootsd::transport_publish::v5::{ + DeliveryPolicy as TransportPublishDeliveryPolicy, EventRequest as TransportPublishEventRequest, + EventResponse as TransportPublishEventResponse, Job as TransportPublishJobView, + JobStatus as TransportPublishJobStatus, + NostrTargetSourcePolicy as NostrPublishTargetSourcePolicy, + OutcomeKind as TransportPublishOutcomeKind, + ReticulumBehavior as TransportPublishReticulumBehavior, Target as TransportPublishTarget, + TargetOutcome as TransportPublishTargetOutcome, TargetPolicy as TransportPublishTargetPolicy, + TargetSource as TransportPublishTargetSource, +}; +const RADROOTS_RETICULUM_ENDPOINT_URI: &str = "reticulum:local"; +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread::JoinHandle; + +const SIGNED_EVENT_PUBLIC_KEY: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; + +struct RecordedHttpRequest { + request_line: String, + headers: Vec<(String, String)>, + body: String, +} + +fn spawn_http_server( + status: &str, + response_body: &str, +) -> (String, JoinHandle<RecordedHttpRequest>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let endpoint = format!("http://{}/rpc", listener.local_addr().expect("addr")); + let status = status.to_owned(); + let response_body = response_body.to_owned(); + let content_length = response_body.len(); + let handle = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept"); + let mut request = Vec::new(); + let mut buffer = [0u8; 1024]; + loop { + let read = stream.read(&mut buffer).expect("read request"); + if read == 0 { + break; + } + request.extend_from_slice(&buffer[..read]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + let headers_end = request + .windows(4) + .position(|window| window == b"\r\n\r\n") + .expect("headers end") + + 4; + let header_text = String::from_utf8_lossy(&request[..headers_end]); + let request_content_length = header_text + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::<usize>().expect("content length")) + }) + .unwrap_or(0); + while request.len() < headers_end + request_content_length { + let read = stream.read(&mut buffer).expect("read body"); + if read == 0 { + break; + } + request.extend_from_slice(&buffer[..read]); + } + break; + } + } + let request_text = String::from_utf8_lossy(&request); + let (headers_text, body) = request_text.split_once("\r\n\r\n").expect("request body"); + let mut header_lines = headers_text.lines(); + let request_line = header_lines.next().expect("request line").to_owned(); + let headers = header_lines + .filter_map(|line| { + let (name, value) = line.split_once(':')?; + Some((name.to_ascii_lowercase(), value.trim().to_owned())) + }) + .collect::<Vec<_>>(); + let response = format!( + "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {content_length}\r\nconnection: close\r\n\r\n{response_body}", + ); + stream + .write_all(response.as_bytes()) + .expect("write response"); + RecordedHttpRequest { + request_line, + headers, + body: body.to_owned(), + } + }); + (endpoint, handle) +} + +fn signed_event() -> SignedEvent { + let mut wire = Nip01EventWire { + id: String::new(), + pubkey: SIGNED_EVENT_PUBLIC_KEY.to_owned(), + created_at: 1_700_000_000, + kind: 30_402, + tags: vec![vec!["d".to_owned(), "listing-1".to_owned()]], + content: "{\"name\":\"carrots\"}".to_owned(), + sig: "c".repeat(128), + extra: Default::default(), + }; + wire.id = wire.computed_event_id().expect("event id").into_string(); + let raw_json = serde_json::to_string(&wire).expect("raw event json"); + SignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") +} + +fn publish_request() -> TransportPublishEventRequest { + TransportPublishEventRequest { + raw_event_json: signed_event().raw_json().to_owned(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-1".to_owned()), + timeout_ms: Some(10_000), + } +} + +fn signed_event_id() -> String { + signed_event().id_str().to_owned() +} + +fn signed_event_pubkey() -> String { + signed_event().pubkey().to_hex().to_owned() +} + +fn job_status_for_outcome(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobStatus { + if outcome_kind.counts_toward_accepted_delivery() { + TransportPublishJobStatus::DeliverySatisfied + } else if outcome_kind.is_retryable() { + TransportPublishJobStatus::DeliveryUnsatisfiedRetryable + } else if outcome_kind.is_terminal_failure() { + TransportPublishJobStatus::DeliveryUnsatisfiedTerminal + } else if outcome_kind == TransportPublishOutcomeKind::DeferredUntilImplemented { + TransportPublishJobStatus::DeliveryDeferred + } else { + TransportPublishJobStatus::DeliveryUnsatisfiedRetryable + } +} + +fn job(outcome_kind: TransportPublishOutcomeKind) -> TransportPublishJobView { + let status = job_status_for_outcome(outcome_kind); + TransportPublishJobView { + job_id: "job-1".to_owned(), + status, + terminal: matches!( + status, + TransportPublishJobStatus::DeliverySatisfied + | TransportPublishJobStatus::DeliveryUnsatisfiedTerminal + | TransportPublishJobStatus::DeliveryDeferred + | TransportPublishJobStatus::DeliveryDeferredUntilImplemented + | TransportPublishJobStatus::Rejected + ), + delivery_satisfied: status == TransportPublishJobStatus::DeliverySatisfied, + event_id: signed_event_id(), + pubkey: signed_event_pubkey(), + event_kind: 30_402, + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + target_count: 1, + acknowledged_count: usize::from(outcome_kind.counts_toward_accepted_delivery()), + retryable_count: usize::from(outcome_kind.is_retryable()), + terminal_count: usize::from(outcome_kind.is_terminal_failure()), + requested_at_ms: 1_700_000_000_000, + completed_at_ms: Some(1_700_000_000_100), + last_error: None, + targets: vec![TransportPublishTargetOutcome { + transport_kind: "nostr".to_owned(), + endpoint_uri: "wss://relay.example.com".to_owned(), + target_scope: None, + target_label: None, + source: TransportPublishTargetSource::Request, + attempted: true, + outcome_kind, + message: Some("relay outcome".to_owned()), + latency_ms: Some(7), + }], + } +} + +fn explicit_nostr_job( + endpoints: Vec<String>, + delivery_policy: TransportPublishDeliveryPolicy, +) -> TransportPublishJobView { + let targets = endpoints + .iter() + .map(|endpoint| TransportPublishTargetOutcome { + transport_kind: "nostr".to_owned(), + endpoint_uri: endpoint.clone(), + target_scope: None, + target_label: None, + source: TransportPublishTargetSource::Request, + attempted: true, + outcome_kind: TransportPublishOutcomeKind::Accepted, + message: Some("relay outcome".to_owned()), + latency_ms: Some(7), + }) + .collect::<Vec<_>>(); + TransportPublishJobView { + job_id: "job-explicit-nostr".to_owned(), + status: TransportPublishJobStatus::DeliverySatisfied, + terminal: true, + delivery_satisfied: true, + event_id: signed_event_id(), + pubkey: signed_event_pubkey(), + event_kind: 30_402, + target_policy: TransportPublishTargetPolicy::explicit_targets( + endpoints + .iter() + .map(|endpoint| TransportPublishTarget::nostr(endpoint.as_str())) + .collect::<Vec<_>>(), + ), + delivery_policy, + target_count: targets.len(), + acknowledged_count: targets.len(), + retryable_count: 0, + terminal_count: 0, + requested_at_ms: 1_700_000_000_000, + completed_at_ms: Some(1_700_000_000_100), + last_error: None, + targets, + } +} + +fn reticulum_deferred_job() -> TransportPublishJobView { + TransportPublishJobView { + job_id: "job-reticulum".to_owned(), + status: TransportPublishJobStatus::DeliveryDeferred, + terminal: true, + delivery_satisfied: false, + event_id: signed_event_id(), + pubkey: signed_event_pubkey(), + event_kind: 30_402, + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, + ), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + target_count: 1, + acknowledged_count: 0, + retryable_count: 0, + terminal_count: 0, + requested_at_ms: 1_700_000_000_000, + completed_at_ms: Some(1_700_000_000_100), + last_error: Some("delivery_deferred_until_implemented".to_owned()), + targets: vec![TransportPublishTargetOutcome { + transport_kind: "reticulum".to_owned(), + endpoint_uri: RADROOTS_RETICULUM_ENDPOINT_URI.to_owned(), + target_scope: None, + target_label: None, + source: TransportPublishTargetSource::Reticulum, + attempted: false, + outcome_kind: TransportPublishOutcomeKind::DeferredUntilImplemented, + message: Some("reticulum deferred until implemented".to_owned()), + latency_ms: None, + }], + } +} + +fn publish_response_json_for_job(job: TransportPublishJobView) -> String { + serde_json::json!({ + "jsonrpc": "2.0", + "id": SDK_RADROOTSD_PUBLISH_REQUEST_ID, + "result": { + "deduplicated": false, + "job": job + } + }) + .to_string() +} + +fn publish_response_json() -> String { + publish_response_json_for_job(job(TransportPublishOutcomeKind::Accepted)) +} + +fn reticulum_deferred_response_json() -> String { + serde_json::json!({ + "jsonrpc": "2.0", + "id": SDK_RADROOTSD_PUBLISH_REQUEST_ID, + "result": { + "deduplicated": false, + "job": reticulum_deferred_job() + } + }) + .to_string() +} + +fn assert_message(error: RadrootsdError, fragment: &str) { + let message = error.to_string(); + assert!( + message.contains(fragment), + "expected {message:?} to contain {fragment:?}" + ); +} + +#[test] +fn auth_headers_omit_or_redact_bearer_authorization() { + let none = auth_headers(&RadrootsdAuth::None).expect("none auth"); + assert!(!none.contains_key(AUTHORIZATION)); + assert_eq!(format!("{:?}", RadrootsdAuth::None), "None"); + + let bearer = auth_headers(&RadrootsdAuth::BearerToken("sdk-token".into())).expect("bearer"); + assert_eq!( + bearer + .get(AUTHORIZATION) + .expect("authorization") + .to_str() + .expect("authorization str"), + "Bearer sdk-token" + ); + + let error = auth_headers(&RadrootsdAuth::BearerToken("bad\ntoken".into())).expect_err("error"); + assert!(matches!(error, RadrootsdError::InvalidAuthHeader(_))); + assert_eq!( + format!("{:?}", RadrootsdAuth::BearerToken("token-secret".into())), + "BearerToken(<redacted>)" + ); + assert!( + RadrootsdError::InvalidAuthHeader("bad header".to_owned()) + .to_string() + .contains("invalid radrootsd bearer token header") + ); + assert_eq!( + RadrootsdError::InvalidRequest("invalid request".to_owned()).to_string(), + "invalid request" + ); + assert_eq!( + RadrootsdError::Http("http failed".to_owned()).to_string(), + "http failed" + ); + assert_eq!( + RadrootsdError::MalformedResponse("bad envelope".to_owned()).to_string(), + "bad envelope" + ); +} + +#[test] +fn radrootsd_publish_config_builders_preserve_typed_runtime_options() { + let config = RadrootsdPublishConfig::new("http://127.0.0.1:8080/rpc") + .with_auth(RadrootsdAuth::BearerToken("sdk-token".to_owned())) + .with_timeout(Duration::from_millis(250)); + let adapter = RadrootsdPublishAdapter::new(config.clone()); + + assert_eq!(adapter.config(), &config); + assert_eq!(adapter.config().endpoint, "http://127.0.0.1:8080/rpc"); + assert_eq!( + adapter.config().auth, + RadrootsdAuth::BearerToken("sdk-token".to_owned()) + ); + assert_eq!(adapter.config().timeout, Duration::from_millis(250)); +} + +#[test] +fn publish_event_request_json_uses_signed_event_contract() { + let value = publish_event_request_json(&publish_request()).expect("request json"); + + let raw_event_json = value["raw_event_json"].as_str().expect("raw event json"); + let raw_event: serde_json::Value = serde_json::from_str(raw_event_json).expect("raw event"); + assert_eq!(raw_event["id"], signed_event_id()); + assert_eq!(raw_event["pubkey"], SIGNED_EVENT_PUBLIC_KEY); + assert_eq!(raw_event["kind"], 30_402); + assert_eq!(value["target_policy"]["kind"], "nostr"); + assert_eq!( + value["target_policy"]["source_policy"], + "request_then_author_write_then_daemon_default" + ); + assert_eq!( + value["target_policy"]["relay_urls"][0], + "wss://relay.example.com" + ); + assert_eq!(value["delivery_policy"]["mode"], "any"); + assert_eq!(value["idempotency_key"], "idem-1"); + let rendered = value.to_string(); + assert!(!rendered.contains("signer_session_id")); + assert!(!rendered.contains("bridge.")); +} + +#[test] +fn decode_jsonrpc_response_validates_envelope_and_errors() { + let response: TransportPublishEventResponse = decode_jsonrpc_response( + METHOD_EVENT, + SDK_RADROOTSD_PUBLISH_REQUEST_ID, + publish_response_json().as_str(), + ) + .expect("response"); + assert_eq!(response.job.event_id, signed_event_id()); + + let error = decode_jsonrpc_response::<TransportPublishEventResponse>( + METHOD_EVENT, + SDK_RADROOTSD_PUBLISH_REQUEST_ID, + r#"{"jsonrpc":"2.0","id":"radroots-sdk-transport-publish-event","error":{"code":-32001,"message":"principal unauthorized"}}"#, + ) + .expect_err("jsonrpc error"); + assert!(matches!( + error, + RadrootsdError::JsonRpc { code: -32001, .. } + )); + assert_message(error, "principal unauthorized"); + + assert!(matches!( + decode_jsonrpc_response::<serde_json::Value>(METHOD_EVENT, "expected", "not json"), + Err(RadrootsdError::MalformedResponse(_)) + )); + assert!(matches!( + decode_jsonrpc_response::<serde_json::Value>( + METHOD_EVENT, + "expected", + r#"{"jsonrpc":"2.0","id":"other","result":{}}"# + ), + Err(RadrootsdError::MalformedResponse(_)) + )); + assert!(matches!( + decode_jsonrpc_response::<serde_json::Value>( + METHOD_EVENT, + "expected", + r#"{"jsonrpc":"2.0","id":"expected"}"# + ), + Err(RadrootsdError::MalformedResponse(_)) + )); + assert!(matches!( + decode_jsonrpc_response::<serde_json::Value>( + METHOD_EVENT, + "expected", + r#"{"jsonrpc":"1.0","id":"expected","result":{}}"# + ), + Err(RadrootsdError::MalformedResponse(_)) + )); + assert!(matches!( + decode_jsonrpc_response::<serde_json::Value>( + METHOD_EVENT, + "expected", + r#"{"jsonrpc":"2.0","id":"expected","result":{},"error":{"code":-32002,"message":"both"}}"# + ), + Err(RadrootsdError::MalformedResponse(_)) + )); +} + +#[tokio::test] +async fn publish_event_posts_transport_publish_jsonrpc() { + let (endpoint, handle) = spawn_http_server("200 OK", publish_response_json().as_str()); + + let receipt = publish_event( + endpoint.as_str(), + &RadrootsdAuth::BearerToken("sdk-token".into()), + &publish_request(), + Duration::from_secs(2), + ) + .await + .expect("publish"); + + assert_eq!(receipt.job.event_id, signed_event_id()); + let recorded = handle.join().expect("server thread"); + assert_eq!(recorded.request_line, "POST /rpc HTTP/1.1"); + assert!( + recorded + .headers + .iter() + .any(|(name, value)| name == "authorization" && value == "Bearer sdk-token") + ); + let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body"); + assert_eq!(body["method"], METHOD_EVENT); + assert_eq!(body["id"], SDK_RADROOTSD_PUBLISH_REQUEST_ID); + let raw_event_json = body["params"]["raw_event_json"] + .as_str() + .expect("raw event json"); + let raw_event: serde_json::Value = serde_json::from_str(raw_event_json).expect("raw event"); + assert_eq!(raw_event["content"], "{\"name\":\"carrots\"}"); + assert_eq!(body["params"]["target_policy"]["kind"], "nostr"); + assert_eq!( + body["params"]["target_policy"]["relay_urls"][0], + "wss://relay.example.com" + ); +} + +#[tokio::test] +async fn publish_signed_event_posts_typed_radrootsd_request() { + let mut response_job = explicit_nostr_job( + vec!["wss://relay.example.com".to_owned()], + TransportPublishDeliveryPolicy::All, + ); + response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay.example.com") + .with_scope("farm.local") + .with_label("Farm relay"), + ]); + response_job.targets[0].target_scope = Some("farm.local".to_owned()); + response_job.targets[0].target_label = Some("Farm relay".to_owned()); + let response_json = publish_response_json_for_job(response_job); + let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new( + RadrootsdPublishConfig::new(endpoint) + .with_auth(RadrootsdAuth::BearerToken("sdk-token".into())), + ); + + let receipt = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay.example.com") + .with_scope("farm.local") + .with_label("Farm relay"), + ]), + delivery_policy: TransportPublishDeliveryPolicy::All, + idempotency_key: Some("idem-typed".to_owned()), + timeout_ms: Some(7_000), + }) + .await + .expect("typed publish"); + + assert!(receipt.job.delivery_satisfied); + assert_eq!( + receipt.job.targets[0].target_scope.as_deref(), + Some("farm.local") + ); + assert_eq!( + receipt.job.targets[0].target_label.as_deref(), + Some("Farm relay") + ); + let recorded = handle.join().expect("server thread"); + assert!( + recorded + .headers + .iter() + .any(|(name, value)| name == "authorization" && value == "Bearer sdk-token") + ); + let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body"); + assert_eq!(body["params"]["delivery_policy"]["mode"], "all"); + assert_eq!(body["params"]["target_policy"]["kind"], "explicit_targets"); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["endpoint_uri"], + "wss://relay.example.com" + ); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["target_scope"], + "farm.local" + ); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["target_label"], + "Farm relay" + ); + assert_eq!(body["params"]["idempotency_key"], "idem-typed"); + assert_eq!(body["params"]["timeout_ms"], 7_000); +} + +#[tokio::test] +async fn publish_signed_event_preserves_typed_reticulum_behavior() { + let response_json = reticulum_deferred_response_json(); + let (endpoint, handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let response = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, + ), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-reticulum".to_owned()), + timeout_ms: None, + }) + .await + .expect("typed Reticulum publish request"); + assert_eq!( + response.job.status, + TransportPublishJobStatus::DeliveryDeferred + ); + assert!(!response.job.delivery_satisfied); + + let recorded = handle.join().expect("server thread"); + let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body"); + assert_eq!(body["params"]["target_policy"]["kind"], "explicit_targets"); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["transport_kind"], + "reticulum" + ); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["endpoint_uri"], + RADROOTS_RETICULUM_ENDPOINT_URI + ); + assert_eq!( + body["params"]["target_policy"]["targets"][0]["reticulum_behavior"], + "defer_delivery_plans" + ); +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_daemon_event_identity() { + let mut response_job = job(TransportPublishOutcomeKind::Accepted); + response_job.event_id = "0".repeat(64); + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-mismatch".to_owned()), + timeout_ms: None, + }) + .await + .expect_err("mismatched response"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, "event_id"); +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_daemon_pubkey_and_kind() { + for (field, response_job) in [ + { + let mut response_job = job(TransportPublishOutcomeKind::Accepted); + response_job.pubkey = "0".repeat(64); + ("pubkey", response_job) + }, + { + let mut response_job = job(TransportPublishOutcomeKind::Accepted); + response_job.event_kind = 30_403; + ("event_kind", response_job) + }, + ] { + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some(format!("idem-mismatch-{field}")), + timeout_ms: None, + }) + .await + .expect_err("mismatched response"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, field); + } +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_daemon_delivery_policy() { + let mut response_job = job(TransportPublishOutcomeKind::Accepted); + response_job.delivery_policy = TransportPublishDeliveryPolicy::All; + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-delivery-policy-mismatch".to_owned()), + timeout_ms: None, + }) + .await + .expect_err("delivery policy mismatch"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, "delivery_policy"); +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_explicit_target_response() { + let (endpoint, _handle) = spawn_http_server("200 OK", publish_response_json().as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::reticulum( + TransportPublishReticulumBehavior::DeferDeliveryPlans, + ), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-target-mismatch".to_owned()), + timeout_ms: None, + }) + .await + .expect_err("target mismatch"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, "target_policy"); +} + +#[tokio::test] +async fn publish_signed_event_accepts_reordered_explicit_target_outcomes() { + let mut response_job = explicit_nostr_job( + vec![ + "wss://relay-a.example.com".to_owned(), + "wss://relay-b.example.com".to_owned(), + ], + TransportPublishDeliveryPolicy::Any, + ); + response_job.targets.reverse(); + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let response = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay-a.example.com"), + TransportPublishTarget::nostr("wss://relay-b.example.com"), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-explicit-reordered-outcomes".to_owned()), + timeout_ms: None, + }) + .await + .expect("reordered explicit target outcomes"); + + assert!(response.job.delivery_satisfied); + assert_eq!( + response.job.targets[0].endpoint_uri, + "wss://relay-b.example.com" + ); + assert_eq!( + response.job.targets[1].endpoint_uri, + "wss://relay-a.example.com" + ); +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_explicit_target_outcomes() { + let mut response_job = explicit_nostr_job( + vec!["wss://relay.example.com".to_owned()], + TransportPublishDeliveryPolicy::Any, + ); + response_job.targets[0].endpoint_uri = "wss://relay-other.example.com".to_owned(); + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay.example.com"), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-explicit-outcome-mismatch".to_owned()), + timeout_ms: None, + }) + .await + .expect_err("explicit target outcome mismatch"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, "explicit target policy"); +} + +#[tokio::test] +async fn publish_signed_event_rejects_mismatched_scoped_explicit_target_outcomes() { + let mut response_job = explicit_nostr_job( + vec!["wss://relay.example.com".to_owned()], + TransportPublishDeliveryPolicy::Any, + ); + response_job.target_policy = TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"), + ]); + response_job.targets[0].target_scope = Some("farm.remote".to_owned()); + let response_json = publish_response_json_for_job(response_job); + let (endpoint, _handle) = spawn_http_server("200 OK", response_json.as_str()); + let adapter = RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new(endpoint)); + + let error = adapter + .publish_signed_event(RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::explicit_targets(vec![ + TransportPublishTarget::nostr("wss://relay.example.com").with_scope("farm.local"), + ]), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-scoped-explicit-outcome-mismatch".to_owned()), + timeout_ms: None, + }) + .await + .expect_err("scoped explicit target outcome mismatch"); + + assert!(matches!(error, RadrootsdError::MalformedResponse(_))); + assert_message(error, "explicit target policy"); +} + +#[tokio::test] +async fn publish_event_http_errors_omit_body_and_token_material() { + let body = "{\"error\":\"token-secret content carrots\"}"; + let (endpoint, _handle) = spawn_http_server("503 Service Unavailable", body); + + let error = publish_event( + endpoint.as_str(), + &RadrootsdAuth::BearerToken("token-secret".into()), + &publish_request(), + Duration::from_secs(2), + ) + .await + .expect_err("http error"); + let message = error.to_string(); + + assert!(message.contains("503")); + assert!(message.contains("response body omitted")); + assert!(!message.contains("token-secret")); + assert!(!message.contains("carrots")); +} + +#[tokio::test] +async fn publish_event_empty_http_error_reports_empty_body() { + let (endpoint, _handle) = spawn_http_server("500 Internal Server Error", ""); + + let error = publish_event( + endpoint.as_str(), + &RadrootsdAuth::None, + &publish_request(), + Duration::from_secs(2), + ) + .await + .expect_err("http error"); + + assert!(error.to_string().contains("response body empty")); +} + +#[tokio::test] +async fn publish_signed_event_rejects_invalid_target_requests_before_http() { + let adapter = + RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new("http://127.0.0.1:9/rpc")); + let base = RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec!["wss://relay.example.com".to_owned()], + ), + delivery_policy: TransportPublishDeliveryPolicy::Any, + idempotency_key: Some("idem-1".to_owned()), + timeout_ms: Some(1_000), + }; + + let mut invalid_quorum = base.clone(); + invalid_quorum.delivery_policy = TransportPublishDeliveryPolicy::Quorum { quorum: 0 }; + let mut too_many_targets = base.clone(); + too_many_targets.target_policy = TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + (0..=SDK_RADROOTSD_PUBLISH_MAX_TARGETS) + .map(|index| format!("wss://relay-{index}.example.com")) + .collect(), + ); + let mut empty_endpoint_uri = base.clone(); + empty_endpoint_uri.target_policy = TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + vec![" ".to_owned()], + ); + let mut nostr_reticulum_behavior = base.clone(); + nostr_reticulum_behavior.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: "nostr".to_owned(), + endpoint_uri: "wss://relay.example.com".to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: Some(TransportPublishReticulumBehavior::RejectDeliveryAttempts), + }]); + let mut explicit_radrootsd_target = base.clone(); + explicit_radrootsd_target.target_policy = + TransportPublishTargetPolicy::explicit_targets(vec![TransportPublishTarget { + transport_kind: "radrootsd".to_owned(), + endpoint_uri: "radrootsd-execution:publish".to_owned(), + target_scope: None, + target_label: None, + reticulum_behavior: None, + }]); + let radrootsd_error = adapter + .publish_signed_event(explicit_radrootsd_target) + .await + .expect_err("explicit radrootsd target"); + assert!(matches!( + radrootsd_error, + RadrootsdError::InvalidRequest(message) + if message.contains("transport target 0 kind must be canonical lowercase") + )); + let mut empty_idempotency = base; + empty_idempotency.idempotency_key = Some(" ".to_owned()); + + for request in [ + invalid_quorum, + too_many_targets, + empty_endpoint_uri, + nostr_reticulum_behavior, + empty_idempotency, + ] { + assert!(matches!( + adapter.publish_signed_event(request).await, + Err(RadrootsdError::InvalidRequest(_)) + )); + } +} + +#[tokio::test] +async fn adapter_rejects_invalid_request_before_transport() { + let adapter = + RadrootsdPublishAdapter::new(RadrootsdPublishConfig::new("http://127.0.0.1:9/rpc")); + let request = RadrootsdPublishRequest { + signed_event: signed_event(), + target_policy: TransportPublishTargetPolicy::nostr( + NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault, + Vec::new(), + ), + delivery_policy: TransportPublishDeliveryPolicy::Quorum { quorum: 0 }, + idempotency_key: None, + timeout_ms: None, + }; + + let error = adapter + .publish_signed_event(request) + .await + .expect_err("invalid request"); + + assert!(matches!(error, RadrootsdError::InvalidRequest(_))); +} diff --git a/crates/sdk_ffi/Cargo.toml b/crates/sdk_ffi/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "radroots_sdk_ffi" +publish = false +version = "0.1.0-alpha" +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Private UniFFI wrapper over the shared Radroots SDK engine" +repository.workspace = true +homepage.workspace = true +readme = "README.md" + +[lib] +crate-type = ["lib", "staticlib", "cdylib"] + +[[bin]] +name = "radroots-sdk-bindgen" +path = "src/bin/bindgen.rs" + +[dependencies] +radroots_sdk = { workspace = true, features = ["memory"] } +thiserror = { workspace = true } +uniffi = { workspace = true, features = ["cli"] } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt"] } + +[lints] +workspace = true diff --git a/crates/sdk_ffi/README.md b/crates/sdk_ffi/README.md @@ -0,0 +1,10 @@ +# radroots_sdk_ffi + +Private UniFFI bindings over the host-neutral `radroots_sdk` engine. The V1 +surface exposes deterministic memory-client construction, versioned capability +DTOs, secret-safe errors, and explicit asynchronous close. It does not own +keychain prompts, background execution, application scheduling, or presentation +DTOs; those remain mobile-host responsibilities. + +This Rust build crate is never published. Generated Swift and Kotlin artifacts +are qualified independently from the public 19-crate Rust inventory. diff --git a/crates/sdk_ffi/src/bin/bindgen.rs b/crates/sdk_ffi/src/bin/bindgen.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main(); +} diff --git a/crates/sdk_ffi/src/lib.rs b/crates/sdk_ffi/src/lib.rs @@ -0,0 +1,173 @@ +//! Private mobile FFI over the shared SDK engine. + +#![forbid(unsafe_code)] +#![doc = include_str!("../README.md")] + +use std::sync::Arc; + +use radroots_sdk::{Client, ClientBuilder}; + +uniffi::setup_scaffolding!("radroots_sdk"); + +/// Generation-1 mobile DTOs. +pub mod v1 { + use super::*; + + /// Stable capability maturity independent of runtime availability. + #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] + pub enum CapabilityMaturity { + Stable, + Preview, + Experimental, + } + + /// Current side-effect-free capability availability. + #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] + pub enum CapabilityAvailability { + Available, + Degraded, + Unavailable, + Unsupported, + } + + /// Versioned, presentation-independent capability observation. + #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] + pub struct CapabilityStatus { + pub id: String, + pub compiled: bool, + pub configured: bool, + pub availability: CapabilityAvailability, + pub maturity: CapabilityMaturity, + } + + /// Secret-safe SDK failure at the language boundary. + #[derive(Debug, thiserror::Error, uniffi::Error)] + pub enum Error { + #[error("SDK {code}: {message}")] + Sdk { + code: String, + message: String, + retryable: bool, + }, + } + + impl From<radroots_sdk::Error> for Error { + fn from(error: radroots_sdk::Error) -> Self { + let descriptor = error.descriptor(); + Self::Sdk { + code: descriptor.code().as_str().to_owned(), + message: descriptor.message().to_owned(), + retryable: descriptor.retryable(), + } + } + } + + /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`]. + #[derive(uniffi::Object)] + pub struct MobileClient { + client: Client, + } + + #[uniffi::export] + impl MobileClient { + /// Creates a deterministic, local-only memory client without I/O. + #[uniffi::constructor] + pub fn memory() -> Result<Arc<Self>, Error> { + let client = ClientBuilder::memory_default().build()?; + Ok(Arc::new(Self { client })) + } + + /// Returns stable capability DTOs without probing host resources. + pub fn capabilities(&self) -> Vec<CapabilityStatus> { + self.client + .capabilities() + .iter() + .copied() + .map(CapabilityStatus::from) + .collect() + } + + /// Returns whether explicit SDK close completed across all clones. + pub fn is_closed(&self) -> bool { + self.client.is_closed() + } + + /// Explicitly closes SDK-owned resources without installing a runtime. + pub async fn close(&self) -> Result<(), Error> { + self.client.close().await.map_err(Error::from) + } + } + + impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus { + fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self { + Self { + id: status.id().as_str().to_owned(), + compiled: status.is_compiled(), + configured: status.is_configured(), + availability: match status.availability() { + radroots_sdk::capability::Availability::Available => { + CapabilityAvailability::Available + } + radroots_sdk::capability::Availability::Degraded => { + CapabilityAvailability::Degraded + } + radroots_sdk::capability::Availability::Unavailable => { + CapabilityAvailability::Unavailable + } + radroots_sdk::capability::Availability::Unsupported => { + CapabilityAvailability::Unsupported + } + }, + maturity: match status.maturity() { + radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable, + radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview, + radroots_sdk::capability::Maturity::Experimental => { + CapabilityMaturity::Experimental + } + }, + } + } + } +} + +#[cfg(test)] +mod tests { + use super::v1::{CapabilityAvailability, Error, MobileClient}; + + fn assert_send_sync<T: Send + Sync>() {} + + #[tokio::test(flavor = "current_thread")] + async fn memory_client_delegates_capabilities_and_lifecycle() { + assert_send_sync::<MobileClient>(); + let client = MobileClient::memory().expect("memory client"); + let capabilities = client.capabilities(); + let storage = capabilities + .iter() + .find(|status| status.id == "storage.canonical") + .expect("canonical storage"); + assert!(storage.compiled); + assert!(storage.configured); + assert_eq!(storage.availability, CapabilityAvailability::Available); + assert!(!client.is_closed()); + + client.close().await.expect("close"); + assert!(client.is_closed()); + client.close().await.expect("idempotent close"); + } + + #[test] + fn sdk_error_mapping_is_versioned_and_secret_safe() { + let native = radroots_sdk::ClientBuilder::new() + .build() + .expect_err("missing storage"); + let error = Error::from(native); + let Error::Sdk { + code, + message, + retryable, + } = error; + assert_eq!(code, "missing_storage"); + assert_eq!(message, "SDK storage capability is not configured"); + assert!(!retryable); + } +} diff --git a/crates/sdk_sql_wasm_runtime/Cargo.toml b/crates/sdk_sql_wasm_runtime/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "radroots_sdk_sql_wasm_runtime" +publish = false +version = "0.1.0-alpha" +edition.workspace = true +authors = ["Tyson Lupul <tyson@radroots.org>"] +rust-version.workspace = true +license.workspace = true +description = "SQL WASM runtime for Radroots SDK data surfaces" +repository.workspace = true +homepage.workspace = true + +[lib] +crate-type = ["rlib"] + +[dependencies] +radroots_sql_core = { workspace = true, features = ["web"] } +serde = { workspace = true } +serde_json = { workspace = true } +serde-wasm-bindgen = { workspace = true } +wasm-bindgen = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/sdk_sql_wasm_runtime/src/lib.rs b/crates/sdk_sql_wasm_runtime/src/lib.rs @@ -0,0 +1,534 @@ +#![forbid(unsafe_code)] + +use radroots_sql_core::{ExecOutcome, SqlError, SqlExecutor, utils}; +use serde::de::DeserializeOwned; +use serde_json::Value; +use std::cell::Cell; +use std::sync::atomic::{AtomicBool, Ordering}; +use wasm_bindgen::JsValue; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = __radroots_sql_wasm_exec)] + fn js_exec(sql: &str, params_json: &str) -> JsValue; + + #[wasm_bindgen(js_name = __radroots_sql_wasm_query)] + fn js_query(sql: &str, params_json: &str) -> JsValue; + + #[wasm_bindgen(js_name = __radroots_sql_wasm_export_bytes)] + fn js_export_bytes() -> JsValue; +} + +#[cfg(not(target_arch = "wasm32"))] +use std::sync::{Mutex, OnceLock}; + +#[cfg(all(not(target_arch = "wasm32"), test))] +use std::collections::VecDeque; + +#[cfg(not(target_arch = "wasm32"))] +type RecordedCall = (String, String); + +#[cfg(all(not(target_arch = "wasm32"), test))] +type NativeHostResult = Result<Value, SqlError>; + +#[cfg(not(target_arch = "wasm32"))] +fn exec_calls() -> &'static Mutex<Vec<RecordedCall>> { + static EXEC_CALLS: OnceLock<Mutex<Vec<RecordedCall>>> = OnceLock::new(); + EXEC_CALLS.get_or_init(|| Mutex::new(Vec::new())) +} + +#[cfg(not(target_arch = "wasm32"))] +fn query_calls() -> &'static Mutex<Vec<RecordedCall>> { + static QUERY_CALLS: OnceLock<Mutex<Vec<RecordedCall>>> = OnceLock::new(); + QUERY_CALLS.get_or_init(|| Mutex::new(Vec::new())) +} + +#[cfg(not(target_arch = "wasm32"))] +fn export_calls() -> &'static Mutex<u64> { + static EXPORT_CALLS: OnceLock<Mutex<u64>> = OnceLock::new(); + EXPORT_CALLS.get_or_init(|| Mutex::new(0)) +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn exec_results() -> &'static Mutex<VecDeque<NativeHostResult>> { + static EXEC_RESULTS: OnceLock<Mutex<VecDeque<NativeHostResult>>> = OnceLock::new(); + EXEC_RESULTS.get_or_init(|| Mutex::new(VecDeque::new())) +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn query_results() -> &'static Mutex<VecDeque<NativeHostResult>> { + static QUERY_RESULTS: OnceLock<Mutex<VecDeque<NativeHostResult>>> = OnceLock::new(); + QUERY_RESULTS.get_or_init(|| Mutex::new(VecDeque::new())) +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn push_exec_result(result: NativeHostResult) { + let mut results = exec_results().lock().expect("exec results lock"); + results.push_back(result); +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn push_query_result(result: NativeHostResult) { + let mut results = query_results().lock().expect("query results lock"); + results.push_back(result); +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn take_exec_result() -> Option<NativeHostResult> { + exec_results() + .lock() + .expect("exec results lock") + .pop_front() +} + +#[cfg(all(not(target_arch = "wasm32"), test))] +fn take_query_result() -> Option<NativeHostResult> { + query_results() + .lock() + .expect("query results lock") + .pop_front() +} + +#[cfg(not(target_arch = "wasm32"))] +fn js_exec(sql: &str, params_json: &str) -> JsValue { + let mut calls = exec_calls().lock().expect("exec calls lock"); + calls.push((sql.to_string(), params_json.to_string())); + JsValue::NULL +} + +#[cfg(not(target_arch = "wasm32"))] +fn js_query(sql: &str, params_json: &str) -> JsValue { + let mut calls = query_calls().lock().expect("query calls lock"); + calls.push((sql.to_string(), params_json.to_string())); + JsValue::NULL +} + +#[cfg(not(target_arch = "wasm32"))] +fn js_export_bytes() -> JsValue { + let mut calls = export_calls().lock().expect("export calls lock"); + *calls += 1; + JsValue::NULL +} + +const SAVEPOINT: &str = "radroots_schema_tx"; +const EXPORT_LOCK_ERR: &str = "replica db export in progress"; + +static EXPORT_LOCK_ACTIVE: AtomicBool = AtomicBool::new(false); + +thread_local! { + static EXPORT_LOCK_BYPASS: Cell<bool> = const { Cell::new(false) }; +} + +pub struct WasmSqlExecutor; + +impl WasmSqlExecutor { + pub fn new() -> Self { + Self + } +} + +impl Default for WasmSqlExecutor { + fn default() -> Self { + Self::new() + } +} + +impl SqlExecutor for WasmSqlExecutor { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { + if export_lock_blocked() { + return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string())); + } + let v = host_exec_json(sql, params_json)?; + Ok(exec_outcome_from_json(&v)) + } + + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { + let v = host_query_json(sql, params_json)?; + Ok(query_raw_from_json(&v)) + } + + fn begin(&self) -> Result<(), SqlError> { + if export_lock_blocked() { + return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string())); + } + begin_tx(); + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + if export_lock_blocked() { + return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string())); + } + commit_tx(); + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + if export_lock_blocked() { + return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string())); + } + rollback_tx(); + Ok(()) + } +} + +pub fn parse_json<T: DeserializeOwned>(s: &str) -> Result<T, SqlError> { + utils::parse_json(s) +} + +fn host_exec_json(sql: &str, params_json: &str) -> Result<Value, SqlError> { + let js = exec(sql, params_json); + #[cfg(all(not(target_arch = "wasm32"), test))] + if let Some(result) = take_exec_result() { + return result; + } + sql_value_from_js(js) +} + +fn host_query_json(sql: &str, params_json: &str) -> Result<Value, SqlError> { + let js = query(sql, params_json); + #[cfg(all(not(target_arch = "wasm32"), test))] + if let Some(result) = take_query_result() { + return result; + } + sql_value_from_js(js) +} + +#[cfg(target_arch = "wasm32")] +fn sql_value_from_js(js: JsValue) -> Result<Value, SqlError> { + serde_wasm_bindgen::from_value(js).map_err(|e| SqlError::SerializationError(e.to_string())) +} + +#[cfg(not(target_arch = "wasm32"))] +fn sql_value_from_js(_js: JsValue) -> Result<Value, SqlError> { + Err(SqlError::UnsupportedPlatform) +} + +fn exec_outcome_from_json(v: &Value) -> ExecOutcome { + let changes = v.get("changes").and_then(|x| x.as_i64()).unwrap_or(0); + let last_insert_id = v + .get("last_insert_id") + .or_else(|| v.get("lastInsertRowid")) + .and_then(|x| x.as_i64()) + .unwrap_or(0); + ExecOutcome { + changes, + last_insert_id, + } +} + +fn query_raw_from_json(v: &Value) -> String { + v.to_string() +} + +pub fn err_js(err: SqlError) -> JsValue { + err_js_value(err) +} + +#[cfg(target_arch = "wasm32")] +fn err_js_value(err: SqlError) -> JsValue { + match err_js_with_encoder(err, |err| { + let value = err.to_json(); + serde_wasm_bindgen::to_value(&value).map_err(|_| ()) + }) { + Ok(value) => value, + Err(err) => JsValue::from_str(&err.to_string()), + } +} + +#[cfg(not(target_arch = "wasm32"))] +fn err_js_value(err: SqlError) -> JsValue { + let _ = err.to_json(); + JsValue::NULL +} + +#[cfg(target_arch = "wasm32")] +fn err_js_with_encoder( + err: SqlError, + encode: impl FnOnce(&SqlError) -> Result<JsValue, ()>, +) -> Result<JsValue, SqlError> { + encode(&err).map_err(|()| err) +} + +pub fn exec(sql: &str, params_json: &str) -> JsValue { + js_exec(sql, params_json) +} + +pub fn query(sql: &str, params_json: &str) -> JsValue { + js_query(sql, params_json) +} + +pub fn export_bytes() -> JsValue { + js_export_bytes() +} + +pub fn begin_tx() { + let _ = js_exec(&format!("savepoint {}", SAVEPOINT), "[]"); +} + +pub fn commit_tx() { + let _ = js_exec(&format!("release savepoint {}", SAVEPOINT), "[]"); +} + +pub fn rollback_tx() { + let _ = js_exec(&format!("rollback to savepoint {}", SAVEPOINT), "[]"); + let _ = js_exec(&format!("release savepoint {}", SAVEPOINT), "[]"); +} + +pub fn export_lock_begin() -> Result<(), SqlError> { + let was_active = EXPORT_LOCK_ACTIVE.swap(true, Ordering::SeqCst); + if was_active { + return Err(SqlError::InvalidArgument(EXPORT_LOCK_ERR.to_string())); + } + Ok(()) +} + +pub fn export_lock_end() { + EXPORT_LOCK_ACTIVE.store(false, Ordering::SeqCst); +} + +pub fn export_lock_active() -> bool { + EXPORT_LOCK_ACTIVE.load(Ordering::SeqCst) +} + +pub fn with_export_lock_bypass<T>(f: impl FnOnce() -> T) -> T { + EXPORT_LOCK_BYPASS.with(|flag| { + let prev = flag.replace(true); + let out = f(); + flag.set(prev); + out + }) +} + +fn export_lock_blocked() -> bool { + if !EXPORT_LOCK_ACTIVE.load(Ordering::SeqCst) { + return false; + } + EXPORT_LOCK_BYPASS.with(|flag| !flag.get()) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeMap, + sync::{Mutex, OnceLock}, + }; + + use radroots_sql_core::{SqlError, SqlExecutor}; + use serde_json::json; + + use super::{ + WasmSqlExecutor, begin_tx, commit_tx, err_js, exec, exec_calls, exec_outcome_from_json, + exec_results, export_bytes, export_calls, export_lock_active, export_lock_begin, + export_lock_end, parse_json, push_exec_result, push_query_result, query, query_calls, + query_raw_from_json, query_results, rollback_tx, with_export_lock_bypass, + }; + + fn native_test_lock() -> &'static Mutex<()> { + static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + TEST_LOCK.get_or_init(|| Mutex::new(())) + } + + fn reset_native_state() { + exec_calls().lock().expect("exec calls lock").clear(); + query_calls().lock().expect("query calls lock").clear(); + *export_calls().lock().expect("export calls lock") = 0; + exec_results().lock().expect("exec results lock").clear(); + query_results().lock().expect("query results lock").clear(); + export_lock_end(); + } + + #[test] + fn parse_json_reports_valid_and_invalid_payloads() { + let parsed: BTreeMap<String, u64> = parse_json(r#"{"count":2}"#).expect("parse json"); + assert_eq!(parsed.get("count"), Some(&2)); + assert_eq!( + parse_json::<BTreeMap<String, u64>>("{").unwrap_err().code(), + "ERR_SERIALIZATION" + ); + } + + #[test] + fn err_js_accepts_sql_errors() { + let _ = err_js(SqlError::Internal); + let _ = err_js(SqlError::UnsupportedPlatform); + } + + #[test] + fn exec_query_export_delegate_to_js_hooks() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + let _ = exec("select 1", "[]"); + let _ = query("select 2", "[1]"); + let _ = export_bytes(); + + let exec_len = exec_calls().lock().map(|calls| calls.len()).unwrap_or(0); + let query_len = query_calls().lock().map(|calls| calls.len()).unwrap_or(0); + let export_len = export_calls().lock().map(|calls| *calls).unwrap_or(0); + assert!(exec_len >= 1); + assert!(query_len >= 1); + assert!(export_len >= 1); + } + + #[test] + fn tx_helpers_emit_expected_savepoint_statements() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + begin_tx(); + commit_tx(); + rollback_tx(); + + let calls = exec_calls() + .lock() + .map(|calls| calls.clone()) + .unwrap_or_default(); + assert!( + calls + .iter() + .any(|(sql, _)| sql == "savepoint radroots_schema_tx") + ); + assert!( + calls + .iter() + .any(|(sql, _)| sql == "release savepoint radroots_schema_tx") + ); + assert!( + calls + .iter() + .any(|(sql, _)| sql == "rollback to savepoint radroots_schema_tx") + ); + } + + #[test] + fn export_lock_tracks_state() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + assert!(!export_lock_active()); + export_lock_begin().expect("begin export lock"); + assert!(export_lock_active()); + assert!(with_export_lock_bypass(|| true)); + export_lock_end(); + assert!(!export_lock_active()); + } + + #[test] + fn executor_decodes_exec_outcomes() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + let executor = WasmSqlExecutor; + push_exec_result(Ok(json!({"changes": 2, "lastInsertRowid": 99}))); + let outcome = executor + .exec("insert into listing values (?)", r#"["bin-1"]"#) + .expect("exec outcome"); + assert_eq!(outcome.changes, 2); + assert_eq!(outcome.last_insert_id, 99); + + push_exec_result(Ok(json!({"changes": 3, "last_insert_id": 101}))); + let outcome = executor + .exec("update listing set qty = ?", "[1]") + .expect("exec outcome"); + assert_eq!(outcome.changes, 3); + assert_eq!(outcome.last_insert_id, 101); + + let default_outcome = exec_outcome_from_json(&json!({})); + assert_eq!(default_outcome.changes, 0); + assert_eq!(default_outcome.last_insert_id, 0); + + let calls = exec_calls().lock().expect("exec calls lock").clone(); + assert!(calls.iter().any(|(sql, params)| { + sql == "insert into listing values (?)" && params == r#"["bin-1"]"# + })); + } + + #[test] + fn executor_decodes_query_results_and_host_errors() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + let executor = WasmSqlExecutor::new(); + let rows = json!([{"id": "listing-1"}]); + push_query_result(Ok(rows.clone())); + assert_eq!( + executor + .query_raw("select id from listing", "[]") + .expect("query rows"), + query_raw_from_json(&rows) + ); + + assert_eq!( + executor + .query_raw("select id from listing", "[]") + .unwrap_err() + .code(), + "ERR_UNSUPPORTED_PLATFORM" + ); + + push_exec_result(Err(SqlError::SerializationError( + "host response was not an object".to_string(), + ))); + assert_eq!( + executor + .exec("insert into listing values (?)", "[]") + .unwrap_err() + .code(), + "ERR_SERIALIZATION" + ); + assert_eq!( + executor + .exec("insert into listing values (?)", "[]") + .unwrap_err() + .code(), + "ERR_UNSUPPORTED_PLATFORM" + ); + } + + #[test] + fn export_lock_rejects_nested_lock_and_write_trait_calls() { + let _guard = native_test_lock().lock().expect("native test lock"); + reset_native_state(); + + let executor = WasmSqlExecutor::new(); + export_lock_begin().expect("begin export lock"); + assert_eq!( + export_lock_begin().unwrap_err().to_string(), + "invalid argument: replica db export in progress" + ); + assert_eq!( + executor + .exec("insert into listing values (?)", "[]") + .unwrap_err() + .code(), + "ERR_INVALID_ARGUMENT" + ); + assert_eq!(executor.begin().unwrap_err().code(), "ERR_INVALID_ARGUMENT"); + assert_eq!( + executor.commit().unwrap_err().code(), + "ERR_INVALID_ARGUMENT" + ); + assert_eq!( + executor.rollback().unwrap_err().code(), + "ERR_INVALID_ARGUMENT" + ); + + with_export_lock_bypass(|| { + push_exec_result(Ok(json!({"changes": 1, "last_insert_id": 7}))); + let outcome = executor + .exec("insert into listing values (?)", "[]") + .expect("bypassed exec"); + assert_eq!(outcome.changes, 1); + executor.begin().expect("bypassed begin"); + executor.commit().expect("bypassed commit"); + executor.rollback().expect("bypassed rollback"); + }); + assert!(export_lock_active()); + export_lock_end(); + } +} diff --git a/crates/trade_bindings/Cargo.toml b/crates/trade_bindings/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "radroots_trade_bindings" +description = "Generated bindings for Radroots trade contracts" +version = "0.1.0-alpha" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs @@ -0,0 +1,297 @@ +//! Private inventory for canonical native trade bindings. +//! +//! The SDK generator authenticates the reviewed mapping against final +//! `radroots_trade`, `radroots_event`, and `radroots_core` owners. This crate +//! intentionally does not activate code generation in a public runtime crate. + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TradeTypeDisposition { + SourceTradeRoot, + SourceTradeSupport, + EventsBindingImport, + SdkLocalPackageShape, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TradeTypeInventoryEntry { + pub export_name: &'static str, + pub disposition: TradeTypeDisposition, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TradeLargeIntegerPolicy { + JsonNumberSafeCount, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TradeLargeIntegerPolicyEntry { + pub type_name: &'static str, + pub field_name: &'static str, + pub policy: TradeLargeIntegerPolicy, +} + +pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[ + event_import("RadrootsFarmRef"), + event_import("RadrootsOperationalListing"), + event_import("RadrootsOperationalListingAvailability"), + event_import("RadrootsOperationalListingBin"), + event_import("RadrootsOperationalListingDeliveryMethod"), + event_import("RadrootsOperationalListingProduct"), + event_import("RadrootsOperationalListingPublicLocation"), + event_import("RadrootsOperationalListingStatus"), + local_shape("RadrootsTradeFacetCount"), + source_root("RadrootsTradeAgreementStateV1"), + source_root("RadrootsTradeAttestationStateV1"), + source_root("RadrootsTradeConflictStateV1"), + source_root("RadrootsTradeEvidenceStateV1"), + source_root("RadrootsTradeFulfillmentStateV1"), + source_root("RadrootsOperationalListingTradeProjection"), + local_shape("RadrootsTradeListingBackofficeOverlay"), + local_shape("RadrootsTradeListingBackofficeQuery"), + local_shape("RadrootsTradeListingBackofficeView"), + local_shape("RadrootsTradeListingBinProjection"), + local_shape("RadrootsTradeListingFacets"), + local_shape("RadrootsTradeListingMarketStatus"), + local_shape("RadrootsTradeListingProjection"), + local_shape("RadrootsTradeListingQuery"), + local_shape("RadrootsTradeListingSort"), + local_shape("RadrootsTradeListingSortField"), + source_root("RadrootsOperationalListingSubtotal"), + source_root("RadrootsOperationalListingTotal"), + local_shape("RadrootsTradeMarketplaceListingSummary"), + local_shape("RadrootsTradeModerationFlag"), + local_shape("RadrootsTradeModerationSeverity"), + local_shape("RadrootsTradeModerationStatus"), + source_root("RadrootsTradeNegotiationStateV1"), + source_root("RadrootsTradePaymentStateV1"), + source_root("RadrootsTradePrivateTermsStateV1"), + source_root("RadrootsTradeProjectionV1"), + local_shape("RadrootsTradeReviewPriority"), + local_shape("RadrootsTradeReviewQueueEntry"), + local_shape("RadrootsTradeReviewStatus"), + local_shape("RadrootsTradeSortDirection"), +]; + +pub const TRADE_LARGE_INTEGER_POLICIES: &[TradeLargeIntegerPolicyEntry] = &[ + json_number_safe_count("RadrootsTradeFacetCount", "count"), + json_number_safe_count( + "RadrootsTradeListingBackofficeView", + "open_moderation_flag_count", + ), + json_number_safe_count("RadrootsTradeListingProjection", "trade_count"), + json_number_safe_count("RadrootsTradeListingProjection", "open_trade_count"), + json_number_safe_count("RadrootsTradeListingProjection", "terminal_trade_count"), + json_number_safe_count("RadrootsTradeMarketplaceListingSummary", "trade_count"), + json_number_safe_count("RadrootsTradeMarketplaceListingSummary", "open_trade_count"), + json_number_safe_count( + "RadrootsTradeMarketplaceListingSummary", + "terminal_trade_count", + ), +]; + +const fn source_root(export_name: &'static str) -> TradeTypeInventoryEntry { + TradeTypeInventoryEntry { + export_name, + disposition: TradeTypeDisposition::SourceTradeRoot, + } +} + +const fn event_import(export_name: &'static str) -> TradeTypeInventoryEntry { + TradeTypeInventoryEntry { + export_name, + disposition: TradeTypeDisposition::EventsBindingImport, + } +} + +const fn local_shape(export_name: &'static str) -> TradeTypeInventoryEntry { + TradeTypeInventoryEntry { + export_name, + disposition: TradeTypeDisposition::SdkLocalPackageShape, + } +} + +const fn json_number_safe_count( + type_name: &'static str, + field_name: &'static str, +) -> TradeLargeIntegerPolicyEntry { + TradeLargeIntegerPolicyEntry { + type_name, + field_name, + policy: TradeLargeIntegerPolicy::JsonNumberSafeCount, + } +} + +#[cfg(test)] +mod tests { + use super::{TRADE_LARGE_INTEGER_POLICIES, TRADE_TYPE_INVENTORY, TradeTypeDisposition}; + + #[test] + fn trade_type_inventory_is_deterministic() { + let expected = TRADE_TYPE_INVENTORY + .iter() + .map(|entry| entry.export_name) + .collect::<Vec<_>>(); + + assert_eq!( + expected, + [ + "RadrootsFarmRef", + "RadrootsOperationalListing", + "RadrootsOperationalListingAvailability", + "RadrootsOperationalListingBin", + "RadrootsOperationalListingDeliveryMethod", + "RadrootsOperationalListingProduct", + "RadrootsOperationalListingPublicLocation", + "RadrootsOperationalListingStatus", + "RadrootsTradeFacetCount", + "RadrootsTradeAgreementStateV1", + "RadrootsTradeAttestationStateV1", + "RadrootsTradeConflictStateV1", + "RadrootsTradeEvidenceStateV1", + "RadrootsTradeFulfillmentStateV1", + "RadrootsOperationalListingTradeProjection", + "RadrootsTradeListingBackofficeOverlay", + "RadrootsTradeListingBackofficeQuery", + "RadrootsTradeListingBackofficeView", + "RadrootsTradeListingBinProjection", + "RadrootsTradeListingFacets", + "RadrootsTradeListingMarketStatus", + "RadrootsTradeListingProjection", + "RadrootsTradeListingQuery", + "RadrootsTradeListingSort", + "RadrootsTradeListingSortField", + "RadrootsOperationalListingSubtotal", + "RadrootsOperationalListingTotal", + "RadrootsTradeMarketplaceListingSummary", + "RadrootsTradeModerationFlag", + "RadrootsTradeModerationSeverity", + "RadrootsTradeModerationStatus", + "RadrootsTradeNegotiationStateV1", + "RadrootsTradePaymentStateV1", + "RadrootsTradePrivateTermsStateV1", + "RadrootsTradeProjectionV1", + "RadrootsTradeReviewPriority", + "RadrootsTradeReviewQueueEntry", + "RadrootsTradeReviewStatus", + "RadrootsTradeSortDirection" + ] + ); + } + + #[test] + fn source_owned_trade_support_types_are_marked_for_event_import() { + for export_name in [ + "RadrootsFarmRef", + "RadrootsOperationalListing", + "RadrootsOperationalListingAvailability", + "RadrootsOperationalListingBin", + "RadrootsOperationalListingDeliveryMethod", + "RadrootsOperationalListingProduct", + "RadrootsOperationalListingPublicLocation", + "RadrootsOperationalListingStatus", + ] { + assert_eq!( + disposition(export_name), + TradeTypeDisposition::EventsBindingImport + ); + } + } + + #[test] + fn trade_source_roots_are_marked_for_source_registry() { + let source_roots = TRADE_TYPE_INVENTORY + .iter() + .filter(|entry| entry.disposition == TradeTypeDisposition::SourceTradeRoot) + .map(|entry| entry.export_name) + .collect::<Vec<_>>(); + + assert_eq!( + source_roots, + [ + "RadrootsTradeAgreementStateV1", + "RadrootsTradeAttestationStateV1", + "RadrootsTradeConflictStateV1", + "RadrootsTradeEvidenceStateV1", + "RadrootsTradeFulfillmentStateV1", + "RadrootsOperationalListingTradeProjection", + "RadrootsOperationalListingSubtotal", + "RadrootsOperationalListingTotal", + "RadrootsTradeNegotiationStateV1", + "RadrootsTradePaymentStateV1", + "RadrootsTradePrivateTermsStateV1", + "RadrootsTradeProjectionV1" + ] + ); + } + + #[test] + fn trade_source_support_types_are_marked_for_source_registry() { + assert!( + TRADE_TYPE_INVENTORY + .iter() + .all(|entry| entry.disposition != TradeTypeDisposition::SourceTradeSupport) + ); + } + + #[test] + fn trade_large_integer_policy_covers_current_count_fields() { + let actual = TRADE_LARGE_INTEGER_POLICIES + .iter() + .map(|entry| (entry.type_name, entry.field_name, entry.policy)) + .collect::<Vec<_>>(); + + assert_eq!( + actual, + [ + ( + "RadrootsTradeFacetCount", + "count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeListingBackofficeView", + "open_moderation_flag_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeListingProjection", + "trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeListingProjection", + "open_trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeListingProjection", + "terminal_trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeMarketplaceListingSummary", + "trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeMarketplaceListingSummary", + "open_trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ( + "RadrootsTradeMarketplaceListingSummary", + "terminal_trade_count", + super::TradeLargeIntegerPolicy::JsonNumberSafeCount + ), + ] + ); + } + + fn disposition(export_name: &str) -> TradeTypeDisposition { + TRADE_TYPE_INVENTORY + .iter() + .find(|entry| entry.export_name == export_name) + .map(|entry| entry.disposition) + .expect("inventory entry") + } +} diff --git a/tools/sdk_xtask_import/Cargo.toml.imported b/tools/sdk_xtask_import/Cargo.toml.imported @@ -0,0 +1,33 @@ +[package] +name = "radroots_sdk_xtask" +description = "Workspace automation for Radroots SDK crates" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[[bin]] +name = "radroots_sdk_xtask" +path = "src/main.rs" + +[dependencies] +dto_bindgen_backend_ts = { workspace = true } +dto_bindgen_core = { workspace = true } +radroots_core_bindings = { path = "../../crates/core_bindings", version = "=0.1.0-alpha" } +radroots_event_bindings = { path = "../../crates/event_bindings", version = "=0.1.0-alpha" } +radroots_identity = { workspace = true } +radroots_identity_bindings = { path = "../../crates/identity_bindings", version = "=0.1.0-alpha" } +radroots_protocol = { workspace = true } +radroots_replica_schema_bindings = { path = "../../crates/replica_schema_bindings", version = "=0.1.0-alpha" } +flate2 = "1" +serde_json = "1" +serde = { workspace = true, features = ["derive"] } +semver = "1" +sha2 = { workspace = true } +syn = { version = "2", features = ["full", "visit"] } +tar = "0.4" +tempfile = { workspace = true } +toml = "0.8" diff --git a/tools/sdk_xtask_import/src/api_qualification.rs b/tools/sdk_xtask_import/src/api_qualification.rs @@ -0,0 +1,139 @@ +use std::{collections::BTreeSet, fs, path::Path, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Contract { + schema_version: u16, + baseline_revision: String, + package_version: String, + tool: String, + minimum_tool_version: String, + policy: String, + feature_policy: String, + packages: Vec<String>, +} + +pub fn run(root: &Path) -> Result<(), String> { + let contract = load(root)?; + validate(&contract)?; + verify_tool(&contract)?; + verify_revision(root, &contract.baseline_revision)?; + for package in &contract.packages { + let args = invocation(package); + eprintln!("cargo {}", args.join(" ")); + let output = Command::new("cargo") + .args(&args) + .current_dir(root) + .output() + .map_err(|error| format!("failed to start cargo-public-api: {error}"))?; + if !output.status.success() { + return Err(format!( + "public API extraction failed for {package}: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + let actual = String::from_utf8(output.stdout) + .map_err(|error| format!("public API output for {package} was not UTF-8: {error}"))?; + let snapshot_path = root.join(format!( + "docs/api/{package}-{}.txt", + contract.package_version + )); + let expected = fs::read_to_string(&snapshot_path) + .map_err(|error| format!("read {}: {error}", snapshot_path.display()))?; + if actual != expected { + return Err(format!( + "reviewed public API snapshot drifted for {package}: {}", + snapshot_path.display() + )); + } + } + Ok(()) +} + +fn load(root: &Path) -> Result<Contract, String> { + let path = root.join("contracts/releases/api_semver.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn validate(contract: &Contract) -> Result<(), String> { + let unique = contract.packages.iter().collect::<BTreeSet<_>>(); + if contract.schema_version != 2 + || contract.package_version != "0.1.0-alpha" + || contract.tool != "cargo-public-api" + || contract.policy != "reviewed-breaking-snapshot" + || contract.feature_policy != "all" + || contract.baseline_revision.len() < 7 + || unique.len() != 2 + || unique.len() != contract.packages.len() + { + return Err("invalid SDK public API qualification contract".to_owned()); + } + Ok(()) +} + +fn verify_tool(contract: &Contract) -> Result<(), String> { + let output = Command::new("cargo") + .args(["public-api", "--version"]) + .output() + .map_err(|error| format!("failed to start cargo-public-api: {error}"))?; + if !output.status.success() { + return Err("cargo-public-api is required".to_owned()); + } + let stdout = String::from_utf8_lossy(&output.stdout); + let installed = stdout + .split_whitespace() + .find_map(|value| semver::Version::parse(value).ok()) + .ok_or_else(|| format!("could not parse cargo-public-api version: {stdout}"))?; + let minimum = semver::Version::parse(&contract.minimum_tool_version) + .map_err(|error| format!("invalid minimum tool version: {error}"))?; + if installed < minimum { + return Err(format!( + "cargo-public-api {} or newer is required, found {installed}", + contract.minimum_tool_version + )); + } + Ok(()) +} + +fn verify_revision(root: &Path, revision: &str) -> Result<(), String> { + let status = Command::new("git") + .args(["cat-file", "-e", &format!("{revision}^{{commit}}")]) + .current_dir(root) + .status() + .map_err(|error| format!("failed to inspect API baseline revision: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!("API baseline revision {revision} is unavailable")) + } +} + +fn invocation(package: &str) -> Vec<String> { + vec![ + "public-api".to_owned(), + "-p".to_owned(), + package.to_owned(), + "--all-features".to_owned(), + "-sss".to_owned(), + "--color".to_owned(), + "never".to_owned(), + ] +} + +#[cfg(test)] +mod tests { + use super::{invocation, load, validate}; + + #[test] + fn current_contract_covers_both_front_doors() { + let root = crate::fs::workspace_root().expect("workspace root"); + let contract = load(&root).expect("contract"); + validate(&contract).expect("valid contract"); + let invocation = invocation("radroots"); + assert!(invocation.contains(&"--all-features".to_owned())); + assert!(invocation.contains(&"-sss".to_owned())); + } +} diff --git a/tools/sdk_xtask_import/src/architecture.rs b/tools/sdk_xtask_import/src/architecture.rs @@ -0,0 +1,1511 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Component, Path}, +}; + +use serde::Deserialize; + +mod api_leakage; +mod dependency_boundary; + +const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml"; +const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml"; +const ARCHITECTURE_ID: &str = "radroots.crates.release.v1"; +const PUBLIC_HOMEPAGE: &str = "https://radroots.org"; +const PUBLIC_README: &str = "README.md"; +const PUBLIC_AUTHORS: &[&str] = &["Tyson Lupul <tyson@radroots.org>"]; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DeviationLedger { + schema_version: u16, + architecture_id: String, + deviation: Vec<DeviationRecord>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DeviationRecord { + id: String, + date: String, + status: String, + approval: String, + affected_steps: Vec<String>, + spec_anchors: Vec<String>, + source_evidence: Vec<String>, + replacement_action: String, + verification: Vec<String>, + unresolved_risk: String, + normative_architecture_change: bool, + adr_required: bool, + #[serde(default)] + closure_evidence: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct ArchitectureIdentity { + spec_id: String, + edition: String, + resolver: String, + rust_version: String, + license: String, + canonical_repositories: Vec<String>, + repositories: BTreeMap<String, ArchitectureRepository>, + package: Vec<ArchitecturePackage>, +} + +#[derive(Debug, Deserialize)] +struct ArchitectureRepository { + url: String, + version: String, + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct ArchitecturePackage { + name: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceManifest { + workspace: WorkspaceMembers, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceMembershipManifest { + workspace: WorkspaceMembership, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceMembership { + members: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceMembers { + members: Vec<String>, + resolver: String, + package: WorkspacePackage, + metadata: WorkspaceMetadata, + lints: WorkspaceLints, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceLints { + rust: WorkspaceRustLints, + rustdoc: WorkspaceRustdocLints, + clippy: WorkspaceClippyLints, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceRustLints { + unsafe_code: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceRustdocLints { + broken_intra_doc_links: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceClippyLints { + dbg_macro: String, + todo: String, + unimplemented: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceMetadata { + radroots: RadrootsWorkspaceMetadata, +} + +#[derive(Debug, Deserialize)] +struct RadrootsWorkspaceMetadata { + #[serde(rename = "public-package")] + public_package: PublicPackageMetadata, +} + +#[derive(Debug, Deserialize)] +struct PublicPackageMetadata { + version: String, + authors: Vec<String>, + readme: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspacePackage { + version: String, + edition: String, + #[serde(rename = "rust-version")] + rust_version: String, + license: String, + repository: String, + homepage: String, + readme: String, + authors: Vec<String>, +} + +pub fn validate(workspace_root: &Path) -> Result<(), String> { + validate_workspace_members(workspace_root)?; + let architecture_path = workspace_root.join(ARCHITECTURE_RELATIVE); + let architecture_raw = fs::read_to_string(&architecture_path) + .map_err(|error| format!("read {}: {error}", architecture_path.display()))?; + let architecture = toml::from_str::<ArchitectureIdentity>(&architecture_raw) + .map_err(|error| format!("parse {}: {error}", architecture_path.display()))?; + + let architecture_packages = architecture + .package + .iter() + .map(|package| package.name.clone()) + .collect::<BTreeSet<_>>(); + api_leakage::validate_policy_catalog(workspace_root, &architecture_packages)?; + dependency_boundary::validate_policy_catalog(workspace_root, &architecture_packages)?; + validate_workspace_toolchain(workspace_root, &architecture)?; + validate_public_package_metadata(workspace_root, &architecture)?; + validate_no_production_sibling_paths(workspace_root)?; + validate_public_dependency_versions(workspace_root, &architecture)?; + + let ledger_path = workspace_root.join(DEVIATIONS_RELATIVE); + let ledger_raw = fs::read_to_string(&ledger_path) + .map_err(|error| format!("read {}: {error}", ledger_path.display()))?; + validate_ledger(workspace_root, &architecture.spec_id, &ledger_raw) +} + +pub fn validate_dependency_boundaries(workspace_root: &Path) -> Result<(), String> { + validate(workspace_root)?; + dependency_boundary::validate_resolved_boundaries(workspace_root) +} + +pub fn validate_api_boundaries(workspace_root: &Path) -> Result<(), String> { + validate(workspace_root)?; + api_leakage::validate_public_api(workspace_root) +} + +pub fn validate_ci(workspace_root: &Path) -> Result<(), String> { + validate(workspace_root)?; + dependency_boundary::validate_resolved_boundaries(workspace_root)?; + api_leakage::validate_public_api(workspace_root) +} + +fn validate_workspace_toolchain( + workspace_root: &Path, + architecture: &ArchitectureIdentity, +) -> Result<(), String> { + let manifest_path = workspace_root.join("Cargo.toml"); + let manifest_raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let manifest = toml::from_str::<WorkspaceManifest>(&manifest_raw) + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + if manifest.workspace.resolver != architecture.resolver || architecture.resolver != "3" { + return Err(format!( + "workspace resolver {} must match architecture resolver {}", + manifest.workspace.resolver, architecture.resolver + )); + } + if manifest.workspace.package.rust_version != architecture.rust_version + || architecture.rust_version != "1.97.1" + { + return Err(format!( + "workspace rust-version {} must match architecture rust_version {}", + manifest.workspace.package.rust_version, architecture.rust_version + )); + } + let workspace_package = &manifest.workspace.package; + let public_metadata = &manifest.workspace.metadata.radroots.public_package; + let repository = architecture + .repositories + .values() + .find(|repository| repository.url == workspace_package.repository) + .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?; + if workspace_package.version != repository.version + || public_metadata.version != repository.version + { + return Err(format!( + "workspace package version {} and public package version source {} must match frozen repository version {}", + workspace_package.version, public_metadata.version, repository.version + )); + } + for member in &manifest.workspace.members { + let member_path = workspace_root.join(member).join("Cargo.toml"); + let member_raw = fs::read_to_string(&member_path) + .map_err(|error| format!("read {}: {error}", member_path.display()))?; + let member_manifest = member_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", member_path.display()))?; + let package = member_manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} is missing [package]", member_path.display()))?; + let package_name = package + .get("name") + .and_then(toml::Value::as_str) + .ok_or_else(|| format!("{} is missing package.name", member_path.display()))?; + let package_version = match package.get("version") { + Some(toml::Value::String(version)) => version.as_str(), + Some(toml::Value::Table(source)) + if source.get("workspace").and_then(toml::Value::as_bool) == Some(true) => + { + workspace_package.version.as_str() + } + _ => { + return Err(format!( + "workspace package {package_name} must declare version 0.1.0-alpha or inherit it from workspace.package" + )); + } + }; + if package_version != repository.version { + return Err(format!( + "workspace package {package_name} version {package_version} must match frozen repository version {}", + repository.version + )); + } + } + if public_metadata.authors != PUBLIC_AUTHORS { + return Err("public package authors source must match the workspace convention".to_owned()); + } + if public_metadata.readme != PUBLIC_README { + return Err(format!( + "public package readme source must be {PUBLIC_README}" + )); + } + let lints = &manifest.workspace.lints; + if lints.rust.unsafe_code != "forbid" + || lints.rustdoc.broken_intra_doc_links != "deny" + || lints.clippy.dbg_macro != "deny" + || lints.clippy.todo != "deny" + || lints.clippy.unimplemented != "deny" + { + return Err( + "workspace lints must forbid unsafe code and deny the approved rustdoc/Clippy baseline" + .to_owned(), + ); + } + if workspace_package.edition != architecture.edition || architecture.edition != "2024" { + return Err(format!( + "workspace edition {} must match architecture edition {}", + workspace_package.edition, architecture.edition + )); + } + if workspace_package.license != architecture.license { + return Err(format!( + "workspace license {} must match architecture license {}", + workspace_package.license, architecture.license + )); + } + if !architecture + .canonical_repositories + .contains(&workspace_package.repository) + { + return Err(format!( + "workspace repository {} is not canonical", + workspace_package.repository + )); + } + if workspace_package.homepage != PUBLIC_HOMEPAGE { + return Err(format!( + "workspace homepage {} must be {PUBLIC_HOMEPAGE}", + workspace_package.homepage + )); + } + if workspace_package.authors != PUBLIC_AUTHORS { + return Err("workspace authors must match the public package convention".to_owned()); + } + if !workspace_root.join(&workspace_package.readme).is_file() { + return Err(format!( + "workspace readme {} does not exist", + workspace_package.readme + )); + } + for license in ["LICENSE-MIT", "LICENSE-APACHE"] { + if !workspace_root.join(license).is_file() { + return Err(format!("workspace is missing {license}")); + } + } + let toolchain_path = workspace_root.join("rust-toolchain.toml"); + let toolchain_raw = fs::read_to_string(&toolchain_path) + .map_err(|error| format!("read {}: {error}", toolchain_path.display()))?; + let toolchain = toolchain_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", toolchain_path.display()))?; + let channel = toolchain + .get("toolchain") + .and_then(|value| value.get("channel")) + .and_then(toml::Value::as_str); + if channel != Some(architecture.rust_version.as_str()) { + return Err(format!( + "rust-toolchain.toml channel must match architecture rust_version {}", + architecture.rust_version + )); + } + Ok(()) +} + +fn validate_public_package_metadata( + workspace_root: &Path, + architecture: &ArchitectureIdentity, +) -> Result<(), String> { + let workspace_raw = fs::read_to_string(workspace_root.join("Cargo.toml")) + .map_err(|error| format!("read workspace Cargo.toml: {error}"))?; + let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw) + .map_err(|error| format!("parse workspace Cargo.toml: {error}"))?; + let repository = architecture + .repositories + .values() + .find(|repository| repository.url == workspace.workspace.package.repository) + .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?; + let local_packages = repository.packages.iter().cloned().collect::<BTreeSet<_>>(); + let all_packages = architecture + .package + .iter() + .map(|package| package.name.as_str()) + .collect::<BTreeSet<_>>(); + let mut found_local_packages = BTreeSet::new(); + + for member in &workspace.workspace.members { + let manifest_path = workspace_root.join(member).join("Cargo.toml"); + let raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} is missing [package]", manifest_path.display()))?; + let name = package + .get("name") + .and_then(toml::Value::as_str) + .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))?; + if !all_packages.contains(name) { + continue; + } + if !local_packages.contains(name) { + return Err(format!( + "public package {name} belongs to a different canonical repository" + )); + } + found_local_packages.insert(name.to_owned()); + validate_public_manifest_field( + package, + "version", + &workspace.workspace.package.version, + &repository.version, + name, + )?; + validate_public_manifest_field( + package, + "edition", + &workspace.workspace.package.edition, + &architecture.edition, + name, + )?; + validate_public_manifest_field( + package, + "rust-version", + &workspace.workspace.package.rust_version, + &architecture.rust_version, + name, + )?; + validate_public_manifest_field( + package, + "license", + &workspace.workspace.package.license, + &architecture.license, + name, + )?; + validate_public_manifest_field( + package, + "repository", + &workspace.workspace.package.repository, + &repository.url, + name, + )?; + validate_public_manifest_field( + package, + "homepage", + &workspace.workspace.package.homepage, + PUBLIC_HOMEPAGE, + name, + )?; + let authors = resolve_public_authors(package, &workspace.workspace.package.authors) + .ok_or_else(|| format!("public package {name} must declare authors"))?; + if authors != PUBLIC_AUTHORS { + return Err(format!( + "public package {name} authors must match the workspace convention" + )); + } + let readme = package.get("readme").and_then(toml::Value::as_str); + if readme != Some(PUBLIC_README) + || !workspace_root.join(member).join(PUBLIC_README).is_file() + { + return Err(format!( + "public package {name} must use an existing crate-local {PUBLIC_README}" + )); + } + let publish_registries = package + .get("publish") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("public package {name} must set publish = [\"crates-io\"]"))?; + if publish_registries.len() != 1 || publish_registries[0].as_str() != Some("crates-io") { + return Err(format!( + "public package {name} must set publish = [\"crates-io\"]" + )); + } + let inherits_lints = manifest + .get("lints") + .and_then(toml::Value::as_table) + .is_some_and(|lints| { + lints.len() == 1 + && lints.get("workspace").and_then(toml::Value::as_bool) == Some(true) + }); + if !inherits_lints { + return Err(format!( + "public package {name} must inherit the workspace lint policy" + )); + } + } + if found_local_packages != local_packages { + let missing = local_packages + .difference(&found_local_packages) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = found_local_packages + .difference(&local_packages) + .cloned() + .collect::<Vec<_>>() + .join(", "); + return Err(format!( + "workspace public package inventory is missing: {missing}; workspace public package inventory has unallocated packages: {extra}" + )); + } + Ok(()) +} + +fn validate_public_dependency_versions( + workspace_root: &Path, + architecture: &ArchitectureIdentity, +) -> Result<(), String> { + let workspace_path = workspace_root.join("Cargo.toml"); + let workspace_raw = fs::read_to_string(&workspace_path) + .map_err(|error| format!("read {}: {error}", workspace_path.display()))?; + let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw) + .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?; + let workspace_value = workspace_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?; + let workspace_dependencies = workspace_value + .get("workspace") + .and_then(toml::Value::as_table) + .and_then(|workspace| workspace.get("dependencies")) + .and_then(toml::Value::as_table); + let repository = architecture + .repositories + .values() + .find(|repository| repository.url == workspace.workspace.package.repository) + .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?; + let local_packages = repository + .packages + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let public_packages = architecture + .package + .iter() + .map(|package| package.name.as_str()) + .collect::<BTreeSet<_>>(); + let package_versions = architecture + .repositories + .values() + .flat_map(|repository| { + repository + .packages + .iter() + .map(|package| (package.as_str(), repository.version.as_str())) + }) + .collect::<BTreeMap<_, _>>(); + + for member in &workspace.workspace.members { + let manifest_path = workspace_root.join(member).join("Cargo.toml"); + let raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + let package_name = manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .ok_or_else(|| format!("{} is missing package.name", manifest_path.display()))?; + if !local_packages.contains(package_name) { + continue; + } + let policy = PublicDependencyPolicy { + workspace_root, + workspace_dependencies, + public_packages: &public_packages, + local_packages: &local_packages, + package_versions: &package_versions, + }; + validate_public_dependency_sections(member, package_name, &manifest, &policy)?; + } + Ok(()) +} + +struct PublicDependencyPolicy<'a> { + workspace_root: &'a Path, + workspace_dependencies: Option<&'a toml::value::Table>, + public_packages: &'a BTreeSet<&'a str>, + local_packages: &'a BTreeSet<&'a str>, + package_versions: &'a BTreeMap<&'a str, &'a str>, +} + +fn validate_public_dependency_sections( + member: &str, + owner: &str, + manifest: &toml::Value, + policy: &PublicDependencyPolicy<'_>, +) -> Result<(), String> { + let manifest_table = manifest + .as_table() + .ok_or_else(|| format!("{member}/Cargo.toml must be a TOML table"))?; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = manifest_table.get(section).and_then(toml::Value::as_table) { + validate_public_dependency_table(member, owner, section, dependencies, policy)?; + } + } + if let Some(targets) = manifest_table.get("target").and_then(toml::Value::as_table) { + for (target, target_value) in targets { + let Some(target_table) = target_value.as_table() else { + continue; + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = + target_table.get(section).and_then(toml::Value::as_table) + { + validate_public_dependency_table( + member, + owner, + &format!("target.{target}.{section}"), + dependencies, + policy, + )?; + } + } + } + } + Ok(()) +} + +fn validate_public_dependency_table( + member: &str, + owner: &str, + section: &str, + dependencies: &toml::value::Table, + policy: &PublicDependencyPolicy<'_>, +) -> Result<(), String> { + for (dependency_key, declaration) in dependencies { + let inherits_workspace = declaration + .as_table() + .and_then(|table| table.get("workspace")) + .and_then(toml::Value::as_bool) + == Some(true); + let resolved = if inherits_workspace { + policy + .workspace_dependencies + .and_then(|dependencies| dependencies.get(dependency_key)) + .ok_or_else(|| { + format!( + "public package {owner} {section}.{dependency_key} inherits a missing workspace dependency" + ) + })? + } else { + declaration + }; + let resolved_table = resolved.as_table(); + let dependency_path = resolved_table + .and_then(|table| table.get("path")) + .and_then(toml::Value::as_str); + let declared_package = resolved_table + .and_then(|table| table.get("package")) + .and_then(toml::Value::as_str); + let path_base = if inherits_workspace { + policy.workspace_root.to_path_buf() + } else { + policy.workspace_root.join(member) + }; + let path_package = if declared_package.is_none() { + dependency_path + .map(|path| dependency_package_name(&path_base.join(path))) + .transpose()? + } else { + None + }; + let dependency_name = declared_package + .or(path_package.as_deref()) + .unwrap_or(dependency_key.as_str()); + if !policy.public_packages.contains(dependency_name) { + continue; + } + let version = match resolved { + toml::Value::String(version) => Some(version.as_str()), + toml::Value::Table(table) => table.get("version").and_then(toml::Value::as_str), + _ => None, + }; + let governed_version = policy + .package_versions + .get(dependency_name) + .ok_or_else(|| { + format!("public dependency {dependency_name} has no repository version authority") + })?; + let exact_version = format!("={governed_version}"); + if policy.local_packages.contains(dependency_name) + && (dependency_path.is_none() || version != Some(exact_version.as_str())) + { + return Err(format!( + "public package {owner} {section}.{dependency_key} dependency on {dependency_name} must declare path and exact version {exact_version}" + )); + } + if !policy.local_packages.contains(dependency_name) + && (dependency_path.is_some() || version != Some(exact_version.as_str())) + { + return Err(format!( + "public package {owner} {section}.{dependency_key} cross-repository dependency on {dependency_name} must declare exact registry version {exact_version} without a path" + )); + } + } + Ok(()) +} + +fn validate_no_production_sibling_paths(workspace_root: &Path) -> Result<(), String> { + let canonical_root = fs::canonicalize(workspace_root) + .map_err(|error| format!("canonicalize {}: {error}", workspace_root.display()))?; + let workspace_path = workspace_root.join("Cargo.toml"); + let workspace_raw = fs::read_to_string(&workspace_path) + .map_err(|error| format!("read {}: {error}", workspace_path.display()))?; + let workspace = toml::from_str::<WorkspaceManifest>(&workspace_raw) + .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?; + let workspace_value = workspace_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", workspace_path.display()))?; + if let Some(dependencies) = workspace_value + .get("workspace") + .and_then(toml::Value::as_table) + .and_then(|workspace| workspace.get("dependencies")) + .and_then(toml::Value::as_table) + { + validate_dependency_path_table( + &canonical_root, + &canonical_root, + "workspace", + "workspace.dependencies", + dependencies, + )?; + } + for member in &workspace.workspace.members { + let manifest_path = workspace_root.join(member).join("Cargo.toml"); + let raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + validate_manifest_dependency_paths( + &canonical_root, + &canonical_root.join(member), + member, + &manifest, + )?; + } + Ok(()) +} + +fn validate_manifest_dependency_paths( + workspace_root: &Path, + package_root: &Path, + owner: &str, + manifest: &toml::Value, +) -> Result<(), String> { + let Some(manifest_table) = manifest.as_table() else { + return Err(format!("{owner}/Cargo.toml must be a TOML table")); + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = manifest_table.get(section).and_then(toml::Value::as_table) { + validate_dependency_path_table( + workspace_root, + package_root, + owner, + section, + dependencies, + )?; + } + } + if let Some(targets) = manifest_table.get("target").and_then(toml::Value::as_table) { + for (target, target_value) in targets { + let Some(target_table) = target_value.as_table() else { + continue; + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = + target_table.get(section).and_then(toml::Value::as_table) + { + validate_dependency_path_table( + workspace_root, + package_root, + owner, + &format!("target.{target}.{section}"), + dependencies, + )?; + } + } + } + } + Ok(()) +} + +fn validate_dependency_path_table( + workspace_root: &Path, + path_base: &Path, + owner: &str, + section: &str, + dependencies: &toml::value::Table, +) -> Result<(), String> { + for (dependency, declaration) in dependencies { + let Some(path) = declaration + .as_table() + .and_then(|table| table.get("path")) + .and_then(toml::Value::as_str) + else { + continue; + }; + let resolved = fs::canonicalize(path_base.join(path)).map_err(|error| { + format!( + "resolve production dependency {owner} {section}.{dependency} path {path}: {error}" + ) + })?; + if !resolved.starts_with(workspace_root) { + return Err(format!( + "production dependency {owner} {section}.{dependency} path {path} escapes the repository; use a registry version or an external local-development override" + )); + } + } + Ok(()) +} + +fn dependency_package_name(package_root: &Path) -> Result<String, String> { + let manifest_path = package_root.join("Cargo.toml"); + let raw = fs::read_to_string(&manifest_path).map_err(|error| { + format!( + "read dependency manifest {}: {error}", + manifest_path.display() + ) + })?; + let manifest = raw.parse::<toml::Value>().map_err(|error| { + format!( + "parse dependency manifest {}: {error}", + manifest_path.display() + ) + })?; + manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .map(str::to_owned) + .ok_or_else(|| format!("{} is missing package.name", manifest_path.display())) +} + +fn validate_public_manifest_field( + package: &toml::value::Table, + key: &str, + workspace_value: &str, + expected: &str, + package_name: &str, +) -> Result<(), String> { + let value = resolve_public_string(package, key, workspace_value) + .ok_or_else(|| format!("public package {package_name} must declare {key}"))?; + if value != expected { + return Err(format!( + "public package {package_name} {key} {value} must be {expected}" + )); + } + Ok(()) +} + +fn resolve_public_string<'a>( + package: &'a toml::value::Table, + key: &str, + workspace_value: &'a str, +) -> Option<&'a str> { + match package.get(key)? { + toml::Value::String(value) => Some(value), + toml::Value::Table(value) + if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) => + { + Some(workspace_value) + } + _ => None, + } +} + +fn resolve_public_authors<'a>( + package: &'a toml::value::Table, + workspace_authors: &'a [String], +) -> Option<Vec<&'a str>> { + match package.get("authors")? { + toml::Value::Array(values) => values.iter().map(toml::Value::as_str).collect(), + toml::Value::Table(value) + if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) => + { + Some(workspace_authors.iter().map(String::as_str).collect()) + } + _ => None, + } +} + +fn validate_workspace_members(workspace_root: &Path) -> Result<(), String> { + let manifest_path = workspace_root.join("Cargo.toml"); + let manifest_raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("read {}: {error}", manifest_path.display()))?; + let manifest = toml::from_str::<WorkspaceMembershipManifest>(&manifest_raw) + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + let manifest_value = manifest_raw + .parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", manifest_path.display()))?; + validate_manifest_dependency_keys(&manifest_value, "Cargo.toml")?; + let declared = manifest + .workspace + .members + .into_iter() + .collect::<BTreeSet<_>>(); + let mut discovered = BTreeSet::new(); + for root in ["crates", "tools"] { + let directory = workspace_root.join(root); + for entry in fs::read_dir(&directory) + .map_err(|error| format!("read {}: {error}", directory.display()))? + { + let entry = + entry.map_err(|error| format!("read {} entry: {error}", directory.display()))?; + if entry + .file_type() + .map_err(|error| format!("read {} type: {error}", entry.path().display()))? + .is_dir() + && entry.path().join("Cargo.toml").is_file() + { + let directory_name = entry.file_name().to_string_lossy().into_owned(); + require_lower_snake_case( + &directory_name, + &format!("{root}/{directory_name} crate directory"), + )?; + let package_manifest_path = entry.path().join("Cargo.toml"); + let package_manifest_raw = + fs::read_to_string(&package_manifest_path).map_err(|error| { + format!("read {}: {error}", package_manifest_path.display()) + })?; + let package_manifest = + package_manifest_raw + .parse::<toml::Value>() + .map_err(|error| { + format!("parse {}: {error}", package_manifest_path.display()) + })?; + let package_name = package_manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .ok_or_else(|| { + format!( + "{} is missing package.name", + package_manifest_path.display() + ) + })?; + require_lower_snake_case( + package_name, + &format!("{} package.name", package_manifest_path.display()), + )?; + validate_manifest_dependency_keys( + &package_manifest, + &package_manifest_path.display().to_string(), + )?; + discovered.insert(format!("{root}/{directory_name}")); + } + } + } + if declared != discovered { + let missing = discovered + .difference(&declared) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let unknown = declared + .difference(&discovered) + .cloned() + .collect::<Vec<_>>() + .join(", "); + return Err(format!( + "workspace membership is missing local package roots: {missing}; workspace membership has unknown package roots: {unknown}" + )); + } + Ok(()) +} + +fn validate_manifest_dependency_keys(manifest: &toml::Value, label: &str) -> Result<(), String> { + let Some(manifest) = manifest.as_table() else { + return Err(format!("{label} must be a TOML table")); + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = manifest.get(section).and_then(toml::Value::as_table) { + validate_dependency_keys(dependencies, label, section)?; + } + } + if let Some(workspace_dependencies) = manifest + .get("workspace") + .and_then(toml::Value::as_table) + .and_then(|workspace| workspace.get("dependencies")) + .and_then(toml::Value::as_table) + { + validate_dependency_keys(workspace_dependencies, label, "workspace.dependencies")?; + } + if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) { + for (target, target_value) in targets { + let Some(target_table) = target_value.as_table() else { + continue; + }; + for section in ["dependencies", "dev-dependencies", "build-dependencies"] { + if let Some(dependencies) = + target_table.get(section).and_then(toml::Value::as_table) + { + validate_dependency_keys( + dependencies, + label, + &format!("target.{target}.{section}"), + )?; + } + } + } + } + Ok(()) +} + +fn validate_dependency_keys( + dependencies: &toml::value::Table, + label: &str, + section: &str, +) -> Result<(), String> { + for key in dependencies + .keys() + .filter(|key| key.starts_with("radroots")) + { + require_lower_snake_case(key, &format!("{label} {section} dependency key"))?; + } + Ok(()) +} + +fn require_lower_snake_case(value: &str, label: &str) -> Result<(), String> { + if is_lower_snake_case(value) { + Ok(()) + } else { + Err(format!("{label} `{value}` must use lowercase snake case")) + } +} + +fn is_lower_snake_case(value: &str) -> bool { + !value.is_empty() + && value.split('_').all(|segment| { + segment + .as_bytes() + .first() + .is_some_and(u8::is_ascii_lowercase) + && segment + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) + }) +} + +fn validate_ledger( + workspace_root: &Path, + expected_architecture_id: &str, + raw: &str, +) -> Result<(), String> { + let ledger = toml::from_str::<DeviationLedger>(raw) + .map_err(|error| format!("parse {DEVIATIONS_RELATIVE}: {error}"))?; + if ledger.schema_version != 1 { + return Err("deviation ledger schema_version must be 1".to_owned()); + } + if ledger.architecture_id != expected_architecture_id + || ledger.architecture_id != ARCHITECTURE_ID + { + return Err(format!( + "deviation ledger architecture_id {} must match {}", + ledger.architecture_id, expected_architecture_id + )); + } + + let mut ids = BTreeSet::new(); + for record in &ledger.deviation { + validate_record(workspace_root, record)?; + if !ids.insert(record.id.as_str()) { + return Err(format!("duplicate deviation id {}", record.id)); + } + } + Ok(()) +} + +fn validate_record(workspace_root: &Path, record: &DeviationRecord) -> Result<(), String> { + if !is_deviation_id(&record.id) { + return Err(format!("deviation id {} must use RCRV1-DEV-NNN", record.id)); + } + if !is_iso_date(&record.date) { + return Err(format!("deviation {} date must use YYYY-MM-DD", record.id)); + } + if !matches!(record.status.as_str(), "active" | "closed" | "superseded") { + return Err(format!( + "deviation {} status must be active, closed, or superseded", + record.id + )); + } + require_text(&record.id, "approval", &record.approval)?; + require_text(&record.id, "replacement_action", &record.replacement_action)?; + require_text(&record.id, "unresolved_risk", &record.unresolved_risk)?; + require_nonempty_list(&record.id, "affected_steps", &record.affected_steps)?; + require_nonempty_list(&record.id, "spec_anchors", &record.spec_anchors)?; + require_nonempty_list(&record.id, "source_evidence", &record.source_evidence)?; + require_nonempty_list(&record.id, "verification", &record.verification)?; + + for step in &record.affected_steps { + let valid = step.len() == 3 + && step.bytes().all(|byte| byte.is_ascii_digit()) + && step + .parse::<u16>() + .is_ok_and(|value| (1..=315).contains(&value)); + if !valid { + return Err(format!( + "deviation {} affected step {} must be in 001..315", + record.id, step + )); + } + } + for anchor in &record.spec_anchors { + validate_spec_anchor(workspace_root, &record.id, anchor)?; + } + if record.status == "active" && !record.closure_evidence.is_empty() { + return Err(format!( + "active deviation {} must not carry closure_evidence", + record.id + )); + } + if record.status != "active" { + require_nonempty_list(&record.id, "closure_evidence", &record.closure_evidence)?; + } + + let _ = (record.normative_architecture_change, record.adr_required); + Ok(()) +} + +fn validate_spec_anchor( + workspace_root: &Path, + deviation_id: &str, + anchor: &str, +) -> Result<(), String> { + let (relative, fragment) = anchor + .split_once('#') + .map_or((anchor, None), |(path, fragment)| (path, Some(fragment))); + if relative.trim().is_empty() || fragment.is_some_and(|value| value.trim().is_empty()) { + return Err(format!( + "deviation {deviation_id} has invalid spec anchor {anchor}" + )); + } + let path = Path::new(relative); + if path.components().any(|component| { + matches!( + component, + Component::ParentDir | Component::RootDir | Component::Prefix(_) + ) + }) { + return Err(format!( + "deviation {deviation_id} spec anchor must be repository-relative: {anchor}" + )); + } + if !relative.starts_with("docs/specs/") || !workspace_root.join(path).is_file() { + return Err(format!( + "deviation {deviation_id} spec anchor does not resolve to a local spec: {anchor}" + )); + } + Ok(()) +} + +fn require_text(deviation_id: &str, field: &str, value: &str) -> Result<(), String> { + if value.trim().is_empty() { + return Err(format!( + "deviation {deviation_id} field {field} must not be empty" + )); + } + Ok(()) +} + +fn require_nonempty_list(deviation_id: &str, field: &str, values: &[String]) -> Result<(), String> { + if values.is_empty() || values.iter().any(|value| value.trim().is_empty()) { + return Err(format!( + "deviation {deviation_id} field {field} must contain non-empty values" + )); + } + Ok(()) +} + +fn is_deviation_id(value: &str) -> bool { + value + .strip_prefix("RCRV1-DEV-") + .is_some_and(|suffix| suffix.len() == 3 && suffix.bytes().all(|byte| byte.is_ascii_digit())) +} + +fn is_iso_date(value: &str) -> bool { + value.len() == 10 + && value.as_bytes()[4] == b'-' + && value.as_bytes()[7] == b'-' + && value + .bytes() + .enumerate() + .all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit()) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeMap, + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{ + ArchitectureIdentity, ArchitecturePackage, ArchitectureRepository, validate_ledger, + validate_no_production_sibling_paths, validate_public_dependency_versions, + validate_public_package_metadata, validate_workspace_members, validate_workspace_toolchain, + }; + + fn test_root(label: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos(); + let root = std::env::temp_dir().join(format!("radroots_architecture_{label}_{nonce}")); + fs::create_dir_all(root.join("docs/specs")).expect("create spec root"); + fs::write( + root.join("docs/specs/radroots_crates_release_v1.md"), + "# Architecture\n", + ) + .expect("write spec"); + root + } + + fn complete_ledger() -> &'static str { + r#"schema_version = 1 +architecture_id = "radroots.crates.release.v1" + +[[deviation]] +id = "RCRV1-DEV-001" +date = "2026-07-27" +status = "active" +approval = "Explicit user correction dated 2026-07-27." +affected_steps = ["015", "016"] +spec_anchors = ["docs/specs/radroots_crates_release_v1.md#repository-topology"] +source_evidence = ["The approved architecture assigns packages to the existing lib and sdk repositories."] +replacement_action = "Keep both standalone repositories and verify them independently." +verification = ["Repository-local architecture validation passes."] +unresolved_risk = "Remote publication remains separately authorized." +normative_architecture_change = false +adr_required = false +"# + } + + fn architecture() -> ArchitectureIdentity { + ArchitectureIdentity { + spec_id: "radroots.crates.release.v1".to_string(), + edition: "2024".to_string(), + resolver: "3".to_string(), + rust_version: "1.97.1".to_string(), + license: "MIT OR Apache-2.0".to_string(), + canonical_repositories: vec!["https://github.com/radrootslabs/sdk".to_string()], + repositories: BTreeMap::from([( + "sdk".to_string(), + ArchitectureRepository { + url: "https://github.com/radrootslabs/sdk".to_string(), + version: "0.1.0-alpha".to_string(), + packages: vec!["radroots".to_string()], + }, + )]), + package: vec![ArchitecturePackage { + name: "radroots".to_string(), + }], + } + } + + fn complete_workspace_manifest(members: &str) -> String { + format!( + "[workspace]\nmembers = [{members}]\nresolver = \"3\"\n\n[workspace.package]\nversion = \"0.1.0-alpha\"\nedition = \"2024\"\nrust-version = \"1.97.1\"\nlicense = \"MIT OR Apache-2.0\"\nrepository = \"https://github.com/radrootslabs/sdk\"\nhomepage = \"https://radroots.org\"\nreadme = \"README\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\n\n[workspace.metadata.radroots.public-package]\nversion = \"0.1.0-alpha\"\nauthors = [\"Tyson Lupul <tyson@radroots.org>\"]\nreadme = \"README.md\"\n\n[workspace.lints.rust]\nunsafe_code = \"forbid\"\n\n[workspace.lints.rustdoc]\nbroken_intra_doc_links = \"deny\"\n\n[workspace.lints.clippy]\ndbg_macro = \"deny\"\ntodo = \"deny\"\nunimplemented = \"deny\"\n" + ) + } + + #[test] + fn accepts_complete_active_deviation() { + let root = test_root("complete"); + validate_ledger(&root, "radroots.crates.release.v1", complete_ledger()) + .expect("complete deviation"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_incomplete_active_deviation() { + let root = test_root("incomplete"); + let incomplete = complete_ledger().replace( + "spec_anchors = [\"docs/specs/radroots_crates_release_v1.md#repository-topology\"]", + "spec_anchors = []", + ); + let error = validate_ledger(&root, "radroots.crates.release.v1", &incomplete) + .expect_err("missing anchor must fail"); + assert!(error.contains("field spec_anchors must contain non-empty values")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn workspace_membership_requires_every_local_package_root() { + let root = test_root("workspace_members"); + for path in ["crates/a", "tools/xtask"] { + fs::create_dir_all(root.join(path)).expect("create package root"); + fs::write( + root.join(path).join("Cargo.toml"), + "[package]\nname = \"fixture\"\n", + ) + .expect("write package manifest"); + } + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\", \"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.package]\nrust-version = \"1.97.1\"\n", + ) + .expect("write complete workspace"); + validate_workspace_members(&root).expect("complete membership"); + + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.package]\nrust-version = \"1.97.1\"\n", + ) + .expect("write incomplete workspace"); + let error = validate_workspace_members(&root).expect_err("missing member must fail"); + assert!(error.contains("missing local package roots: crates/a")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn workspace_identity_rejects_kebab_case_crate_names_and_dependency_keys() { + let root = test_root("workspace_snake_case_identity"); + fs::create_dir_all(root.join("crates/radroots_probe")).expect("create package root"); + fs::create_dir_all(root.join("tools/xtask")).expect("create xtask root"); + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n", + ) + .expect("write workspace manifest"); + fs::write( + root.join("crates/radroots_probe/Cargo.toml"), + "[package]\nname = \"radroots-probe\"\n", + ) + .expect("write kebab package manifest"); + fs::write( + root.join("tools/xtask/Cargo.toml"), + "[package]\nname = \"xtask\"\n", + ) + .expect("write xtask manifest"); + + let package_error = validate_workspace_members(&root) + .expect_err("kebab-case Cargo package identity must fail"); + assert!(package_error.contains("package.name")); + assert!(package_error.contains("lowercase snake case")); + + fs::write( + root.join("crates/radroots_probe/Cargo.toml"), + "[package]\nname = \"radroots_probe\"\n", + ) + .expect("write snake package manifest"); + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/radroots_probe\", \"tools/xtask\"]\nresolver = \"3\"\n\n[workspace.dependencies]\nradroots-probe = \"0.1.0-alpha\"\n", + ) + .expect("write kebab dependency key"); + + let dependency_error = validate_workspace_members(&root) + .expect_err("kebab-case Radroots dependency key must fail"); + assert!(dependency_error.contains("workspace.dependencies dependency key")); + assert!(dependency_error.contains("lowercase snake case")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn workspace_toolchain_requires_exact_resolver_and_rust_version() { + let root = test_root("workspace_toolchain"); + fs::write(root.join("Cargo.toml"), complete_workspace_manifest("")) + .expect("write workspace manifest"); + for path in ["README", "LICENSE-MIT", "LICENSE-APACHE"] { + fs::write(root.join(path), "fixture\n").expect("write workspace metadata file"); + } + fs::write( + root.join("rust-toolchain.toml"), + "[toolchain]\nchannel = \"1.97.1\"\n", + ) + .expect("write toolchain"); + let architecture = architecture(); + validate_workspace_toolchain(&root, &architecture).expect("exact toolchain policy"); + + fs::write( + root.join("rust-toolchain.toml"), + "[toolchain]\nchannel = \"1.97.0\"\n", + ) + .expect("write mismatched toolchain"); + let error = validate_workspace_toolchain(&root, &architecture) + .expect_err("mismatched toolchain must fail"); + assert!(error.contains("channel must match")); + + fs::write( + root.join("rust-toolchain.toml"), + "[toolchain]\nchannel = \"1.97.1\"\n", + ) + .expect("restore toolchain"); + let manifest = complete_workspace_manifest("").replacen( + "[workspace.package]\nversion = \"0.1.0-alpha\"", + "[workspace.package]\nversion = \"0.1.0\"", + 1, + ); + fs::write(root.join("Cargo.toml"), manifest).expect("write mismatched version cohort"); + let version_error = validate_workspace_toolchain(&root, &architecture) + .expect_err("mismatched version cohort must fail"); + assert!(version_error.contains("must match frozen repository version")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_package_metadata_requires_canonical_inheritance() { + let root = test_root("public_package_metadata"); + fs::create_dir_all(root.join("crates/radroots")).expect("create public package"); + fs::write( + root.join("Cargo.toml"), + complete_workspace_manifest("\"crates/radroots\""), + ) + .expect("write workspace manifest"); + let manifest = "[package]\nname = \"radroots\"\nversion.workspace = true\nedition.workspace = true\nrust-version.workspace = true\nlicense.workspace = true\nrepository.workspace = true\nhomepage.workspace = true\nauthors.workspace = true\nreadme = \"README.md\"\npublish = [\"crates-io\"]\n\n[lints]\nworkspace = true\n"; + fs::write(root.join("crates/radroots/Cargo.toml"), manifest) + .expect("write public manifest"); + fs::write(root.join("crates/radroots/README.md"), "fixture\n") + .expect("write public readme"); + validate_public_package_metadata(&root, &architecture()).expect("canonical metadata"); + + fs::write( + root.join("crates/radroots/Cargo.toml"), + manifest.replace("[lints]\nworkspace = true\n", ""), + ) + .expect("write incomplete public manifest"); + let error = validate_public_package_metadata(&root, &architecture()) + .expect_err("missing lint inheritance must fail"); + assert!(error.contains("must inherit the workspace lint policy")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_package_inventory_requires_every_local_package() { + let root = test_root("public_package_inventory"); + fs::write(root.join("Cargo.toml"), complete_workspace_manifest("")) + .expect("write workspace manifest"); + let error = validate_public_package_metadata(&root, &architecture()) + .expect_err("missing local public package must fail"); + assert!(error.contains("workspace public package inventory is missing: radroots")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_dependencies_require_path_and_exact_version() { + let root = test_root("public_dependency_version"); + fs::create_dir_all(root.join("crates/radroots")).expect("create public package"); + fs::create_dir_all(root.join("crates/dependency")).expect("create dependency package"); + fs::write( + root.join("Cargo.toml"), + format!( + "{}\n[workspace.dependencies]\nradroots_core = {{ package = \"radroots_core\", path = \"crates/dependency\", version = \"=0.1.0-alpha\" }}\n", + complete_workspace_manifest("\"crates/radroots\"") + ), + ) + .expect("write workspace manifest"); + fs::write( + root.join("crates/radroots/Cargo.toml"), + "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nradroots_core = { workspace = true }\n", + ) + .expect("write public manifest"); + fs::write( + root.join("crates/dependency/Cargo.toml"), + "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha\"\n", + ) + .expect("write dependency manifest"); + let mut architecture = architecture(); + architecture.package.push(ArchitecturePackage { + name: "radroots_core".to_owned(), + }); + architecture + .repositories + .get_mut("sdk") + .expect("sdk repository") + .packages + .push("radroots_core".to_owned()); + validate_public_dependency_versions(&root, &architecture) + .expect("path plus exact version public dependency"); + + let workspace_path = root.join("Cargo.toml"); + let workspace = fs::read_to_string(&workspace_path).expect("read workspace manifest"); + fs::write( + &workspace_path, + workspace.replace(", version = \"=0.1.0-alpha\"", ""), + ) + .expect("write path-only dependency"); + let error = validate_public_dependency_versions(&root, &architecture) + .expect_err("path-only public dependency must fail"); + assert!(error.contains("must declare path and exact version =0.1.0-alpha")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn production_dependencies_reject_sibling_paths() { + let root = test_root("production_sibling_path"); + let sibling = root.with_extension("sibling"); + fs::create_dir_all(root.join("crates/radroots")).expect("create public package"); + fs::create_dir_all(root.join("crates/dependency")).expect("create local dependency"); + fs::create_dir_all(&sibling).expect("create sibling dependency"); + fs::write( + root.join("Cargo.toml"), + complete_workspace_manifest("\"crates/radroots\""), + ) + .expect("write workspace manifest"); + let sibling_path = sibling.to_string_lossy(); + fs::write( + root.join("crates/radroots/Cargo.toml"), + format!( + "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nprobe = {{ path = \"{sibling_path}\" }}\n" + ), + ) + .expect("write sibling dependency"); + let error = validate_no_production_sibling_paths(&root) + .expect_err("sibling production path must fail"); + assert!(error.contains("escapes the repository")); + + fs::write( + root.join("crates/radroots/Cargo.toml"), + "[package]\nname = \"radroots\"\nversion = \"0.1.0-alpha\"\n\n[dependencies]\nprobe = { path = \"../dependency\" }\n", + ) + .expect("write local dependency"); + fs::create_dir_all(root.join(".cargo")).expect("create cargo config directory"); + fs::write( + root.join(".cargo/config.toml"), + format!("[patch.crates-io]\nprobe = {{ path = \"{sibling_path}\" }}\n"), + ) + .expect("write development override"); + validate_no_production_sibling_paths(&root).expect("in-repository path"); + let _ = fs::remove_dir_all(root); + let _ = fs::remove_dir_all(sibling); + } +} diff --git a/tools/sdk_xtask_import/src/architecture/api_leakage.rs b/tools/sdk_xtask_import/src/architecture/api_leakage.rs @@ -0,0 +1,1433 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Component, Path, PathBuf}, + process::Command, +}; + +use serde::Deserialize; +use syn::{ + Expr, ExprLit, Fields, ForeignItem, ImplItem, Item, Lit, Meta, Path as SynPath, TraitItem, + Type, UseTree, Visibility, visit::Visit, +}; + +const API_BOUNDARIES_RELATIVE: &str = "contracts/releases/api_boundaries.toml"; +const SPEC_ID: &str = "radroots.crates.release.v1"; +const POLICY_SCHEMA_VERSION: u16 = 1; +const CURRENT_STEP: u16 = 25; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ApiBoundaryPolicy { + schema_version: u16, + spec_id: String, + forbidden_public_paths: Vec<String>, + package: Vec<ApiPackagePolicy>, + #[serde(default)] + exception: Vec<ApiException>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ApiPackagePolicy { + name: String, + allowed_public_paths: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ApiException { + id: String, + package: String, + source: String, + forbidden_path: String, + items: Vec<String>, + observed_paths: Vec<String>, + adr: String, + removal_step: u16, + rationale: String, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec<CargoPackage>, + workspace_members: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct CargoPackage { + id: String, + name: String, + manifest_path: String, + targets: Vec<CargoTarget>, +} + +#[derive(Debug, Deserialize)] +struct CargoTarget { + kind: Vec<String>, + src_path: String, +} + +struct ModuleUnit { + module: Vec<String>, + parent: Option<Vec<String>>, + declared_public: bool, + initially_effective: bool, + source_relative: String, + items: Vec<Item>, +} + +#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)] +struct ApiFinding { + package: String, + source: String, + item: String, + forbidden_path: String, + observed_path: String, +} + +#[derive(Debug)] +enum InternalExport { + Module(Vec<String>), + Item(Vec<String>, String), + External, +} + +pub(super) fn validate_policy_catalog( + workspace_root: &Path, + expected_packages: &BTreeSet<String>, +) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + validate_policy(workspace_root, &policy, expected_packages) +} + +pub(super) fn validate_public_api(workspace_root: &Path) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + let expected_packages = policy + .package + .iter() + .map(|package| package.name.clone()) + .collect::<BTreeSet<_>>(); + validate_policy(workspace_root, &policy, &expected_packages)?; + let metadata = load_metadata(workspace_root)?; + let findings = scan_workspace(workspace_root, &policy, &metadata)?; + let unapproved = findings + .into_iter() + .filter(|finding| !exception_matches(&policy.exception, finding)) + .collect::<Vec<_>>(); + if unapproved.is_empty() { + Ok(()) + } else { + Err(format!( + "forbidden public implementation type leakage:\n{}", + unapproved + .iter() + .map(format_finding) + .collect::<Vec<_>>() + .join("\n") + )) + } +} + +fn load_policy(workspace_root: &Path) -> Result<ApiBoundaryPolicy, String> { + let path = workspace_root.join(API_BOUNDARIES_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display())) +} + +fn validate_policy( + workspace_root: &Path, + policy: &ApiBoundaryPolicy, + expected_packages: &BTreeSet<String>, +) -> Result<(), String> { + if policy.schema_version != POLICY_SCHEMA_VERSION { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} schema_version must be {POLICY_SCHEMA_VERSION}" + )); + } + if policy.spec_id != SPEC_ID { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} spec_id must be {SPEC_ID}" + )); + } + + let forbidden = sorted_unique( + "forbidden_public_paths", + &policy.forbidden_public_paths, + false, + )?; + if forbidden.len() != policy.forbidden_public_paths.len() { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} forbidden_public_paths must be unique" + )); + } + for required in [ + "keyring", + "nostr_sdk", + "reqwest", + "sqlx", + "std::os", + "tokio", + ] { + if !forbidden.contains(required) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} must forbid public {required} paths" + )); + } + } + + let mut package_names = BTreeSet::new(); + for package in &policy.package { + if !package_names.insert(package.name.as_str()) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} package {} is declared more than once", + package.name + )); + } + let allowed = sorted_unique( + &format!("package {} allowed_public_paths", package.name), + &package.allowed_public_paths, + true, + )?; + for path in allowed { + if !forbidden.contains(path) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} package {} allows {path}, which is not forbidden globally", + package.name + )); + } + } + } + let actual_packages = package_names + .into_iter() + .map(str::to_owned) + .collect::<BTreeSet<_>>(); + if &actual_packages != expected_packages { + return Err(package_set_mismatch(expected_packages, &actual_packages)); + } + + let package_policy = policy + .package + .iter() + .map(|package| (package.name.as_str(), package)) + .collect::<BTreeMap<_, _>>(); + let mut exception_ids = BTreeSet::new(); + let mut exception_keys = BTreeSet::new(); + for exception in &policy.exception { + if !exception_ids.insert(exception.id.as_str()) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception id {} is duplicated", + exception.id + )); + } + if !valid_exception_id(&exception.id) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception id {} must match RCRV1-API-NNN", + exception.id + )); + } + let package = package_policy + .get(exception.package.as_str()) + .ok_or_else(|| { + format!( + "{API_BOUNDARIES_RELATIVE} exception {} names unknown package {}", + exception.id, exception.package + ) + })?; + if !forbidden.contains(exception.forbidden_path.as_str()) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} names unknown forbidden path {}", + exception.id, exception.forbidden_path + )); + } + if package + .allowed_public_paths + .iter() + .any(|allowed| allowed == &exception.forbidden_path) + { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} is redundant with package allowance {}", + exception.id, exception.forbidden_path + )); + } + validate_relative_source(&exception.id, &exception.source)?; + let items = sorted_unique( + &format!("exception {} items", exception.id), + &exception.items, + false, + )?; + if items.len() != exception.items.len() { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} items must be unique", + exception.id + )); + } + let observed_paths = sorted_unique( + &format!("exception {} observed_paths", exception.id), + &exception.observed_paths, + false, + )?; + if observed_paths.len() != exception.observed_paths.len() + || observed_paths.iter().any(|path| { + !(path == &exception.forbidden_path + || path + .strip_prefix(&exception.forbidden_path) + .is_some_and(|suffix| suffix.starts_with("::"))) + }) + { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} observed_paths must be sorted, unique, and rooted at {}", + exception.id, exception.forbidden_path + )); + } + if exception.removal_step <= CURRENT_STEP { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} expired at Step {}", + exception.id, exception.removal_step + )); + } + if exception.rationale.trim().is_empty() { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} must include a rationale", + exception.id + )); + } + validate_adr(workspace_root, exception)?; + for item in &exception.items { + let key = ( + exception.package.as_str(), + exception.source.as_str(), + item.as_str(), + exception.forbidden_path.as_str(), + ); + if !exception_keys.insert(key) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} duplicates an item-scoped allowance", + exception.id + )); + } + } + } + Ok(()) +} + +fn sorted_unique<'a>( + label: &str, + values: &'a [String], + allow_empty: bool, +) -> Result<BTreeSet<&'a str>, String> { + if !allow_empty && values.is_empty() { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} {label} must not be empty" + )); + } + let unique = values.iter().map(String::as_str).collect::<BTreeSet<_>>(); + if unique.iter().copied().ne(values.iter().map(String::as_str)) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} {label} must be sorted and unique" + )); + } + Ok(unique) +} + +fn valid_exception_id(id: &str) -> bool { + id.strip_prefix("RCRV1-API-") + .is_some_and(|suffix| suffix.len() == 3 && suffix.bytes().all(|byte| byte.is_ascii_digit())) +} + +fn validate_relative_source(id: &str, source: &str) -> Result<(), String> { + let path = Path::new(source); + if source.is_empty() + || path.is_absolute() + || path + .components() + .any(|component| !matches!(component, Component::Normal(_))) + || path.extension().and_then(|extension| extension.to_str()) != Some("rs") + { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {id} source must be a normalized relative Rust path" + )); + } + Ok(()) +} + +fn validate_adr(workspace_root: &Path, exception: &ApiException) -> Result<(), String> { + let path = Path::new(&exception.adr); + if path.is_absolute() + || !exception.adr.starts_with("docs/decisions/") + || path + .components() + .any(|component| !matches!(component, Component::Normal(_))) + || path.extension().and_then(|extension| extension.to_str()) != Some("md") + { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} ADR must be a normalized docs/decisions/*.md path", + exception.id + )); + } + let full = workspace_root.join(path); + let raw = fs::read_to_string(&full).map_err(|error| { + format!( + "{API_BOUNDARIES_RELATIVE} exception {} ADR {} is not readable: {error}", + exception.id, + full.display() + ) + })?; + if !raw.contains(&exception.id) { + return Err(format!( + "{API_BOUNDARIES_RELATIVE} exception {} ADR {} must cite the exception id", + exception.id, exception.adr + )); + } + Ok(()) +} + +fn package_set_mismatch(expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> String { + let missing = expected + .difference(actual) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = actual + .difference(expected) + .cloned() + .collect::<Vec<_>>() + .join(", "); + format!( + "{API_BOUNDARIES_RELATIVE} package catalog mismatch; missing: {missing}; extra: {extra}" + ) +} + +fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--locked", "--no-deps"]) + .current_dir(workspace_root) + .output() + .map_err(|error| format!("run cargo metadata: {error}"))?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed while checking public API boundaries: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + serde_json::from_slice(&output.stdout) + .map_err(|error| format!("parse cargo metadata for public API boundaries: {error}")) +} + +fn scan_workspace( + _workspace_root: &Path, + policy: &ApiBoundaryPolicy, + metadata: &CargoMetadata, +) -> Result<Vec<ApiFinding>, String> { + let workspace_members = metadata + .workspace_members + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let package_policy = policy + .package + .iter() + .map(|package| (package.name.as_str(), package)) + .collect::<BTreeMap<_, _>>(); + let forbidden = policy + .forbidden_public_paths + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let mut findings = BTreeSet::new(); + + for package in &metadata.packages { + if !workspace_members.contains(package.id.as_str()) { + continue; + } + let Some(package_policy) = package_policy.get(package.name.as_str()).copied() else { + continue; + }; + let manifest_path = Path::new(&package.manifest_path); + let package_root = manifest_path.parent().ok_or_else(|| { + format!( + "package {} manifest has no parent: {}", + package.name, package.manifest_path + ) + })?; + let allowed = package_policy + .allowed_public_paths + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let library_targets = package + .targets + .iter() + .filter(|target| { + target + .kind + .iter() + .any(|kind| kind == "lib" || kind == "proc-macro") + }) + .collect::<Vec<_>>(); + if library_targets.len() != 1 { + return Err(format!( + "public package {} must expose exactly one library target for API leakage analysis", + package.name + )); + } + let source_path = Path::new(&library_targets[0].src_path); + let source_module_directory = module_directory(source_path)?; + let mut units = BTreeMap::new(); + collect_module( + package_root, + Vec::new(), + None, + true, + true, + source_path, + &source_module_directory, + None, + &mut units, + )?; + findings.extend(scan_package_units( + &package.name, + &units, + &forbidden, + &allowed, + )?); + } + + Ok(findings.into_iter().collect()) +} + +#[allow(clippy::too_many_arguments)] +fn collect_module( + package_root: &Path, + module: Vec<String>, + parent: Option<Vec<String>>, + declared_public: bool, + initially_effective: bool, + source_path: &Path, + module_directory: &Path, + inline_items: Option<Vec<Item>>, + units: &mut BTreeMap<Vec<String>, ModuleUnit>, +) -> Result<(), String> { + if units.contains_key(&module) { + return Ok(()); + } + let (items, source_relative) = if let Some(items) = inline_items { + let relative = source_path + .strip_prefix(package_root) + .map_err(|_| { + format!( + "module source {} escapes package {}", + source_path.display(), + package_root.display() + ) + })? + .to_string_lossy() + .replace('\\', "/"); + (items, relative) + } else { + let raw = fs::read_to_string(source_path) + .map_err(|error| format!("read {}: {error}", source_path.display()))?; + let file = syn::parse_file(&raw) + .map_err(|error| format!("parse {}: {error}", source_path.display()))?; + let relative = source_path + .strip_prefix(package_root) + .map_err(|_| { + format!( + "module source {} escapes package {}", + source_path.display(), + package_root.display() + ) + })? + .to_string_lossy() + .replace('\\', "/"); + (file.items, relative) + }; + + let children = items + .iter() + .filter_map(|item| match item { + Item::Mod(item_mod) => Some(item_mod.clone()), + _ => None, + }) + .collect::<Vec<_>>(); + units.insert( + module.clone(), + ModuleUnit { + module: module.clone(), + parent, + declared_public, + initially_effective, + source_relative, + items, + }, + ); + + for child in children { + let mut child_module = module.clone(); + child_module.push(child.ident.to_string()); + let child_public = is_public(&child.vis); + let child_effective = initially_effective && child_public; + if let Some((_, inline)) = child.content { + collect_module( + package_root, + child_module, + Some(module.clone()), + child_public, + child_effective, + source_path, + &module_directory.join(child.ident.to_string()), + Some(inline), + units, + )?; + } else { + let child_path = module_source_path(source_path, module_directory, &child)?; + collect_module( + package_root, + child_module, + Some(module.clone()), + child_public, + child_effective, + &child_path, + &module_directory.join(child.ident.to_string()), + None, + units, + )?; + } + } + Ok(()) +} + +fn module_directory(source_path: &Path) -> Result<PathBuf, String> { + let parent = source_path + .parent() + .ok_or_else(|| format!("module source has no parent: {}", source_path.display()))?; + let file_name = source_path.file_name().and_then(|name| name.to_str()); + if matches!(file_name, Some("lib.rs" | "main.rs" | "mod.rs")) { + Ok(parent.to_path_buf()) + } else { + Ok(parent.join( + source_path + .file_stem() + .ok_or_else(|| format!("module source has no stem: {}", source_path.display()))?, + )) + } +} + +fn module_source_path( + source_path: &Path, + module_directory: &Path, + item_mod: &syn::ItemMod, +) -> Result<PathBuf, String> { + if let Some(path) = path_attribute(item_mod) { + return Ok(source_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(path)); + } + let name = item_mod.ident.to_string(); + let flat = module_directory.join(format!("{name}.rs")); + let nested = module_directory.join(&name).join("mod.rs"); + match (flat.is_file(), nested.is_file()) { + (true, false) => Ok(flat), + (false, true) => Ok(nested), + (true, true) => Err(format!( + "module {} is ambiguous between {} and {}", + item_mod.ident, + flat.display(), + nested.display() + )), + (false, false) => Err(format!( + "module {} source is missing under {}", + item_mod.ident, + module_directory.display() + )), + } +} + +fn path_attribute(item_mod: &syn::ItemMod) -> Option<String> { + item_mod.attrs.iter().find_map(|attribute| { + if !attribute.path().is_ident("path") { + return None; + } + let Meta::NameValue(value) = &attribute.meta else { + return None; + }; + let Expr::Lit(ExprLit { + lit: Lit::Str(path), + .. + }) = &value.value + else { + return None; + }; + Some(path.value()) + }) +} + +fn scan_package_units( + package: &str, + units: &BTreeMap<Vec<String>, ModuleUnit>, + forbidden: &BTreeSet<&str>, + allowed: &BTreeSet<&str>, +) -> Result<Vec<ApiFinding>, String> { + let mut effective = units + .values() + .filter(|unit| unit.initially_effective) + .map(|unit| unit.module.clone()) + .collect::<BTreeSet<_>>(); + let mut exported_items = BTreeSet::new(); + let mut changed = true; + while changed { + changed = propagate_public_modules(units, &mut effective); + let scopes = effective.iter().cloned().collect::<Vec<_>>(); + for scope in scopes { + let unit = units + .get(&scope) + .ok_or_else(|| format!("missing module unit {}", module_label(&scope)))?; + for item_use in unit.items.iter().filter_map(|item| match item { + Item::Use(item_use) if is_public(&item_use.vis) => Some(item_use), + _ => None, + }) { + for (segments, glob) in flatten_use_tree(&item_use.tree) { + match resolve_internal_export(&scope, &segments, glob, units) { + InternalExport::Module(module) => { + changed |= effective.insert(module); + } + InternalExport::Item(module, item) => { + changed |= exported_items.insert((module, item)); + } + InternalExport::External => {} + } + } + } + } + } + propagate_public_modules(units, &mut effective); + + let mut findings = BTreeSet::new(); + for unit in units.values() { + let imports = import_map(&unit.items); + let unit_effective = effective.contains(&unit.module); + let explicitly_exported = exported_items + .iter() + .filter(|(module, _)| module == &unit.module) + .map(|(_, item)| item.as_str()) + .collect::<BTreeSet<_>>(); + + for item in &unit.items { + if let Item::Use(item_use) = item { + if unit_effective && is_public(&item_use.vis) { + scan_public_use( + package, + unit, + item_use, + &imports, + forbidden, + allowed, + &mut findings, + ); + } + continue; + } + if let Item::Impl(item_impl) = item { + let Some(self_name) = impl_self_name(&item_impl.self_ty) else { + continue; + }; + if unit_effective || explicitly_exported.contains(self_name.as_str()) { + scan_impl( + package, + unit, + item_impl, + &self_name, + &imports, + forbidden, + allowed, + &mut findings, + ); + } + continue; + } + let Some(name) = item_name(item) else { + continue; + }; + if (unit_effective && item_is_public(item)) + || explicitly_exported.contains(name.as_str()) + { + scan_item( + package, + unit, + item, + &imports, + forbidden, + allowed, + &mut findings, + ); + } + } + } + Ok(findings.into_iter().collect()) +} + +fn propagate_public_modules( + units: &BTreeMap<Vec<String>, ModuleUnit>, + effective: &mut BTreeSet<Vec<String>>, +) -> bool { + let mut changed = false; + loop { + let before = effective.len(); + for unit in units.values() { + if unit.declared_public + && unit + .parent + .as_ref() + .is_some_and(|parent| effective.contains(parent)) + { + effective.insert(unit.module.clone()); + } + } + if effective.len() == before { + break; + } + changed = true; + } + changed +} + +fn flatten_use_tree(tree: &UseTree) -> Vec<(Vec<String>, bool)> { + fn visit(tree: &UseTree, prefix: &mut Vec<String>, output: &mut Vec<(Vec<String>, bool)>) { + match tree { + UseTree::Path(path) => { + prefix.push(path.ident.to_string()); + visit(&path.tree, prefix, output); + prefix.pop(); + } + UseTree::Name(name) => { + if name.ident == "self" { + output.push((prefix.clone(), false)); + } else { + let mut path = prefix.clone(); + path.push(name.ident.to_string()); + output.push((path, false)); + } + } + UseTree::Rename(rename) => { + let mut path = prefix.clone(); + path.push(rename.ident.to_string()); + output.push((path, false)); + } + UseTree::Glob(_) => output.push((prefix.clone(), true)), + UseTree::Group(group) => { + for item in &group.items { + visit(item, prefix, output); + } + } + } + } + + let mut output = Vec::new(); + visit(tree, &mut Vec::new(), &mut output); + output +} + +fn resolve_internal_export( + current: &[String], + segments: &[String], + glob: bool, + units: &BTreeMap<Vec<String>, ModuleUnit>, +) -> InternalExport { + if segments.is_empty() { + return InternalExport::External; + } + let candidates = internal_candidates(current, segments); + for candidate in candidates { + if glob && units.contains_key(&candidate) { + return InternalExport::Module(candidate); + } + if units.contains_key(&candidate) { + return InternalExport::Module(candidate); + } + if let Some((item, module)) = candidate.split_last() { + let module = module.to_vec(); + if units.contains_key(&module) { + return InternalExport::Item(module, item.clone()); + } + } + } + InternalExport::External +} + +fn internal_candidates(current: &[String], segments: &[String]) -> Vec<Vec<String>> { + let mut candidates = Vec::new(); + match segments.first().map(String::as_str) { + Some("crate") => candidates.push(segments[1..].to_vec()), + Some("self") => { + let mut path = current.to_vec(); + path.extend_from_slice(&segments[1..]); + candidates.push(path); + } + Some("super") => { + let mut base = current.to_vec(); + let mut index = 0; + while segments.get(index).map(String::as_str) == Some("super") { + base.pop(); + index += 1; + } + base.extend_from_slice(&segments[index..]); + candidates.push(base); + } + _ => { + candidates.push(segments.to_vec()); + let mut local = current.to_vec(); + local.extend_from_slice(segments); + if !candidates.contains(&local) { + candidates.push(local); + } + } + } + candidates +} + +fn import_map(items: &[Item]) -> BTreeMap<String, Vec<String>> { + fn visit(tree: &UseTree, prefix: &mut Vec<String>, output: &mut BTreeMap<String, Vec<String>>) { + match tree { + UseTree::Path(path) => { + prefix.push(path.ident.to_string()); + visit(&path.tree, prefix, output); + prefix.pop(); + } + UseTree::Name(name) => { + if name.ident == "self" { + if let Some(local) = prefix.last() { + output.insert(local.clone(), prefix.clone()); + } + } else { + let mut path = prefix.clone(); + path.push(name.ident.to_string()); + output.insert(name.ident.to_string(), path); + } + } + UseTree::Rename(rename) => { + let mut path = prefix.clone(); + path.push(rename.ident.to_string()); + output.insert(rename.rename.to_string(), path); + } + UseTree::Glob(_) => {} + UseTree::Group(group) => { + for item in &group.items { + visit(item, prefix, output); + } + } + } + } + + let mut output = BTreeMap::new(); + for item_use in items.iter().filter_map(|item| match item { + Item::Use(item_use) => Some(item_use), + _ => None, + }) { + visit(&item_use.tree, &mut Vec::new(), &mut output); + } + output +} + +#[allow(clippy::too_many_arguments)] +fn scan_item( + package: &str, + unit: &ModuleUnit, + item: &Item, + imports: &BTreeMap<String, Vec<String>>, + forbidden: &BTreeSet<&str>, + allowed: &BTreeSet<&str>, + findings: &mut BTreeSet<ApiFinding>, +) { + let name = item_name(item).unwrap_or_else(|| "<item>".to_owned()); + let label = qualified_item(&unit.module, &name); + let mut visitor = + LeakageVisitor::new(package, unit, label, imports, forbidden, allowed, findings); + match item { + Item::Const(item) => visitor.visit_type(&item.ty), + Item::Enum(item) => { + visitor.visit_generics(&item.generics); + for variant in &item.variants { + visit_fields(&mut visitor, &variant.fields, true); + } + } + Item::Fn(item) => visitor.visit_signature(&item.sig), + Item::ForeignMod(item) => { + for foreign in &item.items { + if let ForeignItem::Fn(function) = foreign { + visitor.visit_signature(&function.sig); + } + } + } + Item::Static(item) => visitor.visit_type(&item.ty), + Item::Struct(item) => { + visitor.visit_generics(&item.generics); + visit_fields(&mut visitor, &item.fields, false); + } + Item::Trait(item) => { + visitor.visit_generics(&item.generics); + for bound in &item.supertraits { + visitor.visit_type_param_bound(bound); + } + for trait_item in &item.items { + match trait_item { + TraitItem::Const(item) => visitor.visit_type(&item.ty), + TraitItem::Fn(item) => visitor.visit_signature(&item.sig), + TraitItem::Type(item) => { + visitor.visit_generics(&item.generics); + for bound in &item.bounds { + visitor.visit_type_param_bound(bound); + } + if let Some((_, ty)) = &item.default { + visitor.visit_type(ty); + } + } + _ => {} + } + } + } + Item::TraitAlias(item) => { + visitor.visit_generics(&item.generics); + for bound in &item.bounds { + visitor.visit_type_param_bound(bound); + } + } + Item::Type(item) => { + visitor.visit_generics(&item.generics); + visitor.visit_type(&item.ty); + } + Item::Union(item) => { + visitor.visit_generics(&item.generics); + for field in &item.fields.named { + if is_public(&field.vis) { + visitor.visit_type(&field.ty); + } + } + } + _ => {} + } +} + +fn visit_fields(visitor: &mut LeakageVisitor<'_>, fields: &Fields, all_visible: bool) { + for field in fields { + if all_visible || is_public(&field.vis) { + visitor.visit_type(&field.ty); + } + } +} + +#[allow(clippy::too_many_arguments)] +fn scan_impl( + package: &str, + unit: &ModuleUnit, + item_impl: &syn::ItemImpl, + self_name: &str, + imports: &BTreeMap<String, Vec<String>>, + forbidden: &BTreeSet<&str>, + allowed: &BTreeSet<&str>, + findings: &mut BTreeSet<ApiFinding>, +) { + let trait_impl = item_impl.trait_.is_some(); + for item in &item_impl.items { + let (name, is_exposed) = match item { + ImplItem::Const(item) => (item.ident.to_string(), trait_impl || is_public(&item.vis)), + ImplItem::Fn(item) => ( + item.sig.ident.to_string(), + trait_impl || is_public(&item.vis), + ), + ImplItem::Type(item) => (item.ident.to_string(), trait_impl || is_public(&item.vis)), + _ => continue, + }; + if !is_exposed { + continue; + } + let label = format!("{}::{name}", qualified_item(&unit.module, self_name)); + let mut visitor = + LeakageVisitor::new(package, unit, label, imports, forbidden, allowed, findings); + if let Some((_, trait_path, _)) = &item_impl.trait_ { + visitor.visit_path(trait_path); + } + match item { + ImplItem::Const(item) => visitor.visit_type(&item.ty), + ImplItem::Fn(item) => visitor.visit_signature(&item.sig), + ImplItem::Type(item) => { + visitor.visit_generics(&item.generics); + visitor.visit_type(&item.ty); + } + _ => {} + } + } +} + +#[allow(clippy::too_many_arguments)] +fn scan_public_use( + package: &str, + unit: &ModuleUnit, + item_use: &syn::ItemUse, + imports: &BTreeMap<String, Vec<String>>, + forbidden: &BTreeSet<&str>, + allowed: &BTreeSet<&str>, + findings: &mut BTreeSet<ApiFinding>, +) { + for (segments, _) in flatten_use_tree(&item_use.tree) { + if segments.is_empty() { + continue; + } + let label = format!("{}::pub use", module_label(&unit.module)); + record_path( + package, unit, &label, &segments, imports, forbidden, allowed, findings, + ); + } +} + +struct LeakageVisitor<'a> { + package: &'a str, + unit: &'a ModuleUnit, + item: String, + imports: &'a BTreeMap<String, Vec<String>>, + forbidden: &'a BTreeSet<&'a str>, + allowed: &'a BTreeSet<&'a str>, + findings: &'a mut BTreeSet<ApiFinding>, +} + +impl<'a> LeakageVisitor<'a> { + #[allow(clippy::too_many_arguments)] + fn new( + package: &'a str, + unit: &'a ModuleUnit, + item: String, + imports: &'a BTreeMap<String, Vec<String>>, + forbidden: &'a BTreeSet<&'a str>, + allowed: &'a BTreeSet<&'a str>, + findings: &'a mut BTreeSet<ApiFinding>, + ) -> Self { + Self { + package, + unit, + item, + imports, + forbidden, + allowed, + findings, + } + } +} + +impl<'ast> Visit<'ast> for LeakageVisitor<'_> { + fn visit_path(&mut self, path: &'ast SynPath) { + let segments = path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::<Vec<_>>(); + record_path( + self.package, + self.unit, + &self.item, + &segments, + self.imports, + self.forbidden, + self.allowed, + self.findings, + ); + syn::visit::visit_path(self, path); + } +} + +#[allow(clippy::too_many_arguments)] +fn record_path( + package: &str, + unit: &ModuleUnit, + item: &str, + segments: &[String], + imports: &BTreeMap<String, Vec<String>>, + forbidden: &BTreeSet<&str>, + allowed: &BTreeSet<&str>, + findings: &mut BTreeSet<ApiFinding>, +) { + if segments.is_empty() { + return; + } + let resolved = if let Some(imported) = imports.get(&segments[0]) { + let mut resolved = imported.clone(); + resolved.extend_from_slice(&segments[1..]); + resolved + } else { + segments.to_vec() + }; + let observed = resolved.join("::"); + for forbidden_path in forbidden { + if allowed.contains(forbidden_path) + || !(observed == *forbidden_path + || observed + .strip_prefix(forbidden_path) + .is_some_and(|suffix| suffix.starts_with("::"))) + { + continue; + } + findings.insert(ApiFinding { + package: package.to_owned(), + source: unit.source_relative.clone(), + item: item.to_owned(), + forbidden_path: (*forbidden_path).to_owned(), + observed_path: observed.clone(), + }); + } +} + +fn exception_matches(exceptions: &[ApiException], finding: &ApiFinding) -> bool { + exceptions.iter().any(|exception| { + exception.package == finding.package + && exception.source == finding.source + && exception.forbidden_path == finding.forbidden_path + && exception.items.binary_search(&finding.item).is_ok() + && exception + .observed_paths + .binary_search(&finding.observed_path) + .is_ok() + }) +} + +fn format_finding(finding: &ApiFinding) -> String { + format!( + "package={} source={} item={} forbidden_path={} observed_path={}", + finding.package, + finding.source, + finding.item, + finding.forbidden_path, + finding.observed_path + ) +} + +fn item_is_public(item: &Item) -> bool { + match item { + Item::Const(item) => is_public(&item.vis), + Item::Enum(item) => is_public(&item.vis), + Item::Fn(item) => is_public(&item.vis), + Item::ForeignMod(_) => true, + Item::Static(item) => is_public(&item.vis), + Item::Struct(item) => is_public(&item.vis), + Item::Trait(item) => is_public(&item.vis), + Item::TraitAlias(item) => is_public(&item.vis), + Item::Type(item) => is_public(&item.vis), + Item::Union(item) => is_public(&item.vis), + _ => false, + } +} + +fn item_name(item: &Item) -> Option<String> { + match item { + Item::Const(item) => Some(item.ident.to_string()), + Item::Enum(item) => Some(item.ident.to_string()), + Item::Fn(item) => Some(item.sig.ident.to_string()), + Item::Static(item) => Some(item.ident.to_string()), + Item::Struct(item) => Some(item.ident.to_string()), + Item::Trait(item) => Some(item.ident.to_string()), + Item::TraitAlias(item) => Some(item.ident.to_string()), + Item::Type(item) => Some(item.ident.to_string()), + Item::Union(item) => Some(item.ident.to_string()), + _ => None, + } +} + +fn impl_self_name(self_ty: &Type) -> Option<String> { + let Type::Path(path) = self_ty else { + return None; + }; + path.path + .segments + .last() + .map(|segment| segment.ident.to_string()) +} + +fn is_public(visibility: &Visibility) -> bool { + matches!(visibility, Visibility::Public(_)) +} + +fn qualified_item(module: &[String], item: &str) -> String { + if module.is_empty() { + item.to_owned() + } else { + format!("{}::{item}", module.join("::")) + } +} + +fn module_label(module: &[String]) -> String { + if module.is_empty() { + "crate".to_owned() + } else { + module.join("::") + } +} + +#[cfg(test)] +mod tests { + use super::{ + ApiBoundaryPolicy, ApiException, CargoMetadata, exception_matches, scan_workspace, + validate_policy, + }; + use serde::Deserialize; + use std::{collections::BTreeSet, fs}; + + const POLICY: &str = include_str!("../../../../contracts/releases/api_boundaries.toml"); + const ARCHITECTURE: &str = + include_str!("../../../../docs/specs/radroots_crates_release_v1.toml"); + const GENERIC_SQLX: &str = include_str!("../../tests/fixtures/api-leakage/generic-sqlx.rs"); + const GENERIC_RENAMED_TOKIO: &str = + include_str!("../../tests/fixtures/api-leakage/generic-renamed-tokio.rs"); + const ALLOWED_ADAPTER: &str = + include_str!("../../tests/fixtures/api-leakage/allowed-concrete-adapter.rs"); + const PRIVATE_IMPLEMENTATION: &str = + include_str!("../../tests/fixtures/api-leakage/private-implementation.rs"); + const ADR_EXCEPTION: &str = include_str!("../../tests/fixtures/api-leakage/adr-exception.rs"); + + #[derive(Deserialize)] + struct ArchitectureCatalog { + package: Vec<ArchitecturePackage>, + } + + #[derive(Deserialize)] + struct ArchitecturePackage { + name: String, + } + + fn policy() -> ApiBoundaryPolicy { + toml::from_str(POLICY).expect("API boundary policy") + } + + fn expected_packages() -> BTreeSet<String> { + toml::from_str::<ArchitectureCatalog>(ARCHITECTURE) + .expect("architecture") + .package + .into_iter() + .map(|package| package.name) + .collect() + } + + fn fixture_metadata(root: &std::path::Path, package: &str, source: &str) -> CargoMetadata { + let package_root = root.join(package); + fs::create_dir_all(package_root.join("src")).expect("create fixture source"); + fs::write( + package_root.join("Cargo.toml"), + "[package]\nname='fixture'\n", + ) + .expect("write fixture manifest"); + fs::write(package_root.join("src/lib.rs"), source).expect("write fixture source"); + let id = format!("fixture#{package}@0.1.0"); + CargoMetadata { + workspace_members: vec![id.clone()], + packages: vec![super::CargoPackage { + id, + name: package.to_owned(), + manifest_path: package_root + .join("Cargo.toml") + .to_string_lossy() + .into_owned(), + targets: vec![super::CargoTarget { + kind: vec!["lib".to_owned()], + src_path: package_root + .join("src/lib.rs") + .to_string_lossy() + .into_owned(), + }], + }], + } + } + + fn scan_fixture(package: &str, source: &str) -> Vec<super::ApiFinding> { + let root = tempfile::TempDir::new().expect("fixture root"); + scan_workspace( + root.path(), + &policy(), + &fixture_metadata(root.path(), package, source), + ) + .expect("scan fixture") + } + + fn write_baseline_adr(root: &std::path::Path) { + fs::create_dir_all(root.join("docs/decisions")).expect("baseline ADR directory"); + fs::write( + root.join("docs/decisions/0001-public-api-leakage-migration-baseline.md"), + "RCRV1-API-001 RCRV1-API-002 RCRV1-API-003 RCRV1-API-004 RCRV1-API-005 RCRV1-API-006 RCRV1-API-007 RCRV1-API-008\n", + ) + .expect("baseline ADR"); + } + + #[test] + fn policy_covers_exact_architecture_catalog() { + let root = tempfile::TempDir::new().expect("policy root"); + write_baseline_adr(root.path()); + validate_policy(root.path(), &policy(), &expected_packages()) + .expect("policy without exceptions"); + } + + #[test] + fn generic_sqlx_public_field_is_forbidden() { + let findings = scan_fixture("radroots_storage", GENERIC_SQLX); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].item, "StorageHandle"); + assert_eq!(findings[0].forbidden_path, "sqlx"); + assert_eq!(findings[0].observed_path, "sqlx::SqlitePool"); + } + + #[test] + fn renamed_tokio_return_type_is_resolved() { + let findings = scan_fixture("radroots_sync", GENERIC_RENAMED_TOKIO); + assert_eq!(findings.len(), 1); + assert_eq!(findings[0].item, "executor"); + assert_eq!(findings[0].forbidden_path, "tokio"); + assert_eq!(findings[0].observed_path, "tokio::runtime::Handle"); + } + + #[test] + fn specified_nostr_adapter_may_expose_protocol_types() { + let findings = scan_fixture("radroots_nostr", ALLOWED_ADAPTER); + assert!(findings.is_empty()); + } + + #[test] + fn private_implementation_types_are_not_public_api() { + let findings = scan_fixture("radroots_sdk", PRIVATE_IMPLEMENTATION); + assert!(findings.is_empty()); + } + + #[test] + fn exact_item_exception_requires_resolving_adr() { + let root = tempfile::TempDir::new().expect("exception root"); + write_baseline_adr(root.path()); + fs::create_dir_all(root.path().join("docs/decisions")).expect("ADR directory"); + fs::write( + root.path().join("docs/decisions/0001-test.md"), + "# Test\n\nRCRV1-API-999\n", + ) + .expect("ADR"); + let mut policy = policy(); + policy.exception.push(ApiException { + id: "RCRV1-API-999".to_owned(), + package: "radroots_sdk".to_owned(), + source: "src/lib.rs".to_owned(), + forbidden_path: "reqwest".to_owned(), + items: vec!["temporary_client".to_owned()], + observed_paths: vec!["reqwest::Client".to_owned()], + adr: "docs/decisions/0001-test.md".to_owned(), + removal_step: 226, + rationale: "test-only exception".to_owned(), + }); + validate_policy(root.path(), &policy, &expected_packages()).expect("resolving ADR"); + let finding = scan_workspace( + root.path(), + &policy, + &fixture_metadata(root.path(), "radroots_sdk", ADR_EXCEPTION), + ) + .expect("scan exception fixture") + .pop() + .expect("finding"); + assert!(exception_matches(&policy.exception, &finding)); + + fs::remove_file(root.path().join("docs/decisions/0001-test.md")).expect("remove ADR"); + let error = validate_policy(root.path(), &policy, &expected_packages()) + .expect_err("missing ADR must fail"); + assert!(error.contains("is not readable")); + } +} diff --git a/tools/sdk_xtask_import/src/architecture/dependency_boundary.rs b/tools/sdk_xtask_import/src/architecture/dependency_boundary.rs @@ -0,0 +1,718 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, + process::Command, +}; + +use serde::Deserialize; + +const PACKAGE_TIERS_RELATIVE: &str = "contracts/releases/package_tiers.toml"; +const SPEC_ID: &str = "radroots.crates.release.v1"; +const DIRECTION: &str = "same_or_lower"; +const POLICY_SCHEMA_VERSION: u16 = 1; +const CURRENT_STEP: u16 = 24; +const EXPECTED_TIERS: &[(&str, u8)] = &[ + ("foundation", 0), + ("domain", 1), + ("spi", 2), + ("adapter", 3), + ("orchestration", 4), + ("sdk", 5), + ("facade", 6), +]; +const EXPECTED_DEPENDENCY_KINDS: &[&str] = &["build", "dev", "normal"]; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct DependencyBoundaryPolicy { + schema_version: u16, + spec_id: String, + direction: String, + enforced_dependency_kinds: Vec<String>, + tier: Vec<PackageTier>, + #[serde(default)] + temporary_exception: Vec<TemporaryException>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PackageTier { + id: String, + rank: u8, + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct TemporaryException { + owner: String, + dependency: String, + kind: String, + target: String, + features: Vec<String>, + uses_default_features: bool, + removal_step: u16, + rationale: String, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec<CargoPackage>, + resolve: Option<CargoResolve>, +} + +#[derive(Debug, Deserialize)] +struct CargoPackage { + id: String, + name: String, + #[serde(default)] + dependencies: Vec<CargoDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependency { + name: String, + #[serde(default)] + kind: Option<String>, + #[serde(default)] + features: Vec<String>, + #[serde(default)] + target: Option<String>, + #[serde(default = "default_true")] + uses_default_features: bool, +} + +#[derive(Debug, Deserialize)] +struct CargoResolve { + nodes: Vec<CargoNode>, +} + +#[derive(Debug, Deserialize)] +struct CargoNode { + id: String, + #[serde(default)] + deps: Vec<CargoNodeDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoNodeDependency { + name: String, + pkg: String, + #[serde(default)] + dep_kinds: Vec<CargoDependencyKind>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependencyKind { + #[serde(default)] + kind: Option<String>, + #[serde(default)] + target: Option<String>, +} + +#[derive(Clone, Copy)] +struct PackagePlacement<'a> { + tier: &'a str, + rank: u8, +} + +fn default_true() -> bool { + true +} + +pub(super) fn validate_policy_catalog( + workspace_root: &Path, + expected_packages: &BTreeSet<String>, +) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + validate_policy(&policy, expected_packages).map(|_| ()) +} + +pub(super) fn validate_resolved_boundaries(workspace_root: &Path) -> Result<(), String> { + let policy = load_policy(workspace_root)?; + let expected_packages = policy + .tier + .iter() + .flat_map(|tier| tier.packages.iter().cloned()) + .collect::<BTreeSet<_>>(); + let placements = validate_policy(&policy, &expected_packages)?; + let metadata = load_metadata(workspace_root)?; + validate_metadata(&policy, &placements, &metadata) +} + +fn load_policy(workspace_root: &Path) -> Result<DependencyBoundaryPolicy, String> { + let path = workspace_root.join(PACKAGE_TIERS_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display())) +} + +fn validate_policy<'a>( + policy: &'a DependencyBoundaryPolicy, + expected_packages: &BTreeSet<String>, +) -> Result<BTreeMap<&'a str, PackagePlacement<'a>>, String> { + if policy.schema_version != POLICY_SCHEMA_VERSION { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} schema_version must be {POLICY_SCHEMA_VERSION}" + )); + } + if policy.spec_id != SPEC_ID { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} spec_id must be {SPEC_ID}" + )); + } + if policy.direction != DIRECTION { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} direction must be {DIRECTION}" + )); + } + + let actual_kinds = unique_strings( + "enforced_dependency_kinds", + &policy.enforced_dependency_kinds, + )?; + let expected_kinds = EXPECTED_DEPENDENCY_KINDS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if actual_kinds != expected_kinds { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} enforced_dependency_kinds must be build, dev, normal" + )); + } + + let actual_tiers = policy + .tier + .iter() + .map(|tier| (tier.id.as_str(), tier.rank)) + .collect::<Vec<_>>(); + if actual_tiers != EXPECTED_TIERS { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} tiers must match the approved ordered architecture" + )); + } + + let mut placements = BTreeMap::new(); + for tier in &policy.tier { + if tier.packages.is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} tier {} must allocate at least one package", + tier.id + )); + } + for package in &tier.packages { + if placements + .insert( + package.as_str(), + PackagePlacement { + tier: &tier.id, + rank: tier.rank, + }, + ) + .is_some() + { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} package {package} is allocated more than once" + )); + } + } + } + + let actual_packages = placements + .keys() + .map(|package| (*package).to_owned()) + .collect::<BTreeSet<_>>(); + if &actual_packages != expected_packages { + return Err(package_set_mismatch(expected_packages, &actual_packages)); + } + + let mut exception_keys = BTreeSet::new(); + for exception in &policy.temporary_exception { + let owner = placements.get(exception.owner.as_str()).ok_or_else(|| { + format!( + "{PACKAGE_TIERS_RELATIVE} exception owner {} is not an approved package", + exception.owner + ) + })?; + let dependency = placements + .get(exception.dependency.as_str()) + .ok_or_else(|| { + format!( + "{PACKAGE_TIERS_RELATIVE} exception dependency {} is not an approved package", + exception.dependency + ) + })?; + if dependency.rank <= owner.rank { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} does not describe an upward edge", + exception.owner, exception.dependency + )); + } + if !expected_kinds.contains(exception.kind.as_str()) { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} uses unknown dependency kind {}", + exception.owner, exception.dependency, exception.kind + )); + } + if exception.target.trim().is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must name its exact target", + exception.owner, exception.dependency + )); + } + let normalized_features = normalized_features(&exception.features); + if normalized_features.len() != exception.features.len() + || normalized_features.iter().ne(exception.features.iter()) + { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} features must be sorted and unique", + exception.owner, exception.dependency + )); + } + if exception.removal_step <= CURRENT_STEP { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} expired at Step {}", + exception.owner, exception.dependency, exception.removal_step + )); + } + if exception.rationale.trim().is_empty() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} exception {} -> {} must include a rationale", + exception.owner, exception.dependency + )); + } + let key = ( + exception.owner.as_str(), + exception.dependency.as_str(), + exception.kind.as_str(), + exception.target.as_str(), + exception.features.clone(), + exception.uses_default_features, + ); + if !exception_keys.insert(key) { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} contains a duplicate exception for {} -> {}", + exception.owner, exception.dependency + )); + } + } + + Ok(placements) +} + +fn unique_strings<'a>(label: &str, values: &'a [String]) -> Result<BTreeSet<&'a str>, String> { + let unique = values.iter().map(String::as_str).collect::<BTreeSet<_>>(); + if unique.len() != values.len() { + return Err(format!( + "{PACKAGE_TIERS_RELATIVE} {label} must not contain duplicates" + )); + } + Ok(unique) +} + +fn package_set_mismatch(expected: &BTreeSet<String>, actual: &BTreeSet<String>) -> String { + let missing = expected + .difference(actual) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = actual + .difference(expected) + .cloned() + .collect::<Vec<_>>() + .join(", "); + format!("{PACKAGE_TIERS_RELATIVE} package catalog mismatch; missing: {missing}; extra: {extra}") +} + +fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> { + let output = Command::new("cargo") + .args([ + "metadata", + "--format-version", + "1", + "--locked", + "--all-features", + ]) + .current_dir(workspace_root) + .output() + .map_err(|error| format!("run cargo metadata: {error}"))?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed while checking dependency boundaries: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + serde_json::from_slice(&output.stdout) + .map_err(|error| format!("parse cargo metadata for dependency boundaries: {error}")) +} + +fn validate_metadata( + policy: &DependencyBoundaryPolicy, + placements: &BTreeMap<&str, PackagePlacement<'_>>, + metadata: &CargoMetadata, +) -> Result<(), String> { + let resolve = metadata + .resolve + .as_ref() + .ok_or_else(|| "cargo metadata did not include a resolved dependency graph".to_owned())?; + let mut packages_by_id = BTreeMap::new(); + for package in &metadata.packages { + if packages_by_id + .insert(package.id.as_str(), package) + .is_some() + { + return Err(format!( + "cargo metadata contains duplicate package id {}", + package.id + )); + } + } + + let mut nodes_by_id = BTreeMap::new(); + for node in &resolve.nodes { + if nodes_by_id.insert(node.id.as_str(), node).is_some() { + return Err(format!( + "cargo metadata contains duplicate resolve node {}", + node.id + )); + } + } + + let enforced_kinds = policy + .enforced_dependency_kinds + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let mut violations = Vec::new(); + + for node in &resolve.nodes { + let Some(owner_package) = packages_by_id.get(node.id.as_str()).copied() else { + return Err(format!( + "cargo metadata resolve node {} has no package record", + node.id + )); + }; + let Some(owner_placement) = placements.get(owner_package.name.as_str()).copied() else { + continue; + }; + + for resolved_dependency in &node.deps { + let dependency_package = packages_by_id + .get(resolved_dependency.pkg.as_str()) + .copied() + .ok_or_else(|| { + format!( + "cargo metadata resolved dependency {} has no package record", + resolved_dependency.pkg + ) + })?; + let Some(dependency_placement) = + placements.get(dependency_package.name.as_str()).copied() + else { + continue; + }; + if resolved_dependency.dep_kinds.is_empty() { + return Err(format!( + "cargo metadata edge {} -> {} has no dependency kind", + owner_package.name, dependency_package.name + )); + } + + for dependency_kind in &resolved_dependency.dep_kinds { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + if !enforced_kinds.contains(kind) { + continue; + } + let declaration = + resolve_declaration(owner_package, dependency_package, dependency_kind)?; + if dependency_placement.rank <= owner_placement.rank { + continue; + } + if exception_matches( + &policy.temporary_exception, + owner_package, + dependency_package, + dependency_kind, + declaration, + ) { + continue; + } + violations.push(format_violation( + owner_package, + owner_placement, + dependency_package, + dependency_placement, + resolved_dependency, + dependency_kind, + declaration, + )); + } + } + } + + if violations.is_empty() { + Ok(()) + } else { + violations.sort(); + Err(format!( + "forbidden package dependency direction(s):\n{}", + violations.join("\n") + )) + } +} + +fn resolve_declaration<'a>( + owner: &'a CargoPackage, + dependency: &CargoPackage, + dependency_kind: &CargoDependencyKind, +) -> Result<&'a CargoDependency, String> { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + let candidates = owner + .dependencies + .iter() + .filter(|candidate| { + candidate.name == dependency.name + && normalized_kind(candidate.kind.as_deref()) == kind + && candidate.target == dependency_kind.target + }) + .collect::<Vec<_>>(); + match candidates.as_slice() { + [declaration] => Ok(*declaration), + [] => Err(format!( + "cargo metadata edge {} -> {} kind={kind} target={} has no matching package dependency declaration", + owner.name, + dependency.name, + target_label(dependency_kind.target.as_deref()) + )), + _ => Err(format!( + "cargo metadata edge {} -> {} kind={kind} target={} has ambiguous package dependency declarations", + owner.name, + dependency.name, + target_label(dependency_kind.target.as_deref()) + )), + } +} + +fn exception_matches( + exceptions: &[TemporaryException], + owner: &CargoPackage, + dependency: &CargoPackage, + dependency_kind: &CargoDependencyKind, + declaration: &CargoDependency, +) -> bool { + let kind = normalized_kind(dependency_kind.kind.as_deref()); + let target = target_label(dependency_kind.target.as_deref()); + let features = normalized_features(&declaration.features); + exceptions.iter().any(|exception| { + exception.owner == owner.name + && exception.dependency == dependency.name + && exception.kind == kind + && exception.target == target + && exception.features == features + && exception.uses_default_features == declaration.uses_default_features + }) +} + +fn format_violation( + owner: &CargoPackage, + owner_placement: PackagePlacement<'_>, + dependency: &CargoPackage, + dependency_placement: PackagePlacement<'_>, + resolved_dependency: &CargoNodeDependency, + dependency_kind: &CargoDependencyKind, + declaration: &CargoDependency, +) -> String { + let features = normalized_features(&declaration.features); + format!( + "{} (tier={} rank={}) -> {} (tier={} rank={}); owner_id={}; dependency_id={}; alias={}; kind={}; target={}; features=[{}]; default_features={}", + owner.name, + owner_placement.tier, + owner_placement.rank, + dependency.name, + dependency_placement.tier, + dependency_placement.rank, + owner.id, + dependency.id, + resolved_dependency.name, + normalized_kind(dependency_kind.kind.as_deref()), + target_label(dependency_kind.target.as_deref()), + features.join(","), + declaration.uses_default_features, + ) +} + +fn normalized_kind(kind: Option<&str>) -> &str { + kind.unwrap_or("normal") +} + +fn target_label(target: Option<&str>) -> &str { + target.unwrap_or("all") +} + +fn normalized_features(features: &[String]) -> Vec<String> { + features + .iter() + .cloned() + .collect::<BTreeSet<_>>() + .into_iter() + .collect() +} + +#[cfg(test)] +mod tests { + use super::{ + CargoMetadata, DependencyBoundaryPolicy, exception_matches, validate_metadata, + validate_policy, + }; + use serde::Deserialize; + use std::collections::BTreeSet; + + const POLICY: &str = include_str!("../../../../contracts/releases/package_tiers.toml"); + const ARCHITECTURE: &str = + include_str!("../../../../docs/specs/radroots_crates_release_v1.toml"); + const DOMAIN_TO_STORAGE: &str = + include_str!("../../tests/fixtures/dependency-boundaries/domain-to-storage.json"); + const SPI_TO_ADAPTER: &str = + include_str!("../../tests/fixtures/dependency-boundaries/spi-to-adapter.json"); + const SERVICE_TO_SDK: &str = + include_str!("../../tests/fixtures/dependency-boundaries/service-to-sdk.json"); + const VALID_DOWNWARD: &str = + include_str!("../../tests/fixtures/dependency-boundaries/valid-downward.json"); + + #[derive(Deserialize)] + struct ArchitectureCatalog { + package: Vec<ArchitecturePackage>, + } + + #[derive(Deserialize)] + struct ArchitecturePackage { + name: String, + } + + fn policy() -> DependencyBoundaryPolicy { + toml::from_str(POLICY).expect("package tier policy") + } + + fn metadata(raw: &str) -> CargoMetadata { + serde_json::from_str(raw).expect("Cargo metadata fixture") + } + + fn placements<'a>( + policy: &'a DependencyBoundaryPolicy, + ) -> std::collections::BTreeMap<&'a str, super::PackagePlacement<'a>> { + let architecture = + toml::from_str::<ArchitectureCatalog>(ARCHITECTURE).expect("architecture catalog"); + let expected = architecture + .package + .into_iter() + .map(|package| package.name) + .collect::<BTreeSet<_>>(); + validate_policy(policy, &expected).expect("valid package tier policy") + } + + #[test] + fn policy_covers_the_exact_architecture_catalog() { + let policy = policy(); + let placements = placements(&policy); + assert_eq!(placements.len(), 19); + } + + #[test] + fn domain_to_storage_reports_resolved_alias_kind_target_and_features() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(DOMAIN_TO_STORAGE)) + .expect_err("domain must not depend on storage"); + assert!(error.contains("radroots_event (tier=domain rank=1)")); + assert!(error.contains("radroots_storage (tier=spi rank=2)")); + assert!(error.contains("alias=storage_alias")); + assert!(error.contains("kind=normal")); + assert!(error.contains("target=cfg(unix)")); + assert!(error.contains("features=[memory]")); + } + + #[test] + fn spi_to_adapter_reports_build_and_target_specific_edge() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(SPI_TO_ADAPTER)) + .expect_err("SPI must not depend on adapter"); + assert!(error.contains("radroots_transport (tier=spi rank=2)")); + assert!(error.contains("radroots_nostr (tier=adapter rank=3)")); + assert!(error.contains("alias=protocol_adapter")); + assert!(error.contains("kind=build")); + assert!(error.contains("target=cfg(target_arch = \"wasm32\")")); + assert!(error.contains("features=[events]")); + } + + #[test] + fn service_orchestration_to_sdk_is_forbidden() { + let policy = policy(); + let error = validate_metadata(&policy, &placements(&policy), &metadata(SERVICE_TO_SDK)) + .expect_err("service orchestration must not depend on SDK"); + assert!(error.contains("radroots_sync (tier=orchestration rank=4)")); + assert!(error.contains("radroots_sdk (tier=sdk rank=5)")); + assert!(error.contains("alias=client_engine")); + } + + #[test] + fn valid_downward_dev_target_edge_passes() { + let policy = policy(); + validate_metadata(&policy, &placements(&policy), &metadata(VALID_DOWNWARD)) + .expect("adapter dev dependency on SPI points downward"); + } + + #[test] + fn migration_exception_is_exact_about_features() { + let policy = policy(); + let owner = super::CargoPackage { + id: "owner".to_owned(), + name: "radroots_trade".to_owned(), + dependencies: Vec::new(), + }; + let dependency = super::CargoPackage { + id: "dependency".to_owned(), + name: "radroots_nostr".to_owned(), + dependencies: Vec::new(), + }; + let kind = super::CargoDependencyKind { + kind: Some("dev".to_owned()), + target: None, + }; + let exact = super::CargoDependency { + name: "radroots_nostr".to_owned(), + kind: Some("dev".to_owned()), + features: vec!["std".to_owned(), "events".to_owned()], + target: None, + uses_default_features: false, + }; + assert!(exception_matches( + &policy.temporary_exception, + &owner, + &dependency, + &kind, + &exact + )); + + let broadened = super::CargoDependency { + features: vec!["client".to_owned(), "events".to_owned(), "std".to_owned()], + ..exact + }; + assert!(!exception_matches( + &policy.temporary_exception, + &owner, + &dependency, + &kind, + &broadened + )); + } + + #[test] + fn metadata_without_resolve_graph_fails_closed() { + let policy = policy(); + let mut fixture = metadata(VALID_DOWNWARD); + fixture.resolve = None; + let error = validate_metadata(&policy, &placements(&policy), &fixture) + .expect_err("missing resolved graph must fail"); + assert!(error.contains("did not include a resolved dependency graph")); + } +} diff --git a/tools/sdk_xtask_import/src/bindings.rs b/tools/sdk_xtask_import/src/bindings.rs @@ -0,0 +1,281 @@ +use std::{ + collections::BTreeMap, + env, fs, + path::{Path, PathBuf}, + process::Command, +}; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::fs::{workspace_root, write_if_changed}; + +const FFI_PACKAGE: &str = "radroots_sdk_ffi"; +const FFI_VERSION: &str = "0.1.0-alpha"; +const BINDGEN_VERSION: &str = "0.29.5"; + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +struct SourceLock { + schema_version: u16, + source_package: String, + source_version: String, + source_sha256: String, + generator: String, + generator_version: String, + language: String, + outputs: BTreeMap<String, String>, +} + +pub fn generate(args: &[String]) -> Result<(), String> { + match args { + [language] if language == "swift" => generate_language("swift"), + [language] if language == "kotlin" => generate_language("kotlin"), + _ => Err("usage: cargo xtask generate bindings <swift|kotlin>".to_owned()), + } +} + +fn generate_language(language: &'static str) -> Result<(), String> { + let root = workspace_root()?; + build_bindgen(&root)?; + let target_dir = env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .ok_or_else(|| "CARGO_TARGET_DIR must be set by extbuild for FFI generation".to_owned())?; + let library = target_dir.join("debug").join(format!( + "{}radroots_sdk_ffi{}", + env::consts::DLL_PREFIX, + env::consts::DLL_SUFFIX + )); + if !library.is_file() { + return Err(format!("missing built FFI library: {}", library.display())); + } + let bindgen = target_dir + .join("debug") + .join(format!("radroots-sdk-bindgen{}", env::consts::EXE_SUFFIX)); + if !bindgen.is_file() { + return Err(format!( + "missing FFI bindgen executable: {}", + bindgen.display() + )); + } + let temporary = tempfile::tempdir() + .map_err(|error| format!("failed to create binding output directory: {error}"))?; + let status = Command::new(&bindgen) + .args(["generate", "--library"]) + .arg(&library) + .args(["--crate", FFI_PACKAGE, "--language", language, "--out-dir"]) + .arg(temporary.path()) + .current_dir(&root) + .status() + .map_err(|error| format!("failed to start {}: {error}", bindgen.display()))?; + if !status.success() { + return Err(format!( + "{language} binding generation failed with {status}" + )); + } + install_outputs(&root, language, temporary.path())?; + println!("generated {language} bindings for {FFI_PACKAGE}"); + Ok(()) +} + +fn build_bindgen(root: &Path) -> Result<(), String> { + let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); + let status = Command::new(cargo) + .args([ + "build", + "--locked", + "-p", + FFI_PACKAGE, + "--lib", + "--bin", + "radroots-sdk-bindgen", + ]) + .current_dir(root) + .status() + .map_err(|error| format!("failed to start Cargo for FFI generation: {error}"))?; + if !status.success() { + return Err(format!( + "FFI library and bindgen build failed with {status}" + )); + } + Ok(()) +} + +fn install_outputs(root: &Path, language: &'static str, temporary: &Path) -> Result<(), String> { + let output_dir = root.join("generated").join(language); + fs::create_dir_all(&output_dir) + .map_err(|error| format!("failed to create {}: {error}", output_dir.display()))?; + let mut generated = Vec::new(); + collect_files(temporary, &mut generated)?; + generated.sort(); + let mut outputs = BTreeMap::new(); + for path in generated { + let name = path + .strip_prefix(temporary) + .map_err(|error| format!("failed to relativize {}: {error}", path.display()))? + .to_str() + .ok_or_else(|| format!("non-UTF-8 generated binding path: {}", path.display()))? + .replace('\\', "/"); + let contents = fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let destination = output_dir.join(&name); + let text = String::from_utf8(contents) + .map_err(|error| format!("generated text binding is not UTF-8: {error}"))?; + let text = normalize_generated_text(&text); + outputs.insert( + name.clone(), + format!("{:x}", Sha256::digest(text.as_bytes())), + ); + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + write_if_changed(&destination, &text)?; + } + if outputs.is_empty() { + return Err(format!("{language} binding generator emitted no files")); + } + let lock = SourceLock { + schema_version: 1, + source_package: FFI_PACKAGE.to_owned(), + source_version: FFI_VERSION.to_owned(), + source_sha256: ffi_source_sha256(root)?, + generator: "uniffi".to_owned(), + generator_version: BINDGEN_VERSION.to_owned(), + language: language.to_owned(), + outputs, + }; + let mut rendered = serde_json::to_string_pretty(&lock) + .map_err(|error| format!("failed to render {language} source lock: {error}"))?; + rendered.push('\n'); + write_if_changed(&output_dir.join("source.lock"), &rendered).map(|_| ()) +} + +fn normalize_generated_text(contents: &str) -> String { + let mut normalized = contents + .lines() + .map(str::trim_end) + .collect::<Vec<_>>() + .join("\n"); + while normalized.ends_with('\n') { + normalized.pop(); + } + normalized.push('\n'); + normalized +} + +pub fn check(root: &Path) -> Result<(), String> { + check_language( + root, + "swift", + &[ + "radroots_sdk.swift", + "radroots_sdkFFI.h", + "radroots_sdkFFI.modulemap", + ], + )?; + check_language(root, "kotlin", &["uniffi/radroots_sdk/radroots_sdk.kt"])?; + check_kotlin_schema_inventory(root) +} + +fn check_kotlin_schema_inventory(root: &Path) -> Result<(), String> { + let path = root.join("generated/kotlin/uniffi/radroots_sdk/radroots_sdk.kt"); + let source = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + for required in [ + "class MobileClient", + "data class CapabilityStatus", + "enum class CapabilityAvailability", + "enum class CapabilityMaturity", + "sealed class Exception", + ] { + if !source.contains(required) { + return Err(format!( + "generated Kotlin schema inventory is missing {required}: {}", + path.display() + )); + } + } + for forbidden in ["Keychain", "BackgroundTask", "Presentation"] { + if source.contains(forbidden) { + return Err(format!( + "generated Kotlin bindings contain host-owned {forbidden} API: {}", + path.display() + )); + } + } + Ok(()) +} + +fn check_language(root: &Path, language: &'static str, expected: &[&str]) -> Result<(), String> { + let output_dir = root.join("generated").join(language); + let lock_path = output_dir.join("source.lock"); + let raw = fs::read_to_string(&lock_path) + .map_err(|error| format!("failed to read {}: {error}", lock_path.display()))?; + let actual: SourceLock = serde_json::from_str(&raw) + .map_err(|error| format!("failed to parse {}: {error}", lock_path.display()))?; + let mut outputs = BTreeMap::new(); + for name in expected { + let path = output_dir.join(name); + let contents = fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + outputs.insert( + (*name).to_owned(), + format!("{:x}", Sha256::digest(contents)), + ); + } + let expected_lock = SourceLock { + schema_version: 1, + source_package: FFI_PACKAGE.to_owned(), + source_version: FFI_VERSION.to_owned(), + source_sha256: ffi_source_sha256(root)?, + generator: "uniffi".to_owned(), + generator_version: BINDGEN_VERSION.to_owned(), + language: language.to_owned(), + outputs, + }; + if actual != expected_lock { + return Err(format!( + "stale generated {language} bindings: {}", + lock_path.display() + )); + } + Ok(()) +} + +fn ffi_source_sha256(root: &Path) -> Result<String, String> { + let crate_dir = root.join("crates/ffi"); + let mut paths = vec![crate_dir.join("Cargo.toml"), crate_dir.join("README.md")]; + collect_files(&crate_dir.join("src"), &mut paths)?; + paths.sort(); + let mut hasher = Sha256::new(); + for path in paths { + let relative = path + .strip_prefix(root) + .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?; + hasher.update(relative.to_string_lossy().as_bytes()); + hasher.update([0]); + hasher.update( + fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?, + ); + hasher.update([0]); + } + Ok(format!("{:x}", hasher.finalize())) +} + +fn collect_files(dir: &Path, paths: &mut Vec<PathBuf>) -> Result<(), String> { + let mut entries = fs::read_dir(dir) + .map_err(|error| format!("failed to read {}: {error}", dir.display()))? + .collect::<Result<Vec<_>, _>>() + .map_err(|error| format!("failed to enumerate {}: {error}", dir.display()))?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + collect_files(&path, paths)?; + } else if path.is_file() { + paths.push(path); + } + } + Ok(()) +} diff --git a/tools/sdk_xtask_import/src/check.rs b/tools/sdk_xtask_import/src/check.rs @@ -0,0 +1,3710 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + io::Read, + path::{Path, PathBuf}, + process::Command, +}; + +use flate2::read::GzDecoder; +use serde::Deserialize; +use tar::Archive; + +use crate::{ + contracts::validate_sdk_contracts, + fs::workspace_root, + output::{PackageOutput, package_outputs}, + package_matrix::{ + FORBIDDEN_PACKAGE_NAMES, WasmPackageSpec, package_specs, validate_package_matrix, + wasm_package_specs, + }, + package_metadata::{PACKAGE_FILES, check_package_distribution_metadata, package_description}, + ts::generated_header, + wasm::validate_provenance_manifest, + wasm_declarations::declaration_files, +}; + +const PACKAGE_VERSION: &str = "0.1.0"; +const PACKAGE_LICENSE: &str = "MIT OR Apache-2.0"; +const PACKAGE_HOMEPAGE: &str = "https://radroots.org"; +const PACKAGE_REPOSITORY_URL: &str = "git+https://github.com/radrootslabs/sdk.git"; +const PUBLISH_ACCESS: &str = "public"; +const ALLOWED_RETICULUM_PREVIEW_PACKAGE: &str = "radroots_transport_reticulum"; +const RADROOTS_FACADE_MODULES: [&str; 11] = [ + "client", + "event", + "farm", + "identity", + "knowledge", + "listing", + "signing", + "storage", + "sync", + "trade", + "transport", +]; +const RADROOTS_FACADE_FEATURES: [&str; 9] = [ + "client", + "default", + "full", + "geonames", + "knowledge", + "native", + "nip46", + "nostr", + "radrootsd", +]; + +#[derive(Debug, Deserialize)] +struct PnpmPackEntry { + filename: String, + files: Vec<PnpmPackFile>, +} + +#[derive(Debug, Deserialize)] +struct PnpmPackFile { + path: String, +} + +#[derive(Debug)] +struct PackedPackage { + package_name: String, + tarball_path: PathBuf, + files: Vec<NpmPackFile>, +} + +#[derive(Debug)] +struct NpmPackFile { + path: String, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec<CargoMetadataPackage>, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadataPackage { + name: String, +} + +#[derive(Debug, Deserialize)] +struct PublicationPolicyFile { + publication: PublicationPolicy, + workspace_classification: WorkspacePublicationClassification, + #[serde(default)] + publish_order: PublishOrder, +} + +#[derive(Debug, Default, Deserialize)] +struct PublishOrder { + crates: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct PublicationPolicy { + frozen: bool, + registry: String, + final_enablement_step: u16, + spec_id: String, + approved_packages: Vec<String>, + local_packages: Vec<String>, + external_packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct WorkspacePublicationClassification { + private: Vec<String>, + build_codegen: Vec<String>, + test_support: Vec<String>, + preview: Vec<String>, + retired: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct CratesReleaseArchitecture { + spec_id: String, + package_count: usize, + repositories: CratesReleaseRepositories, + package: Vec<CratesReleasePackage>, +} + +#[derive(Debug, Deserialize)] +struct CratesReleaseRepositories { + lib: CratesReleaseRepository, + sdk: CratesReleaseRepository, +} + +#[derive(Debug, Deserialize)] +struct CratesReleaseRepository { + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct CratesReleasePackage { + name: String, +} + +pub fn check() -> Result<(), String> { + validate_package_matrix()?; + let root = workspace_root()?; + crate::architecture::validate(&root)?; + check_publication_policy(&root)?; + check_public_rust_package_metadata(&root)?; + check_radroots_facade_conformance(&root)?; + validate_sdk_contracts(&root)?; + check_sdk_feature_matrix(&root)?; + check_forbidden_packages(&root)?; + check_binding_crate_sources(&root)?; + check_package_source_metadata(&root)?; + check_generated_outputs(&root)?; + check_package_build_artifacts(&root)?; + check_npm_pack_payloads(&root)?; + crate::bindings::check(&root)?; + Ok(()) +} + +pub fn architecture_ci(root: &Path) -> Result<(), String> { + validate_package_matrix()?; + check_publication_policy(root)?; + check_public_rust_package_metadata(root)?; + check_radroots_facade_conformance(root)?; + validate_sdk_contracts(root)?; + check_sdk_feature_matrix(root)?; + check_forbidden_packages(root)?; + check_binding_crate_sources(root)?; + check_package_source_metadata(root)?; + check_generated_outputs(root)?; + crate::bindings::check(root) +} + +fn check_radroots_facade_conformance(root: &Path) -> Result<(), String> { + let manifest_path = root.join("crates/radroots/Cargo.toml"); + let manifest_raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("failed to read {}: {error}", manifest_path.display()))?; + let manifest = manifest_raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", manifest_path.display()))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| "crates/radroots/Cargo.toml must define [package]".to_owned())?; + for (field, expected) in [ + ("name", "radroots"), + ( + "description", + "Curated ordinary-user Rust facade for Radroots", + ), + ("readme", "README.md"), + ] { + if package.get(field).and_then(toml::Value::as_str) != Some(expected) { + return Err(format!( + "crates/radroots/Cargo.toml package.{field} must be {expected}" + )); + } + } + let publish = package + .get("publish") + .and_then(toml::Value::as_array) + .ok_or_else(|| "radroots facade must set publish = [\"crates-io\"]".to_owned())?; + if publish.len() != 1 || publish[0].as_str() != Some("crates-io") { + return Err("radroots facade must set publish = [\"crates-io\"]".to_owned()); + } + for field in [ + "version", + "edition", + "rust-version", + "license", + "repository", + "homepage", + ] { + let inherited = package + .get(field) + .and_then(toml::Value::as_table) + .and_then(|value| value.get("workspace")) + .and_then(toml::Value::as_bool); + if inherited != Some(true) { + return Err(format!( + "crates/radroots/Cargo.toml package.{field} must inherit workspace metadata" + )); + } + } + let library = manifest + .get("lib") + .and_then(toml::Value::as_table) + .ok_or_else(|| "crates/radroots/Cargo.toml must define [lib]".to_owned())?; + if library.get("name").and_then(toml::Value::as_str) != Some("radroots") + || library.get("path").and_then(toml::Value::as_str) != Some("src/lib.rs") + { + return Err("radroots facade [lib] must use name radroots and path src/lib.rs".to_owned()); + } + let dependencies = manifest + .get("dependencies") + .and_then(toml::Value::as_table) + .ok_or_else(|| "radroots facade must define [dependencies]".to_owned())?; + let actual_dependencies = dependencies + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let expected_dependencies = [ + "radroots_core", + "radroots_event", + "radroots_identity", + "radroots_sdk", + "radroots_trade", + "radroots_transport", + ] + .into_iter() + .collect::<BTreeSet<_>>(); + if actual_dependencies != expected_dependencies { + return Err(format!( + "radroots facade dependencies must be exactly {}; found {}", + expected_dependencies + .into_iter() + .collect::<Vec<_>>() + .join(", "), + actual_dependencies + .into_iter() + .collect::<Vec<_>>() + .join(", ") + )); + } + for (name, dependency) in dependencies { + if dependency + .as_table() + .and_then(|value| value.get("workspace")) + .and_then(toml::Value::as_bool) + != Some(true) + { + return Err(format!( + "radroots facade dependency {name} must inherit the governed workspace entry" + )); + } + } + for dependency_table in ["dev-dependencies", "build-dependencies"] { + if manifest + .get(dependency_table) + .and_then(toml::Value::as_table) + .is_some_and(|dependencies| !dependencies.is_empty()) + { + return Err(format!( + "radroots facade must not define [{dependency_table}]" + )); + } + } + + let features = manifest + .get("features") + .and_then(toml::Value::as_table) + .ok_or_else(|| "crates/radroots/Cargo.toml must define [features]".to_owned())?; + let actual_features = features.keys().map(String::as_str).collect::<BTreeSet<_>>(); + let expected_features = RADROOTS_FACADE_FEATURES + .into_iter() + .collect::<BTreeSet<_>>(); + if actual_features != expected_features { + return Err(format!( + "radroots facade features must be exactly {}; found {}", + expected_features.into_iter().collect::<Vec<_>>().join(", "), + actual_features.into_iter().collect::<Vec<_>>().join(", ") + )); + } + let defaults = features + .get("default") + .and_then(toml::Value::as_array) + .ok_or_else(|| "radroots facade default feature must be an array".to_owned())?; + if defaults.len() != 1 || defaults[0].as_str() != Some("client") { + return Err("radroots facade default feature must be exactly client".to_owned()); + } + for (name, expected) in [ + ("client", &["radroots_sdk/default"][..]), + ("native", &["client", "radroots_sdk/native"]), + ("nostr", &["client", "radroots_sdk/nostr"]), + ("nip46", &["nostr", "radroots_sdk/nip46"]), + ("radrootsd", &["client", "radroots_sdk/radrootsd"]), + ("geonames", &["client", "radroots_sdk/geonames"]), + ("knowledge", &["client", "radroots_sdk/knowledge"]), + ("full", &["radroots_sdk/full"]), + ] { + let actual = features + .get(name) + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("radroots facade feature {name} must be an array"))? + .iter() + .filter_map(toml::Value::as_str) + .collect::<Vec<_>>(); + if actual != expected { + return Err(format!( + "radroots facade feature {name} must forward exactly {}", + expected.join(", ") + )); + } + } + + if package.get("autoexamples").and_then(toml::Value::as_bool) != Some(false) { + return Err("radroots facade must disable implicit example discovery".to_owned()); + } + let examples = manifest + .get("example") + .and_then(toml::Value::as_array) + .ok_or_else(|| "radroots facade must declare its ordinary example".to_owned())?; + if examples.len() != 1 + || examples[0].get("name").and_then(toml::Value::as_str) != Some("ordinary_memory") + || examples[0].get("path").and_then(toml::Value::as_str) + != Some("examples/ordinary_memory.rs") + { + return Err("radroots facade must declare only the ordinary_memory example".to_owned()); + } + + let source_path = root.join("crates/radroots/src/lib.rs"); + let source = fs::read_to_string(&source_path) + .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; + let actual_modules = source + .lines() + .filter_map(|line| { + line.trim() + .strip_prefix("pub mod ") + .and_then(|module| module.strip_suffix(';')) + }) + .collect::<BTreeSet<_>>(); + let expected_modules = RADROOTS_FACADE_MODULES.into_iter().collect::<BTreeSet<_>>(); + if actual_modules != expected_modules { + return Err(format!( + "radroots facade modules must be exactly {}; found {}", + expected_modules.into_iter().collect::<Vec<_>>().join(", "), + actual_modules.into_iter().collect::<Vec<_>>().join(", ") + )); + } + if source.contains("pub mod sdk") || source.contains("pub use radroots_sdk::*") { + return Err( + "radroots facade must not expose an sdk namespace or broad SDK re-export".to_owned(), + ); + } + if !source.contains("pub use radroots_sdk::{Client, ClientBuilder, Error, Result};") { + return Err( + "radroots facade must export exactly the four approved SDK root entry points" + .to_owned(), + ); + } + for module in RADROOTS_FACADE_MODULES { + let module_path = root + .join("crates/radroots/src") + .join(format!("{module}.rs")); + if !module_path.is_file() { + return Err(format!( + "radroots facade module file {module}.rs is missing" + )); + } + let module_source = fs::read_to_string(&module_path) + .map_err(|error| format!("failed to read {}: {error}", module_path.display()))?; + if module_source.contains("pub trait ") || module_source.contains("::*") { + return Err(format!( + "radroots facade module {module} must not define traits or wildcard reexports" + )); + } + } + let readme_path = root.join("crates/radroots/README.md"); + let readme = fs::read_to_string(&readme_path) + .map_err(|error| format!("failed to read {}: {error}", readme_path.display()))?; + if !readme.contains("```compile_fail\nuse radroots::sdk;\n```") { + return Err("radroots facade README must compile-fail the SDK namespace".to_owned()); + } + Ok(()) +} + +fn check_publication_policy(root: &Path) -> Result<(), String> { + let policy_path = root.join("contracts/releases/publication.toml"); + let raw = fs::read_to_string(&policy_path) + .map_err(|error| format!("failed to read {}: {error}", policy_path.display()))?; + let policy_file = toml::from_str::<PublicationPolicyFile>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", policy_path.display()))?; + let publish_order = policy_file.publish_order.crates.clone(); + let policy = policy_file.publication; + if policy.registry != "crates-io" { + return Err("publication.registry must be crates-io".to_owned()); + } + if policy.final_enablement_step != 305 { + return Err("publication.final_enablement_step must be 305".to_owned()); + } + let architecture_path = root.join("docs/specs/radroots_crates_release_v1.toml"); + let architecture_raw = fs::read_to_string(&architecture_path) + .map_err(|error| format!("failed to read {}: {error}", architecture_path.display()))?; + let architecture = toml::from_str::<CratesReleaseArchitecture>(&architecture_raw) + .map_err(|error| format!("failed to parse {}: {error}", architecture_path.display()))?; + let expected_approved = policy_set( + architecture + .package + .iter() + .map(|package| package.name.clone()), + "architecture.package.name", + )?; + if architecture.package_count != 19 || expected_approved.len() != architecture.package_count { + return Err(format!( + "release architecture must define exactly 19 unique packages, found package_count {} and {} unique package records", + architecture.package_count, + expected_approved.len() + )); + } + if policy.spec_id != architecture.spec_id || policy.spec_id != "radroots.crates.release.v1" { + return Err(format!( + "publication.spec_id {} must match architecture id {}", + policy.spec_id, architecture.spec_id + )); + } + let approved = policy_set(policy.approved_packages, "publication.approved_packages")?; + let local = policy_set(policy.local_packages, "publication.local_packages")?; + let external = policy_set(policy.external_packages, "publication.external_packages")?; + let expected_local = policy_set( + architecture.repositories.sdk.packages, + "architecture.repositories.sdk.packages", + )?; + let expected_external = policy_set( + architecture.repositories.lib.packages, + "architecture.repositories.lib.packages", + )?; + for (field, actual, expected) in [ + ( + "publication.approved_packages", + &approved, + &expected_approved, + ), + ("publication.local_packages", &local, &expected_local), + ( + "publication.external_packages", + &external, + &expected_external, + ), + ] { + if actual != expected { + let missing = expected + .difference(actual) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = actual + .difference(expected) + .cloned() + .collect::<Vec<_>>() + .join(", "); + return Err(format!( + "{field} is missing approved packages: {missing}; {field} has unapproved packages: {extra}" + )); + } + } + if !local.is_disjoint(&external) { + return Err("local and external approved package ownership must not overlap".to_owned()); + } + let mut owned = local.clone(); + owned.extend(external.iter().cloned()); + if owned != approved { + return Err( + "local and external package ownership must partition approved packages".to_owned(), + ); + } + if policy.frozen { + if !publish_order.is_empty() { + return Err( + "publish_order.crates must remain empty while publication is frozen".to_owned(), + ); + } + } else { + let publish_order_set = policy_set(publish_order.iter().cloned(), "publish_order.crates")?; + if publish_order_set != local { + return Err( + "publish_order.crates must contain exactly the approved local packages when publication is enabled" + .to_owned(), + ); + } + if publish_order != ["radroots_sdk", "radroots"] { + return Err("publish_order.crates must place radroots_sdk before radroots".to_owned()); + } + } + + let mut workspace_packages = BTreeSet::new(); + for path in sdk_manifest_paths(root)?.into_iter().skip(1) { + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} must define [package]", path.display()))?; + let name = package + .get("name") + .and_then(toml::Value::as_str) + .ok_or_else(|| format!("{} package.name must be a string", path.display()))?; + if !workspace_packages.insert(name.to_owned()) { + return Err(format!("duplicate workspace package name {name}")); + } + let publish = package.get("publish"); + if policy.frozen { + if publish.and_then(toml::Value::as_bool) != Some(false) { + return Err(format!( + "publication freeze requires workspace package {name} to set publish = false" + )); + } + } else if approved.contains(name) { + let registries = publish.and_then(toml::Value::as_array).ok_or_else(|| { + format!("approved package {name} must set publish = [\"crates-io\"]") + })?; + if registries.len() != 1 || registries[0].as_str() != Some("crates-io") { + return Err(format!( + "approved package {name} must set publish = [\"crates-io\"]" + )); + } + } else if publish.and_then(toml::Value::as_bool) != Some(false) { + return Err(format!( + "non-approved workspace package {name} must set publish = false" + )); + } + } + let external_in_workspace = external + .intersection(&workspace_packages) + .cloned() + .collect::<Vec<_>>(); + if !external_in_workspace.is_empty() { + return Err(format!( + "externally owned approved packages must not be workspace members: {}", + external_in_workspace.join(", ") + )); + } + let classification = policy_file.workspace_classification; + let private = policy_set(classification.private, "workspace_classification.private")?; + let build_codegen = policy_set( + classification.build_codegen, + "workspace_classification.build_codegen", + )?; + let test_support = policy_set( + classification.test_support, + "workspace_classification.test_support", + )?; + let preview = policy_set(classification.preview, "workspace_classification.preview")?; + let retired = policy_set(classification.retired, "workspace_classification.retired")?; + let classes = [ + ("private", &private), + ("build-codegen", &build_codegen), + ("test-support", &test_support), + ("preview", &preview), + ("retired", &retired), + ]; + for index in 0..classes.len() { + for other_index in (index + 1)..classes.len() { + let overlap = classes[index] + .1 + .intersection(classes[other_index].1) + .cloned() + .collect::<Vec<_>>(); + if !overlap.is_empty() { + return Err(format!( + "workspace classification overlap is not allowed between {} and {}: {}", + classes[index].0, + classes[other_index].0, + overlap.join(", ") + )); + } + } + } + let mut classified = BTreeSet::new(); + for (_, entries) in classes { + classified.extend(entries.iter().cloned()); + } + let local_workspace = local + .intersection(&workspace_packages) + .cloned() + .collect::<BTreeSet<_>>(); + let public_classification_overlap = classified + .intersection(&local_workspace) + .cloned() + .collect::<Vec<_>>(); + if !public_classification_overlap.is_empty() { + return Err(format!( + "approved local packages must not be classified as private workspace packages: {}", + public_classification_overlap.join(", ") + )); + } + let mut accounted = classified; + accounted.extend(local_workspace.iter().cloned()); + if accounted != workspace_packages { + let missing = workspace_packages + .difference(&accounted) + .cloned() + .collect::<Vec<_>>() + .join(", "); + let extra = accounted + .difference(&workspace_packages) + .cloned() + .collect::<Vec<_>>() + .join(", "); + return Err(format!( + "workspace classification is missing packages: {missing}; workspace classification has unknown packages: {extra}" + )); + } + if !policy.frozen && !local.is_subset(&workspace_packages) { + let missing = local + .difference(&workspace_packages) + .cloned() + .collect::<Vec<_>>() + .join(", "); + return Err(format!( + "publication enablement is missing approved workspace packages: {missing}" + )); + } + Ok(()) +} + +fn policy_set( + values: impl IntoIterator<Item = String>, + field: &str, +) -> Result<BTreeSet<String>, String> { + let mut result = BTreeSet::new(); + for value in values { + if value.trim().is_empty() { + return Err(format!("{field} must not contain empty package names")); + } + if !result.insert(value.clone()) { + return Err(format!( + "{field} must not contain duplicate package {value}" + )); + } + } + Ok(result) +} + +fn package_field_configured(package: &toml::value::Table, field: &str) -> bool { + match package.get(field) { + Some(toml::Value::String(value)) => !value.trim().is_empty(), + Some(toml::Value::Array(values)) => !values.is_empty(), + Some(toml::Value::Table(value)) => value + .get("workspace") + .and_then(toml::Value::as_bool) + .is_some_and(|configured| configured), + _ => false, + } +} + +fn package_string_array<'a>( + package: &'a toml::value::Table, + package_name: &str, + field: &str, +) -> Result<Vec<&'a str>, String> { + let values = package + .get(field) + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("public package {package_name} must define package.{field}"))?; + if values.is_empty() { + return Err(format!( + "public package {package_name} package.{field} must not be empty" + )); + } + let mut resolved = Vec::with_capacity(values.len()); + let mut unique = BTreeSet::new(); + for value in values { + let value = value.as_str().ok_or_else(|| { + format!("public package {package_name} package.{field} entries must be strings") + })?; + if value.trim().is_empty() || !unique.insert(value) { + return Err(format!( + "public package {package_name} package.{field} entries must be non-empty and unique" + )); + } + resolved.push(value); + } + Ok(resolved) +} + +fn check_public_rust_package_metadata(root: &Path) -> Result<(), String> { + let policy_path = root.join("contracts/releases/publication.toml"); + let policy_raw = fs::read_to_string(&policy_path) + .map_err(|error| format!("failed to read {}: {error}", policy_path.display()))?; + let policy_file = toml::from_str::<PublicationPolicyFile>(&policy_raw) + .map_err(|error| format!("failed to parse {}: {error}", policy_path.display()))?; + let local = policy_set( + policy_file.publication.local_packages, + "publication.local_packages", + )?; + + let mut manifests = BTreeMap::new(); + for manifest_path in sdk_manifest_paths(root)?.into_iter().skip(1) { + let raw = fs::read_to_string(&manifest_path) + .map_err(|error| format!("failed to read {}: {error}", manifest_path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", manifest_path.display()))?; + let package_name = manifest + .get("package") + .and_then(toml::Value::as_table) + .and_then(|package| package.get("name")) + .and_then(toml::Value::as_str) + .ok_or_else(|| format!("{} must define package.name", manifest_path.display()))? + .to_owned(); + if manifests + .insert(package_name.clone(), (manifest_path, manifest)) + .is_some() + { + return Err(format!("duplicate workspace package name {package_name}")); + } + } + + for package_name in local { + let (manifest_path, manifest) = manifests + .get(&package_name) + .ok_or_else(|| format!("public package {package_name} has no workspace manifest"))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .expect("workspace package manifest has a package table"); + for field in [ + "authors", + "version", + "edition", + "rust-version", + "license", + "repository", + "homepage", + "documentation", + "readme", + "description", + ] { + if !package_field_configured(package, field) { + return Err(format!( + "public package {package_name} must configure package.{field}" + )); + } + } + let expected_documentation = format!("https://docs.rs/{package_name}"); + if package.get("documentation").and_then(toml::Value::as_str) + != Some(expected_documentation.as_str()) + { + return Err(format!( + "public package {package_name} package.documentation must be {expected_documentation}" + )); + } + if package.get("license-file").is_some() { + return Err(format!( + "public package {package_name} must use the workspace SPDX license expression" + )); + } + + let keywords = package_string_array(package, &package_name, "keywords")?; + let categories = package_string_array(package, &package_name, "categories")?; + if keywords.len() > 5 || categories.len() > 5 { + return Err(format!( + "public package {package_name} keywords and categories must respect crates.io limits" + )); + } + let include = package_string_array(package, &package_name, "include")?; + for required in [ + "src/**", + "tests/**", + "README.md", + "LICENSE-APACHE", + "LICENSE-MIT", + ] { + if !include.contains(&required) { + return Err(format!( + "public package {package_name} package.include must contain {required}" + )); + } + } + + let package_root = manifest_path + .parent() + .expect("workspace member manifest has a parent"); + for relative in ["LICENSE-APACHE", "LICENSE-MIT"] { + let package_bytes = fs::read(package_root.join(relative)).map_err(|error| { + format!("public package {package_name} must include {relative}: {error}") + })?; + let root_bytes = fs::read(root.join(relative)) + .map_err(|error| format!("failed to read {relative}: {error}"))?; + if package_bytes != root_bytes { + return Err(format!( + "public package {package_name} {relative} must match the workspace license" + )); + } + } + if !package_root.join("README.md").is_file() { + return Err(format!( + "public package {package_name} must include a package-local README.md" + )); + } + + let docs_rs = package + .get("metadata") + .and_then(toml::Value::as_table) + .and_then(|metadata| metadata.get("docs")) + .and_then(toml::Value::as_table) + .and_then(|docs| docs.get("rs")) + .and_then(toml::Value::as_table) + .ok_or_else(|| { + format!("public package {package_name} must define [package.metadata.docs.rs]") + })?; + if docs_rs + .get("all-features") + .and_then(toml::Value::as_bool) + .unwrap_or(false) + { + return Err(format!( + "public package {package_name} docs.rs must use an intentional feature set" + )); + } + let docs_features = docs_rs + .get("features") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("public package {package_name} docs.rs must define features"))?; + let declared_features = manifest.get("features").and_then(toml::Value::as_table); + for feature in docs_features { + let feature = feature.as_str().ok_or_else(|| { + format!("public package {package_name} docs.rs features must be strings") + })?; + if !declared_features.is_some_and(|features| features.contains_key(feature)) { + return Err(format!( + "public package {package_name} docs.rs selects unknown feature {feature}" + )); + } + } + } + Ok(()) +} + +fn check_sdk_feature_matrix(root: &Path) -> Result<(), String> { + let path = root.join("crates/sdk/Cargo.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let features = manifest + .get("features") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} must define [features]", path.display()))?; + let expected = BTreeSet::from([ + "default", + "full", + "geonames", + "knowledge", + "local-signing", + "memory", + "native", + "nip46", + "nostr", + "radrootsd", + "sqlite", + "sync", + ]); + let actual = features.keys().map(String::as_str).collect::<BTreeSet<_>>(); + if actual != expected { + return Err(format!( + "crates/sdk/Cargo.toml feature vocabulary mismatch: expected {expected:?}, found {actual:?}" + )); + } + for (feature, expected_entries) in [ + ("default", &["memory"][..]), + ("memory", &["radroots_storage/memory"]), + ("sqlite", &["dep:radroots_storage_sqlite"]), + ("sync", &["dep:radroots_sync", "dep:uuid"]), + ( + "nostr", + &["sync", "dep:radroots_nostr", "dep:radroots_transport_nostr"], + ), + ("nip46", &["nostr", "dep:radroots_nostr_connect"]), + ( + "local-signing", + &[ + "dep:radroots_nostr", + "dep:radroots_secrets", + "radroots_nostr/signing", + ], + ), + ( + "radrootsd", + &["sync", "dep:reqwest", "dep:serde", "dep:serde_json"], + ), + ("geonames", &["dep:radroots_geonames"]), + ( + "knowledge", + &["radroots_event/knowledge", "radroots_event_codec/knowledge"], + ), + ("native", &["sqlite", "sync", "local-signing"]), + ( + "full", + &[ + "native", + "nostr", + "nip46", + "radrootsd", + "geonames", + "knowledge", + ], + ), + ] { + let actual_entries = feature_entries(features, feature)? + .into_iter() + .collect::<BTreeSet<_>>(); + let expected_entries = expected_entries.iter().copied().collect::<BTreeSet<_>>(); + if actual_entries != expected_entries { + return Err(format!( + "crates/sdk/Cargo.toml feature `{feature}` mismatch: expected {expected_entries:?}, found {actual_entries:?}" + )); + } + } + check_sdk_workspace_reticulum_dependency_boundaries(root)?; + Ok(()) +} + +fn check_sdk_workspace_reticulum_dependency_boundaries(root: &Path) -> Result<(), String> { + for path in sdk_manifest_paths(root)? { + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let manifest_label = relative_path_string(root, &path)?; + check_manifest_reticulum_dependency_boundaries(&manifest, &manifest_label)?; + } + check_cargo_lock_reticulum_package_names(root)?; + check_cargo_metadata_reticulum_package_names(root)?; + Ok(()) +} + +fn sdk_manifest_paths(root: &Path) -> Result<Vec<PathBuf>, String> { + let root_manifest_path = root.join("Cargo.toml"); + let raw = fs::read_to_string(&root_manifest_path) + .map_err(|error| format!("failed to read {}: {error}", root_manifest_path.display()))?; + let manifest = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", root_manifest_path.display()))?; + let members = manifest + .get("workspace") + .and_then(|workspace| workspace.get("members")) + .and_then(toml::Value::as_array) + .ok_or_else(|| "Cargo.toml must define workspace.members".to_owned())?; + let mut paths = Vec::with_capacity(members.len() + 1); + paths.push(root_manifest_path); + for member in members { + let member = member + .as_str() + .ok_or_else(|| "Cargo.toml workspace.members entries must be strings".to_owned())?; + if member.contains('*') { + return Err(format!( + "Cargo.toml workspace member globs are not supported by the SDK Reticulum gate: {member}" + )); + } + paths.push(root.join(member).join("Cargo.toml")); + } + Ok(paths) +} + +fn check_manifest_reticulum_dependency_boundaries( + manifest: &toml::Value, + manifest_label: &str, +) -> Result<(), String> { + let table = manifest + .as_table() + .ok_or_else(|| format!("{manifest_label} must parse as a TOML table"))?; + inspect_manifest_table_for_reticulum_dependencies(table, manifest_label, &mut Vec::new()) +} + +fn inspect_manifest_table_for_reticulum_dependencies( + table: &toml::map::Map<String, toml::Value>, + manifest_label: &str, + path: &mut Vec<String>, +) -> Result<(), String> { + for (key, value) in table { + path.push(key.clone()); + if is_dependency_section_key(key) { + let dependencies = value.as_table().ok_or_else(|| { + format!( + "{manifest_label} section `{}` must be a table", + path.join(".") + ) + })?; + check_reticulum_dependency_table(dependencies, manifest_label, path)?; + path.pop(); + continue; + } + if key == "features" && path.len() == 1 { + let features = value.as_table().ok_or_else(|| { + format!( + "{manifest_label} section `{}` must be a table", + path.join(".") + ) + })?; + check_reticulum_feature_table(features, manifest_label, path)?; + } + if let Some(nested) = value.as_table() { + inspect_manifest_table_for_reticulum_dependencies(nested, manifest_label, path)?; + } + path.pop(); + } + Ok(()) +} + +fn is_dependency_section_key(key: &str) -> bool { + matches!( + key, + "dependencies" | "dev-dependencies" | "build-dependencies" + ) +} + +fn check_reticulum_dependency_table( + dependencies: &toml::map::Map<String, toml::Value>, + manifest_label: &str, + path: &[String], +) -> Result<(), String> { + let section = path.join("."); + for (dependency_key, dependency_spec) in dependencies { + reject_forbidden_reticulum_runtime_name(dependency_key, manifest_label, &section)?; + if let Some(package_name) = dependency_spec + .as_table() + .and_then(|table| table.get("package")) + .and_then(toml::Value::as_str) + { + reject_forbidden_reticulum_runtime_name(package_name, manifest_label, &section)?; + } + } + Ok(()) +} + +fn check_reticulum_feature_table( + features: &toml::map::Map<String, toml::Value>, + manifest_label: &str, + path: &[String], +) -> Result<(), String> { + let section = path.join("."); + for (feature_name, entries) in features { + if feature_name == "transport-reticulum-preview" || feature_name == "reticulum-runtime" { + return Err(format!( + "{manifest_label} feature `{feature_name}` must not introduce a Reticulum preview alias" + )); + } + reject_forbidden_reticulum_runtime_name(feature_name, manifest_label, &section)?; + let entries = entries + .as_array() + .ok_or_else(|| format!("{manifest_label} feature `{feature_name}` must be an array"))?; + for entry in entries { + let entry = entry.as_str().ok_or_else(|| { + format!("{manifest_label} feature `{feature_name}` entries must be strings") + })?; + check_reticulum_feature_entry(entry, manifest_label, feature_name)?; + } + } + Ok(()) +} + +fn check_reticulum_feature_entry( + entry: &str, + manifest_label: &str, + feature_name: &str, +) -> Result<(), String> { + let feature_dependency = entry.strip_prefix("dep:").unwrap_or(entry); + let (dependency_name, dependency_feature) = feature_dependency + .split_once('/') + .map_or((feature_dependency, None), |(name, feature)| { + (name, Some(feature)) + }); + if dependency_name == ALLOWED_RETICULUM_PREVIEW_PACKAGE && dependency_feature.is_some() { + return Err(format!( + "{manifest_label} feature `{feature_name}` must not enable Reticulum preview crate features through `{entry}`" + )); + } + reject_forbidden_reticulum_runtime_name(dependency_name, manifest_label, "features")?; + Ok(()) +} + +fn check_cargo_lock_reticulum_package_names(root: &Path) -> Result<(), String> { + let path = root.join("Cargo.lock"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let lock = raw + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let packages = lock + .get("package") + .and_then(toml::Value::as_array) + .ok_or_else(|| "Cargo.lock must define package entries".to_owned())?; + for package in packages { + let name = package + .get("name") + .and_then(toml::Value::as_str) + .ok_or_else(|| "Cargo.lock package entry must define name".to_owned())?; + reject_forbidden_reticulum_runtime_name(name, "Cargo.lock", "package")?; + } + Ok(()) +} + +fn check_cargo_metadata_reticulum_package_names(root: &Path) -> Result<(), String> { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--all-features"]) + .current_dir(root) + .output() + .map_err(|error| format!("failed to run cargo metadata: {error}"))?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + let metadata = serde_json::from_slice::<CargoMetadata>(&output.stdout) + .map_err(|error| format!("failed to parse cargo metadata: {error}"))?; + for package in metadata.packages { + reject_forbidden_reticulum_runtime_name(&package.name, "cargo metadata", "packages")?; + } + Ok(()) +} + +fn reject_forbidden_reticulum_runtime_name( + name: &str, + manifest_label: &str, + section: &str, +) -> Result<(), String> { + if is_forbidden_reticulum_runtime_name(name) { + Err(format!( + "{manifest_label} section `{section}` must not reference real Reticulum or Python runtime dependency `{name}`" + )) + } else { + Ok(()) + } +} + +fn is_forbidden_reticulum_runtime_name(name: &str) -> bool { + if name == ALLOWED_RETICULUM_PREVIEW_PACKAGE { + return false; + } + let normalized = normalize_package_name(name); + matches!( + normalized.as_str(), + "rns" | "rnsd" | "reticulum" | "reticulum-rs" | "python" | "pyo3" + ) || normalized.contains("reticulum") + || normalized.starts_with("rns-") + || normalized.contains("-rns-") + || normalized.ends_with("-rns") + || normalized.starts_with("python-") + || normalized.starts_with("pyo3-") +} + +fn normalize_package_name(name: &str) -> String { + name.to_ascii_lowercase().replace('_', "-") +} + +fn feature_entries<'features>( + features: &'features toml::map::Map<String, toml::Value>, + feature: &str, +) -> Result<Vec<&'features str>, String> { + features + .get(feature) + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("crates/sdk/Cargo.toml must define feature `{feature}`"))? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or_else(|| format!("feature `{feature}` must contain only string entries")) + }) + .collect() +} + +fn check_package_source_metadata(root: &Path) -> Result<(), String> { + for spec in package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let index_path = package_dir.join("src/index.ts"); + let package_json = + check_package_json(&package_json_path, spec.package_name, spec.package_dir)?; + check_package_distribution_metadata(root, &package_dir, &package_json_path, &package_json)?; + if !index_path.is_file() { + return Err(format!("missing package index: {}", index_path.display())); + } + check_package_index(&index_path)?; + } + for spec in wasm_package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let package_json = + check_package_json(&package_json_path, spec.package_name, spec.package_dir)?; + check_package_distribution_metadata(root, &package_dir, &package_json_path, &package_json)?; + } + Ok(()) +} + +fn check_generated_outputs(root: &Path) -> Result<(), String> { + let outputs = package_outputs()?; + for output in &outputs { + check_generated_package_artifact_inventory(root, output)?; + } + for output in outputs { + for expected in output.files() { + let path = root + .join(output.spec.package_dir) + .join(expected.relative_path); + let actual = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected.contents { + return Err(format!("stale generated output: {}", path.display())); + } + } + let expected = output.provenance_file(); + let path = root.join(&expected.relative_path); + let actual = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected.contents { + return Err(format!("stale generated provenance: {}", path.display())); + } + } + Ok(()) +} + +fn check_package_build_artifacts(root: &Path) -> Result<(), String> { + for spec in package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let package_json = read_package_json_value(&package_json_path)?; + let surface_paths = package_surface_paths(&package_json, &package_json_path)?; + check_package_surface_artifacts(&package_dir, spec.package_name, &surface_paths)?; + } + for spec in wasm_package_specs() { + check_wasm_package_surface(root, *spec)?; + } + Ok(()) +} + +fn check_generated_package_artifact_inventory( + root: &Path, + output: &PackageOutput, +) -> Result<(), String> { + let package_dir = root.join(output.spec.package_dir); + let generated_dir = package_dir.join("src/generated"); + let expected = output + .files() + .into_iter() + .map(|file| file.relative_path) + .collect::<BTreeSet<_>>(); + let actual = generated_package_files(&package_dir, &generated_dir)?; + if actual != expected { + let missing = expected.difference(&actual).cloned().collect::<Vec<_>>(); + let extra = actual.difference(&expected).cloned().collect::<Vec<_>>(); + return Err(format!( + "generated artifact inventory mismatch for {}: missing {:?}, extra {:?}", + output.spec.package_name, missing, extra + )); + } + Ok(()) +} + +fn generated_package_files( + package_dir: &Path, + generated_dir: &Path, +) -> Result<BTreeSet<String>, String> { + let mut files = BTreeSet::new(); + collect_package_files(package_dir, generated_dir, &mut files)?; + Ok(files) +} + +fn collect_package_files( + package_dir: &Path, + dir: &Path, + files: &mut BTreeSet<String>, +) -> Result<(), String> { + for entry in + fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))? + { + let entry = + entry.map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?; + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|error| format!("failed to inspect {}: {error}", path.display()))?; + if file_type.is_dir() { + collect_package_files(package_dir, &path, files)?; + } else if file_type.is_file() { + files.insert(relative_path_string(package_dir, &path)?); + } + } + Ok(()) +} + +fn relative_path_string(base: &Path, path: &Path) -> Result<String, String> { + let relative = path + .strip_prefix(base) + .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?; + Ok(relative + .components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::<Vec<_>>() + .join("/")) +} + +fn check_package_index(path: &Path) -> Result<(), String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if raw.starts_with(generated_header()) { + return Err(format!( + "package index must be handwritten source: {}", + path.display() + )); + } + Ok(()) +} + +fn check_binding_crate_sources(root: &Path) -> Result<(), String> { + for spec in package_specs() { + let crate_src_dir = root.join(spec.crate_dir).join("src"); + let typescript_dir = crate_src_dir.join("typescript"); + if typescript_dir.exists() { + return Err(format!( + "forbidden crate TypeScript source directory exists: {}", + typescript_dir.display() + )); + } + check_no_typescript_files(&crate_src_dir)?; + } + for spec in wasm_package_specs() { + check_no_typescript_files(&root.join(spec.crate_dir).join("src"))?; + } + Ok(()) +} + +fn check_no_typescript_files(dir: &Path) -> Result<(), String> { + for entry in + fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))? + { + let entry = + entry.map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?; + let path = entry.path(); + let file_type = entry + .file_type() + .map_err(|error| format!("failed to inspect {}: {error}", path.display()))?; + if file_type.is_dir() { + check_no_typescript_files(&path)?; + } else if file_type.is_file() + && path.extension().and_then(|extension| extension.to_str()) == Some("ts") + { + return Err(format!( + "forbidden crate TypeScript source file exists: {}", + path.display() + )); + } + } + Ok(()) +} + +fn check_forbidden_packages(root: &Path) -> Result<(), String> { + for forbidden in FORBIDDEN_PACKAGE_NAMES { + let package_leaf = forbidden.trim_start_matches("@radroots/").to_owned(); + let forbidden_dir = root.join("packages").join(package_leaf); + if forbidden_dir.exists() { + return Err(format!( + "forbidden package directory exists: {}", + forbidden_dir.display() + )); + } + } + Ok(()) +} + +fn check_package_json( + path: &Path, + expected_name: &str, + expected_directory: &str, +) -> Result<serde_json::Value, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let json = serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let actual_name = json + .get("name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| format!("package.json missing name: {}", path.display()))?; + if actual_name != expected_name { + return Err(format!( + "package name mismatch in {}: expected {expected_name}, found {actual_name}", + path.display() + )); + } + if json.get("private").is_some() { + return Err(format!( + "public package must not set private: {}", + path.display() + )); + } + let _ = package_description(&json, path)?; + require_string_field(&json, path, "version", PACKAGE_VERSION)?; + require_string_field(&json, path, "license", PACKAGE_LICENSE)?; + require_string_field(&json, path, "homepage", PACKAGE_HOMEPAGE)?; + require_string_field(&json, path, "type", "module")?; + require_bool_field(&json, path, "sideEffects", false)?; + check_publish_config(&json, path)?; + check_repository(&json, path, expected_directory)?; + check_package_files(&json, path)?; + check_no_pack_lifecycle_scripts(&json, path)?; + check_workspace_dependencies(&json, path)?; + Ok(json) +} + +pub(crate) fn check_wasm_package_surface(root: &Path, spec: WasmPackageSpec) -> Result<(), String> { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let json = check_package_json(&package_json_path, spec.package_name, spec.package_dir)?; + check_package_distribution_metadata(root, &package_dir, &package_json_path, &json)?; + let dist_manifest = package_dir.join("dist").join("package.json"); + if dist_manifest.exists() { + return Err(format!( + "generated package manifest is forbidden: {}", + dist_manifest.display() + )); + } + check_no_wasm_dist_ignore_files(&package_dir, spec)?; + let surface_paths = package_surface_paths(&json, &package_json_path)?; + check_public_wasm_declaration_inventory(&surface_paths, spec)?; + check_package_surface_artifacts(&package_dir, spec.package_name, &surface_paths)?; + check_wasm_runtime_files(&package_dir, spec)?; + check_wasm_declaration_files(&package_dir, spec)?; + validate_provenance_manifest(root, spec)?; + Ok(()) +} + +fn require_string_field( + json: &serde_json::Value, + package_json_path: &Path, + field: &'static str, + expected: &str, +) -> Result<(), String> { + let actual = json + .get(field) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing {field}: {}", + package_json_path.display() + ) + })?; + if actual != expected { + return Err(format!( + "package.json {field} mismatch in {}: expected {expected}, found {actual}", + package_json_path.display() + )); + } + Ok(()) +} + +fn require_bool_field( + json: &serde_json::Value, + package_json_path: &Path, + field: &'static str, + expected: bool, +) -> Result<(), String> { + let actual = json + .get(field) + .and_then(serde_json::Value::as_bool) + .ok_or_else(|| { + format!( + "package.json missing {field}: {}", + package_json_path.display() + ) + })?; + if actual != expected { + return Err(format!( + "package.json {field} mismatch in {}: expected {expected}, found {actual}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_publish_config(json: &serde_json::Value, package_json_path: &Path) -> Result<(), String> { + let access = json + .get("publishConfig") + .and_then(|value| value.get("access")) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing publishConfig.access: {}", + package_json_path.display() + ) + })?; + if access != PUBLISH_ACCESS { + return Err(format!( + "package.json publishConfig.access mismatch in {}: expected {PUBLISH_ACCESS}, found {access}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_repository( + json: &serde_json::Value, + package_json_path: &Path, + expected_directory: &str, +) -> Result<(), String> { + let repository = json.get("repository").ok_or_else(|| { + format!( + "package.json missing repository: {}", + package_json_path.display() + ) + })?; + let repository_type = repository + .get("type") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing repository.type: {}", + package_json_path.display() + ) + })?; + if repository_type != "git" { + return Err(format!( + "package.json repository.type mismatch in {}: expected git, found {repository_type}", + package_json_path.display() + )); + } + let repository_url = repository + .get("url") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing repository.url: {}", + package_json_path.display() + ) + })?; + if repository_url != PACKAGE_REPOSITORY_URL { + return Err(format!( + "package.json repository.url mismatch in {}: expected {PACKAGE_REPOSITORY_URL}, found {repository_url}", + package_json_path.display() + )); + } + let repository_directory = repository + .get("directory") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing repository.directory: {}", + package_json_path.display() + ) + })?; + if repository_directory != expected_directory { + return Err(format!( + "package.json repository.directory mismatch in {}: expected {expected_directory}, found {repository_directory}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_package_files(json: &serde_json::Value, package_json_path: &Path) -> Result<(), String> { + let files = json + .get("files") + .and_then(serde_json::Value::as_array) + .ok_or_else(|| { + format!( + "package.json missing files: {}", + package_json_path.display() + ) + })?; + let actual = files + .iter() + .map(|value| { + value.as_str().ok_or_else(|| { + format!( + "package.json files entries must be strings: {}", + package_json_path.display() + ) + }) + }) + .collect::<Result<Vec<_>, _>>()?; + if actual != PACKAGE_FILES { + return Err(format!( + "package.json files must publish dist plus approved metadata only: {}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_workspace_dependencies( + json: &serde_json::Value, + package_json_path: &Path, +) -> Result<(), String> { + let Some(dependencies) = json.get("dependencies") else { + return Ok(()); + }; + let dependencies = dependencies.as_object().ok_or_else(|| { + format!( + "package.json dependencies must be an object: {}", + package_json_path.display() + ) + })?; + for (name, value) in dependencies { + if name.starts_with("@radroots/") { + let version = value.as_str().ok_or_else(|| { + format!( + "package.json dependency versions must be strings: {}", + package_json_path.display() + ) + })?; + if version != "workspace:^" { + return Err(format!( + "package.json workspace dependency {name} must use workspace:^ in {}", + package_json_path.display() + )); + } + } + } + Ok(()) +} + +fn check_npm_pack_payloads(root: &Path) -> Result<(), String> { + let pack_dir = + tempfile::tempdir().map_err(|error| format!("failed to create pack temp dir: {error}"))?; + let mut packed_packages = Vec::new(); + for spec in package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let json = read_package_json_value(&package_json_path)?; + let expected_dist_files = + expected_packed_dist_files(&package_dir, &json, &package_json_path, None)?; + let required_files = required_npm_payload_files(&expected_dist_files); + let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?; + let packed_json = read_packed_package_json(&packed, spec.package_name)?; + check_packed_package_json( + &json, + &packed_json, + &package_json_path, + spec.package_name, + spec.package_dir, + )?; + let payload_files = packed_payload_files(&packed)?; + validate_npm_pack_payload(spec.package_name, &payload_files, &required_files, None)?; + validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?; + packed_packages.push(packed); + } + for spec in wasm_package_specs() { + let package_dir = root.join(spec.package_dir); + let package_json_path = package_dir.join("package.json"); + let json = read_package_json_value(&package_json_path)?; + let expected_dist_files = + expected_packed_dist_files(&package_dir, &json, &package_json_path, Some(*spec))?; + let required_files = required_npm_payload_files(&expected_dist_files); + let packed = pnpm_pack_package(&package_dir, spec.package_name, pack_dir.path())?; + let packed_json = read_packed_package_json(&packed, spec.package_name)?; + check_packed_package_json( + &json, + &packed_json, + &package_json_path, + spec.package_name, + spec.package_dir, + )?; + let payload_files = packed_payload_files(&packed)?; + validate_npm_pack_payload( + spec.package_name, + &payload_files, + &required_files, + Some(&format!("dist/{}_bg.wasm", spec.out_name)), + )?; + validate_packed_dist_inventory(spec.package_name, &payload_files, &expected_dist_files)?; + packed_packages.push(packed); + } + check_clean_consumer_smoke(&packed_packages)?; + Ok(()) +} + +fn check_no_pack_lifecycle_scripts( + json: &serde_json::Value, + package_json_path: &Path, +) -> Result<(), String> { + let Some(scripts) = json.get("scripts") else { + return Ok(()); + }; + let scripts = scripts.as_object().ok_or_else(|| { + format!( + "package.json scripts must be an object: {}", + package_json_path.display() + ) + })?; + for forbidden in [ + "prepack", + "postpack", + "prepare", + "prepublish", + "prepublishOnly", + ] { + if scripts.contains_key(forbidden) { + return Err(format!( + "package.json script {forbidden} is forbidden because pnpm pack runs lifecycle scripts: {}", + package_json_path.display() + )); + } + } + Ok(()) +} + +fn read_package_json_value(path: &Path) -> Result<serde_json::Value, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn required_npm_payload_files(expected_dist_files: &BTreeSet<String>) -> BTreeSet<String> { + let mut required = package_metadata_payload_files(); + required.extend(expected_dist_files.iter().cloned()); + required +} + +fn package_metadata_payload_files() -> BTreeSet<String> { + BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "LICENSE-APACHE".to_owned(), + ]) +} + +fn expected_packed_dist_files( + package_dir: &Path, + json: &serde_json::Value, + package_json_path: &Path, + wasm_spec: Option<WasmPackageSpec>, +) -> Result<BTreeSet<String>, String> { + let mut expected = BTreeSet::new(); + for path in package_surface_paths(json, package_json_path)? { + expected.insert(normalized_package_path(&path)?); + } + if let Some(spec) = wasm_spec { + for path in wasm_runtime_files(spec) { + expected.insert(path); + } + expected.insert(format!("dist/{}.provenance.json", spec.out_name)); + } else { + expected.extend(expected_binding_generated_dist_files(package_dir)?); + } + Ok(expected) +} + +fn expected_binding_generated_dist_files(package_dir: &Path) -> Result<BTreeSet<String>, String> { + let generated_dir = package_dir.join("src/generated"); + let mut expected = BTreeSet::new(); + for source in generated_package_files(package_dir, &generated_dir)? { + let stem = source + .strip_prefix("src/generated/") + .and_then(|path| path.strip_suffix(".ts")) + .ok_or_else(|| { + format!( + "generated package source must be a TypeScript file under src/generated: {}", + package_dir.join(&source).display() + ) + })?; + expected.insert(format!("dist/generated/{stem}.js")); + expected.insert(format!("dist/generated/{stem}.d.ts")); + } + Ok(expected) +} + +fn pnpm_pack_package( + package_dir: &Path, + package_name: &str, + pack_destination: &Path, +) -> Result<PackedPackage, String> { + let output = Command::new("pnpm") + .args(["pack", "--json", "--pack-destination"]) + .arg(pack_destination) + .current_dir(package_dir) + .output() + .map_err(|error| { + format!( + "failed to run pnpm pack for {package_name} in {}: {error}", + package_dir.display() + ) + })?; + if !output.status.success() { + return Err(format!( + "pnpm pack failed for {package_name} in {}: {}", + package_dir.display(), + String::from_utf8_lossy(&output.stderr) + )); + } + let entry = parse_pnpm_pack_entry(package_name, &output.stdout, &output.stderr)?; + packed_package_from_pnpm_entry(package_name, pack_destination, entry) +} + +fn parse_pnpm_pack_entry( + package_name: &str, + stdout: &[u8], + stderr: &[u8], +) -> Result<PnpmPackEntry, String> { + serde_json::from_slice::<PnpmPackEntry>(stdout).map_err(|error| { + format!( + "failed to parse pnpm pack output for {package_name}: {error}; stdout: {}; stderr: {}", + String::from_utf8_lossy(stdout), + String::from_utf8_lossy(stderr) + ) + }) +} + +fn packed_package_from_pnpm_entry( + package_name: &str, + pack_destination: &Path, + entry: PnpmPackEntry, +) -> Result<PackedPackage, String> { + if entry.filename.trim().is_empty() { + return Err(format!( + "pnpm pack output for {package_name} is missing tarball filename" + )); + } + let tarball_path = PathBuf::from(&entry.filename); + if !tarball_path.starts_with(pack_destination) { + return Err(format!( + "pnpm pack tarball for {package_name} must be written under {}: {}", + pack_destination.display(), + tarball_path.display() + )); + } + if !tarball_path.is_file() { + return Err(format!( + "pnpm pack tarball for {package_name} does not exist: {}", + tarball_path.display() + )); + } + Ok(PackedPackage { + package_name: package_name.to_owned(), + tarball_path, + files: entry + .files + .into_iter() + .map(|file| NpmPackFile { path: file.path }) + .collect(), + }) +} + +fn check_clean_consumer_smoke(packed_packages: &[PackedPackage]) -> Result<(), String> { + let consumer_dir = tempfile::tempdir() + .map_err(|error| format!("failed to create clean npm consumer temp dir: {error}"))?; + fs::write( + consumer_dir.path().join("package.json"), + "{\n \"name\": \"radroots-sdk-package-smoke\",\n \"private\": true,\n \"type\": \"module\"\n}\n", + ) + .map_err(|error| { + format!( + "failed to write clean npm consumer package.json in {}: {error}", + consumer_dir.path().display() + ) + })?; + let npm_cache_dir = consumer_dir.path().join(".npm-cache"); + let mut install = Command::new("npm"); + install + .args([ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "--registry", + "http://127.0.0.1:9", + "--cache", + ]) + .arg(&npm_cache_dir); + for packed in packed_packages { + install.arg(&packed.tarball_path); + } + let output = install + .current_dir(consumer_dir.path()) + .output() + .map_err(|error| { + format!( + "failed to run clean npm consumer install in {}: {error}", + consumer_dir.path().display() + ) + })?; + if !output.status.success() { + return Err(format!( + "clean npm consumer install failed in {}: stdout: {}; stderr: {}", + consumer_dir.path().display(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + let package_names = packed_packages + .iter() + .map(|package| package.package_name.clone()) + .collect::<Vec<_>>(); + let smoke_script = consumer_smoke_script(&package_names)?; + fs::write(consumer_dir.path().join("smoke.mjs"), smoke_script).map_err(|error| { + format!( + "failed to write clean npm consumer smoke script in {}: {error}", + consumer_dir.path().display() + ) + })?; + let output = Command::new("node") + .arg("smoke.mjs") + .current_dir(consumer_dir.path()) + .output() + .map_err(|error| { + format!( + "failed to run clean npm consumer import smoke in {}: {error}", + consumer_dir.path().display() + ) + })?; + if !output.status.success() { + return Err(format!( + "clean npm consumer import smoke failed in {}: stdout: {}; stderr: {}", + consumer_dir.path().display(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) +} + +fn consumer_smoke_script(package_names: &[String]) -> Result<String, String> { + let mut script = String::new(); + for package_name in package_names { + let quoted = serde_json::to_string(package_name) + .map_err(|error| format!("failed to quote package name {package_name}: {error}"))?; + script.push_str("await import("); + script.push_str(&quoted); + script.push_str(");\n"); + } + Ok(script) +} + +fn packed_payload_files(packed: &PackedPackage) -> Result<BTreeSet<String>, String> { + debug_assert!(packed.tarball_path.is_file()); + packed + .files + .iter() + .map(|file| normalized_package_path(&file.path)) + .collect() +} + +fn read_packed_package_json( + packed: &PackedPackage, + package_name: &str, +) -> Result<serde_json::Value, String> { + let file = fs::File::open(&packed.tarball_path).map_err(|error| { + format!( + "failed to open pnpm pack tarball for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + })?; + let decoder = GzDecoder::new(file); + let mut archive = Archive::new(decoder); + let entries = archive.entries().map_err(|error| { + format!( + "failed to read pnpm pack tarball entries for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + })?; + for entry in entries { + let mut entry = entry.map_err(|error| { + format!( + "failed to read pnpm pack tarball entry for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + })?; + let path = entry.path().map_err(|error| { + format!( + "failed to read pnpm pack tarball entry path for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + })?; + if path.as_ref() != Path::new("package/package.json") { + continue; + } + let mut raw = String::new(); + entry.read_to_string(&mut raw).map_err(|error| { + format!( + "failed to read packed package.json for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + })?; + return serde_json::from_str::<serde_json::Value>(&raw).map_err(|error| { + format!( + "failed to parse packed package.json for {package_name}: {}: {error}", + packed.tarball_path.display() + ) + }); + } + Err(format!( + "pnpm pack tarball for {package_name} is missing package/package.json: {}", + packed.tarball_path.display() + )) +} + +fn check_packed_package_json( + source_json: &serde_json::Value, + packed_json: &serde_json::Value, + package_json_path: &Path, + expected_name: &str, + expected_directory: &str, +) -> Result<(), String> { + let source_description = package_description(source_json, package_json_path)?; + let packed_description = package_description(packed_json, package_json_path)?; + if packed_description != source_description { + return Err(format!( + "packed package.json description mismatch in {}: expected {source_description}, found {packed_description}", + package_json_path.display() + )); + } + require_string_field(packed_json, package_json_path, "name", expected_name)?; + require_string_field(packed_json, package_json_path, "version", PACKAGE_VERSION)?; + require_string_field(packed_json, package_json_path, "license", PACKAGE_LICENSE)?; + require_string_field(packed_json, package_json_path, "homepage", PACKAGE_HOMEPAGE)?; + require_string_field(packed_json, package_json_path, "type", "module")?; + require_bool_field(packed_json, package_json_path, "sideEffects", false)?; + check_publish_config(packed_json, package_json_path)?; + check_repository(packed_json, package_json_path, expected_directory)?; + check_package_files(packed_json, package_json_path)?; + check_no_pack_lifecycle_scripts(packed_json, package_json_path)?; + check_same_packed_field(source_json, packed_json, package_json_path, "main")?; + check_same_packed_field(source_json, packed_json, package_json_path, "types")?; + check_same_packed_field(source_json, packed_json, package_json_path, "exports")?; + check_same_packed_field(source_json, packed_json, package_json_path, "scripts")?; + check_packed_dependency_maps(source_json, packed_json, package_json_path)?; + let source_surface = package_surface_paths(source_json, package_json_path)?; + let packed_surface = package_surface_paths(packed_json, package_json_path)?; + if packed_surface != source_surface { + return Err(format!( + "packed package.json export surface mismatch in {}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_same_packed_field( + source_json: &serde_json::Value, + packed_json: &serde_json::Value, + package_json_path: &Path, + field: &'static str, +) -> Result<(), String> { + if source_json.get(field) != packed_json.get(field) { + return Err(format!( + "packed package.json {field} mismatch in {}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_packed_dependency_maps( + source_json: &serde_json::Value, + packed_json: &serde_json::Value, + package_json_path: &Path, +) -> Result<(), String> { + for field in ["dependencies", "peerDependencies", "optionalDependencies"] { + check_packed_dependency_map(source_json, packed_json, package_json_path, field)?; + } + Ok(()) +} + +fn check_packed_dependency_map( + source_json: &serde_json::Value, + packed_json: &serde_json::Value, + package_json_path: &Path, + field: &'static str, +) -> Result<(), String> { + let source_dependencies = optional_dependency_map(source_json, package_json_path, field)?; + let packed_dependencies = optional_dependency_map(packed_json, package_json_path, field)?; + let source_keys = source_dependencies + .iter() + .flat_map(|dependencies| dependencies.keys().cloned()) + .collect::<BTreeSet<_>>(); + let packed_keys = packed_dependencies + .iter() + .flat_map(|dependencies| dependencies.keys().cloned()) + .collect::<BTreeSet<_>>(); + if packed_keys != source_keys { + return Err(format!( + "packed package.json {field} keys mismatch in {}: expected {:?}, found {:?}", + package_json_path.display(), + source_keys, + packed_keys + )); + } + let Some(packed_dependencies) = packed_dependencies else { + return Ok(()); + }; + let source_dependencies = source_dependencies.expect("matching dependency keys require source"); + for (name, packed_version) in packed_dependencies { + let packed_version = packed_version.as_str().ok_or_else(|| { + format!( + "packed package.json {field} dependency versions must be strings in {}", + package_json_path.display() + ) + })?; + if packed_version.starts_with("workspace:") { + return Err(format!( + "packed package.json {field} dependency {name} must not use workspace protocol in {}", + package_json_path.display() + )); + } + let source_version = source_dependencies + .get(name) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "source package.json {field} dependency {name} must be a string in {}", + package_json_path.display() + ) + })?; + let expected = expected_packed_dependency_version(name, source_version); + if packed_version != expected { + return Err(format!( + "packed package.json {field} dependency {name} mismatch in {}: expected {expected}, found {packed_version}", + package_json_path.display() + )); + } + } + Ok(()) +} + +fn optional_dependency_map<'a>( + json: &'a serde_json::Value, + package_json_path: &Path, + field: &'static str, +) -> Result<Option<&'a serde_json::Map<String, serde_json::Value>>, String> { + json.get(field) + .map(|value| { + value.as_object().ok_or_else(|| { + format!( + "package.json {field} must be an object: {}", + package_json_path.display() + ) + }) + }) + .transpose() +} + +fn expected_packed_dependency_version(name: &str, source_version: &str) -> String { + if name.starts_with("@radroots/") && source_version == "workspace:^" { + format!("^{PACKAGE_VERSION}") + } else { + source_version.to_owned() + } +} + +fn validate_npm_pack_payload( + package_name: &str, + payload_files: &BTreeSet<String>, + required_files: &BTreeSet<String>, + expected_wasm_file: Option<&str>, +) -> Result<(), String> { + let missing = required_files + .difference(payload_files) + .cloned() + .collect::<Vec<_>>(); + if !missing.is_empty() { + return Err(format!( + "npm pack payload for {package_name} is missing required files: {missing:?}" + )); + } + for path in payload_files { + if let Some(reason) = forbidden_npm_payload_path(path) { + return Err(format!( + "npm pack payload for {package_name} includes forbidden {reason}: {path}" + )); + } + } + if let Some(expected_wasm_file) = expected_wasm_file { + let wasm_files = payload_files + .iter() + .filter(|path| path.ends_with(".wasm")) + .cloned() + .collect::<Vec<_>>(); + if wasm_files != [expected_wasm_file.to_owned()] { + return Err(format!( + "npm pack payload for {package_name} must include exactly {expected_wasm_file}; found {wasm_files:?}" + )); + } + } + Ok(()) +} + +fn validate_packed_dist_inventory( + package_name: &str, + payload_files: &BTreeSet<String>, + expected_dist_files: &BTreeSet<String>, +) -> Result<(), String> { + let actual_dist_files = payload_files + .iter() + .filter(|path| path.starts_with("dist/")) + .cloned() + .collect::<BTreeSet<_>>(); + if &actual_dist_files != expected_dist_files { + let missing = expected_dist_files + .difference(&actual_dist_files) + .cloned() + .collect::<Vec<_>>(); + let extra = actual_dist_files + .difference(expected_dist_files) + .cloned() + .collect::<Vec<_>>(); + return Err(format!( + "npm pack payload for {package_name} has dist inventory mismatch: missing {:?}, extra {:?}", + missing, extra + )); + } + Ok(()) +} + +fn normalized_package_path(path: &str) -> Result<String, String> { + let normalized = path.trim_start_matches("./"); + if normalized.is_empty() + || normalized.starts_with('/') + || normalized.contains('\\') + || normalized + .split('/') + .any(|segment| segment.is_empty() || segment == "." || segment == "..") + { + return Err(format!("invalid npm pack payload path: {path}")); + } + Ok(normalized.to_owned()) +} + +fn forbidden_npm_payload_path(path: &str) -> Option<&'static str> { + if path.starts_with("src/") { + return Some("source path"); + } + if path.starts_with("contracts/") { + return Some("contract path"); + } + if path.contains("sdk-manifest") { + return Some("manifest provenance path"); + } + if path.ends_with(".tsbuildinfo") { + return Some("TypeScript build info path"); + } + if path + .split('/') + .any(|segment| matches!(segment, ".gitignore" | ".npmignore")) + { + return Some("ignore file"); + } + None +} + +fn check_public_wasm_declaration_inventory( + surface_paths: &BTreeSet<String>, + spec: WasmPackageSpec, +) -> Result<(), String> { + let actual = surface_paths + .iter() + .filter(|path| path.ends_with(".d.ts")) + .cloned() + .collect::<BTreeSet<_>>(); + let expected = BTreeSet::from([format!("./dist/{}.d.ts", spec.out_name)]); + if actual != expected { + return Err(format!( + "public wasm declaration inventory mismatch for {}: expected {:?}, found {:?}", + spec.package_name, expected, actual + )); + } + Ok(()) +} + +fn check_no_wasm_dist_ignore_files( + package_dir: &Path, + spec: WasmPackageSpec, +) -> Result<(), String> { + for file_name in [".gitignore", ".npmignore"] { + let path = package_dir.join("dist").join(file_name); + if path.exists() { + return Err(format!( + "wasm dist ignore file would hide package payload from npm for {}: {}", + spec.package_name, + path.display() + )); + } + } + Ok(()) +} + +fn check_wasm_runtime_files(package_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> { + for relative in wasm_runtime_files(spec) { + let path = package_dir.join(&relative); + if !path.is_file() { + return Err(format!( + "missing wasm package runtime artifact for {}: {}", + spec.package_name, + path.display() + )); + } + } + Ok(()) +} + +fn wasm_runtime_files(spec: WasmPackageSpec) -> [String; 3] { + [ + format!("dist/{}.js", spec.out_name), + format!("dist/{}_bg.wasm", spec.out_name), + format!("dist/{}_bg.wasm.d.ts", spec.out_name), + ] +} + +fn check_wasm_declaration_files(package_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> { + for expected in declaration_files(spec)? { + let path = package_dir.join(&expected.relative_path); + let actual = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected.contents { + return Err(format!( + "stale dto_bindgen wasm declaration: {}", + path.display() + )); + } + } + Ok(()) +} + +fn package_surface_paths( + json: &serde_json::Value, + package_json_path: &Path, +) -> Result<BTreeSet<String>, String> { + let mut paths = BTreeSet::new(); + collect_required_package_path(json, package_json_path, "main", &mut paths)?; + collect_required_package_path(json, package_json_path, "types", &mut paths)?; + let exports = json.get("exports").ok_or_else(|| { + format!( + "package.json missing exports: {}", + package_json_path.display() + ) + })?; + match exports { + serde_json::Value::String(path) => { + validate_package_surface_path(path, package_json_path, "exports")?; + paths.insert(path.clone()); + } + serde_json::Value::Object(map) => { + if map.keys().any(|key| key != ".") { + return Err(format!( + "package.json only supports root exports: {}", + package_json_path.display() + )); + } + let root_export = map.get(".").ok_or_else(|| { + format!( + "package.json missing root export: {}", + package_json_path.display() + ) + })?; + collect_export_paths(root_export, package_json_path, "exports[\".\"]", &mut paths)?; + } + _ => { + return Err(format!( + "package.json exports must be a string or object: {}", + package_json_path.display() + )); + } + } + Ok(paths) +} + +fn collect_required_package_path( + json: &serde_json::Value, + package_json_path: &Path, + field: &'static str, + paths: &mut BTreeSet<String>, +) -> Result<(), String> { + let value = json + .get(field) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing {field}: {}", + package_json_path.display() + ) + })?; + validate_package_surface_path(value, package_json_path, field)?; + paths.insert(value.to_owned()); + Ok(()) +} + +fn collect_export_paths( + value: &serde_json::Value, + package_json_path: &Path, + field: &str, + paths: &mut BTreeSet<String>, +) -> Result<(), String> { + match value { + serde_json::Value::String(path) => { + validate_package_surface_path(path, package_json_path, field)?; + paths.insert(path.clone()); + Ok(()) + } + serde_json::Value::Object(map) => { + for (key, value) in map { + collect_export_paths(value, package_json_path, &format!("{field}.{key}"), paths)?; + } + Ok(()) + } + _ => Err(format!( + "package.json {field} must name file paths: {}", + package_json_path.display() + )), + } +} + +fn validate_package_surface_path( + value: &str, + package_json_path: &Path, + field: &str, +) -> Result<(), String> { + if value.trim().is_empty() + || value.trim() != value + || !value.starts_with("./dist/") + || value.contains('\\') + || value.split('/').any(|segment| segment == "..") + { + return Err(format!( + "package.json {field} must be a relative dist path: {}", + package_json_path.display() + )); + } + Ok(()) +} + +fn check_package_surface_artifacts( + package_dir: &Path, + package_name: &str, + surface_paths: &BTreeSet<String>, +) -> Result<(), String> { + for relative in surface_paths { + let normalized = relative.trim_start_matches("./"); + let path = package_dir.join(normalized); + if !path.is_file() { + return Err(format!( + "missing package export artifact for {package_name}: {}", + path.display() + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeSet, + fs, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use flate2::{Compression, write::GzEncoder}; + + use crate::{ + output::package_outputs, + package_matrix::{WasmPackageSpec, validate_package_matrix}, + package_metadata::package_readme, + }; + + use super::{ + PackedPackage, check_binding_crate_sources, check_generated_package_artifact_inventory, + check_manifest_reticulum_dependency_boundaries, check_no_typescript_files, + check_package_distribution_metadata, check_package_index, check_package_json, + check_package_surface_artifacts, check_packed_package_json, check_publication_policy, + check_reticulum_feature_entry, check_wasm_package_surface, consumer_smoke_script, + expected_packed_dist_files, is_forbidden_reticulum_runtime_name, normalized_package_path, + parse_pnpm_pack_entry, read_packed_package_json, validate_npm_pack_payload, + validate_packed_dist_inventory, + }; + + const APPROVED_CRATES: [&str; 19] = [ + "radroots_core", + "radroots_identity", + "radroots_blossom", + "radroots_protocol", + "radroots_event", + "radroots_event_codec", + "radroots_trade", + "radroots_signing", + "radroots_transport", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_secrets", + "radroots_storage", + "radroots_storage_sqlite", + "radroots_transport_nostr", + "radroots_sync", + "radroots_geonames", + "radroots_sdk", + "radroots", + ]; + + fn toml_strings(values: &[&str]) -> String { + values + .iter() + .map(|value| format!("\"{value}\"")) + .collect::<Vec<_>>() + .join(", ") + } + + fn write_publication_architecture(root: &Path) { + fs::create_dir_all(root.join("docs/specs")).expect("create spec directory"); + let mut architecture = format!( + "spec_id = \"radroots.crates.release.v1\"\npackage_count = 19\n\n[repositories.lib]\nversion = \"0.1.0-alpha\"\npackages = [{}]\n\n[repositories.sdk]\nversion = \"0.1.0\"\npackages = [{}]\n", + toml_strings(&APPROVED_CRATES[..17]), + toml_strings(&APPROVED_CRATES[17..]), + ); + for name in APPROVED_CRATES { + architecture.push_str(&format!("\n[[package]]\nname = \"{name}\"\n")); + } + fs::write( + root.join("docs/specs/radroots_crates_release_v1.toml"), + architecture, + ) + .expect("write release architecture"); + } + + fn publication_policy( + frozen: bool, + approved: &[&str], + private: &[&str], + build_codegen: &[&str], + ) -> String { + let publish_order = if frozen { + String::new() + } else { + "\n[publish_order]\ncrates = [\"radroots_sdk\", \"radroots\"]\n".to_owned() + }; + format!( + r#"[publication] +frozen = {frozen} +registry = "crates-io" +final_enablement_step = 305 +spec_id = "radroots.crates.release.v1" +approved_packages = [{}] +local_packages = ["radroots_sdk", "radroots"] +external_packages = [{}] + +[workspace_classification] +private = [{}] +build_codegen = [{}] +test_support = [] +preview = [] +retired = [] +{publish_order} +"#, + toml_strings(approved), + toml_strings(&APPROVED_CRATES[..17]), + toml_strings(private), + toml_strings(build_codegen), + ) + } + + #[test] + fn package_skeleton_is_valid() { + validate_package_matrix().expect("package matrix validates"); + } + + #[test] + fn publication_freeze_rejects_publishable_or_implicit_packages() { + let root = test_root("publication_freeze"); + fs::create_dir_all(root.join("contracts/releases")).expect("create contracts"); + write_publication_architecture(&root); + fs::create_dir_all(root.join("crates/a")).expect("create crate a"); + fs::create_dir_all(root.join("crates/b")).expect("create crate b"); + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n", + ) + .expect("write workspace"); + fs::write( + root.join("contracts/releases/publication.toml"), + publication_policy(true, &APPROVED_CRATES, &["crate-a", "crate-b"], &[]), + ) + .expect("write policy"); + fs::write( + root.join("crates/a/Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = false\n", + ) + .expect("write crate a"); + fs::write( + root.join("crates/b/Cargo.toml"), + "[package]\nname = \"crate-b\"\nversion = \"0.1.0\"\npublish = false\n", + ) + .expect("write crate b"); + check_publication_policy(&root).expect("private workspace satisfies freeze"); + + fs::write( + root.join("crates/a/Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n", + ) + .expect("make crate a publishable"); + let publishable = + check_publication_policy(&root).expect_err("publishable crate must fail freeze"); + assert!(publishable.contains("publication freeze requires workspace package crate-a")); + + fs::write( + root.join("crates/a/Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\n", + ) + .expect("remove publish control"); + let implicit = + check_publication_policy(&root).expect_err("implicit publishability must fail freeze"); + assert!(implicit.contains("publication freeze requires workspace package crate-a")); + + fs::write( + root.join("crates/a/Cargo.toml"), + "[package]\nname = \"crate-a\"\nversion = \"0.1.0\"\npublish = false\n", + ) + .expect("restore private crate a"); + let mut unapproved = APPROVED_CRATES.to_vec(); + unapproved.push("unapproved-public"); + fs::write( + root.join("contracts/releases/publication.toml"), + publication_policy(true, &unapproved, &["crate-a", "crate-b"], &[]), + ) + .expect("write unapproved policy"); + let unapproved_error = check_publication_policy(&root) + .expect_err("unapproved public package must fail exact catalog validation"); + assert!(unapproved_error.contains("unapproved packages: unapproved-public")); + + fs::write( + root.join("contracts/releases/publication.toml"), + publication_policy(true, &APPROVED_CRATES, &["crate-a"], &[]), + ) + .expect("write unclassified policy"); + let unclassified = + check_publication_policy(&root).expect_err("unclassified workspace package must fail"); + assert!(unclassified.contains("workspace classification is missing packages: crate-b")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn publication_enablement_allows_only_the_two_approved_packages() { + let root = test_root("publication_enablement"); + fs::create_dir_all(root.join("contracts/releases")).expect("create contracts"); + write_publication_architecture(&root); + for member in ["sdk", "facade", "tool"] { + fs::create_dir_all(root.join("crates").join(member)).expect("create member"); + } + fs::write( + root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/sdk\", \"crates/facade\", \"crates/tool\"]\n", + ) + .expect("write workspace"); + fs::write( + root.join("contracts/releases/publication.toml"), + publication_policy(false, &APPROVED_CRATES, &[], &["build-tool"]), + ) + .expect("write policy"); + fs::write( + root.join("crates/sdk/Cargo.toml"), + "[package]\nname = \"radroots_sdk\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n", + ) + .expect("write SDK manifest"); + fs::write( + root.join("crates/facade/Cargo.toml"), + "[package]\nname = \"radroots\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n", + ) + .expect("write facade manifest"); + fs::write( + root.join("crates/tool/Cargo.toml"), + "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = false\n", + ) + .expect("write tool manifest"); + check_publication_policy(&root).expect("approved package staging should pass"); + + fs::write( + root.join("crates/tool/Cargo.toml"), + "[package]\nname = \"build-tool\"\nversion = \"0.1.0\"\npublish = [\"crates-io\"]\n", + ) + .expect("make tool publishable"); + let extra = check_publication_policy(&root) + .expect_err("non-approved publishable package must fail enablement"); + assert!(extra.contains("non-approved workspace package build-tool")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn reticulum_gate_allows_first_party_preview_dependency_only() { + let manifest = manifest_value( + r#" +[package] +name = "radroots_sdk" + +[features] +runtime = ["dep:radroots_transport_reticulum"] + +[dependencies] +radroots_transport_reticulum = { workspace = true, optional = true, default-features = false } +"#, + ); + + check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml") + .expect("first-party preview dependency is allowed"); + } + + #[test] + fn reticulum_gate_ignores_docs_rs_feature_selection() { + let manifest = manifest_value( + r#" +[package] +name = "radroots_sdk" + +[package.metadata.docs.rs] +features = ["nostr", "nip46"] + +[features] +nostr = [] +nip46 = ["nostr"] +"#, + ); + + check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml") + .expect("docs.rs feature arrays are metadata, not Cargo feature tables"); + } + + #[test] + fn reticulum_gate_rejects_workspace_package_alias_to_real_runtime() { + let manifest = manifest_value( + r#" +[workspace.dependencies] +rr_mesh = { package = "reticulum", version = "1" } +"#, + ); + + let error = check_manifest_reticulum_dependency_boundaries(&manifest, "Cargo.toml") + .expect_err("Reticulum package alias is rejected"); + + assert!(error.contains("reticulum")); + assert!(error.contains("workspace.dependencies")); + } + + #[test] + fn reticulum_gate_rejects_build_dependency_alias_to_python_binding() { + let manifest = manifest_value( + r#" +[package] +name = "radroots_sdk" + +[build-dependencies] +ffi_bridge = { package = "pyo3", version = "0.24" } +"#, + ); + + let error = + check_manifest_reticulum_dependency_boundaries(&manifest, "crates/sdk/Cargo.toml") + .expect_err("Python runtime binding package alias is rejected"); + + assert!(error.contains("pyo3")); + assert!(error.contains("build-dependencies")); + } + + #[test] + fn reticulum_gate_rejects_reticulum_feature_aliases_and_runtime_features() { + let alias_manifest = manifest_value( + r#" +[package] +name = "radroots_sdk" + +[features] +transport-reticulum-preview = [] +"#, + ); + let alias_error = check_manifest_reticulum_dependency_boundaries( + &alias_manifest, + "crates/sdk/Cargo.toml", + ) + .expect_err("Reticulum feature alias is rejected"); + + assert!(alias_error.contains("transport-reticulum-preview")); + + let entry_error = check_reticulum_feature_entry( + "radroots_transport_reticulum/client", + "crates/sdk/Cargo.toml", + "runtime", + ) + .expect_err("Reticulum preview crate runtime feature is rejected"); + + assert!(entry_error.contains("radroots_transport_reticulum/client")); + } + + #[test] + fn reticulum_gate_package_name_classifier_is_runtime_scoped() { + for name in [ + "reticulum", + "reticulum-rs", + "rns", + "rns-client", + "python", + "python-runtime", + "pyo3", + "pyo3-ffi", + "radroots-transport-reticulum", + ] { + assert!( + is_forbidden_reticulum_runtime_name(name), + "{name} must be forbidden" + ); + } + for name in ["radroots_transport_reticulum", "dto_bindgen_backend_python"] { + assert!( + !is_forbidden_reticulum_runtime_name(name), + "{name} must be allowed" + ); + } + } + + #[test] + fn rejects_crate_typescript_directories() { + let root = test_root("typescript_dir"); + let typescript_dir = root + .join("crates") + .join("core_bindings") + .join("src") + .join("typescript"); + fs::create_dir_all(&typescript_dir).expect("create forbidden directory"); + + let error = check_binding_crate_sources(&root).expect_err("forbidden directory rejected"); + + assert!(error.contains("forbidden crate TypeScript source directory")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_crate_typescript_files() { + let root = test_root("typescript_file"); + let src_dir = root.join("crates/core_bindings/src"); + fs::create_dir_all(&src_dir).expect("create crate source directory"); + fs::write(src_dir.join("types.ts"), "export type A = string;\n") + .expect("write forbidden file"); + + let error = check_no_typescript_files(&src_dir).expect_err("forbidden file rejected"); + + assert!(error.contains("forbidden crate TypeScript source file")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_generated_package_index_source() { + let root = test_root("generated_index"); + let path = root.join("src/index.ts"); + fs::create_dir_all(path.parent().expect("parent")).expect("create source directory"); + fs::write( + &path, + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nexport {};\n", + ) + .expect("write generated index"); + + let error = check_package_index(&path).expect_err("generated index rejected"); + + assert!(error.contains("package index must be handwritten source")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn generated_package_artifact_inventory_rejects_extra_files() { + let root = test_root("generated_inventory_extra"); + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "core") + .expect("core output"); + let package_dir = root.join(output.spec.package_dir); + fs::create_dir_all(package_dir.join("src/generated")).expect("create generated dir"); + for file in output.files() { + let path = package_dir.join(file.relative_path); + fs::write(path, file.contents).expect("write expected file"); + } + fs::write( + package_dir.join("src/generated").join("extra.ts"), + "export type Extra = string;\n", + ) + .expect("write extra file"); + + let error = check_generated_package_artifact_inventory(&root, &output) + .expect_err("extra generated file rejected"); + + assert!(error.contains("generated artifact inventory mismatch")); + assert!(error.contains("extra.ts")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_package_metadata_rejects_private_packages() { + let root = test_root("private_package_json"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(&package_dir).expect("create package"); + let package_json = package_json("example").replace( + r#""sideEffects": false,"#, + r#""private": true, "sideEffects": false,"#, + ); + fs::write(package_dir.join("package.json"), package_json).expect("write package json"); + + let error = check_package_json( + &package_dir.join("package.json"), + "@radroots/example", + "packages/example", + ) + .expect_err("private package rejected"); + + assert!(error.contains("must not set private")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_package_metadata_rejects_src_generated_payloads() { + let root = test_root("src_generated_package_payload"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(&package_dir).expect("create package"); + let package_json = package_json("example").replace( + r#""files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE"]"#, + r#""files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE", "src/generated"]"#, + ); + fs::write(package_dir.join("package.json"), package_json).expect("write package json"); + + let error = check_package_json( + &package_dir.join("package.json"), + "@radroots/example", + "packages/example", + ) + .expect_err("src generated package payload rejected"); + + assert!(error.contains("files must publish dist plus approved metadata only")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn public_package_metadata_rejects_pack_lifecycle_scripts() { + let root = test_root("pack_lifecycle_scripts"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(&package_dir).expect("create package"); + let package_json = package_json("example").replace( + r#""type": "module","#, + r#""scripts": {"prepack": "echo forbidden"}, "type": "module","#, + ); + fs::write(package_dir.join("package.json"), package_json).expect("write package json"); + + let error = check_package_json( + &package_dir.join("package.json"), + "@radroots/example", + "packages/example", + ) + .expect_err("pack lifecycle script rejected"); + + assert!(error.contains("script prepack is forbidden")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn package_distribution_metadata_matches_root_license_files() { + let root = test_root("package_distribution_metadata"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(&package_dir).expect("create package"); + fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license"); + fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license"); + fs::write(package_dir.join("package.json"), package_json("example")) + .expect("write package json"); + fs::write( + package_dir.join("README.md"), + package_readme("@radroots/example", "Example package"), + ) + .expect("write readme"); + fs::write(package_dir.join("LICENSE-MIT"), "MIT license\n") + .expect("write package MIT license"); + fs::write(package_dir.join("LICENSE-APACHE"), "Apache license\n") + .expect("write package Apache license"); + let json = check_package_json( + &package_dir.join("package.json"), + "@radroots/example", + "packages/example", + ) + .expect("valid package json"); + + check_package_distribution_metadata( + &root, + &package_dir, + &package_dir.join("package.json"), + &json, + ) + .expect("metadata matches"); + + fs::write(package_dir.join("README.md"), "stale\n").expect("stale readme"); + let error = check_package_distribution_metadata( + &root, + &package_dir, + &package_dir.join("package.json"), + &json, + ) + .expect_err("stale readme rejected"); + assert!(error.contains("stale package README")); + + fs::write( + package_dir.join("README.md"), + package_readme("@radroots/example", "Example package"), + ) + .expect("restore readme"); + fs::write(package_dir.join("LICENSE-MIT"), "stale\n").expect("stale license"); + let error = check_package_distribution_metadata( + &root, + &package_dir, + &package_dir.join("package.json"), + &json, + ) + .expect_err("stale license rejected"); + assert!(error.contains("stale package license metadata")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn package_surface_artifacts_require_dist_files() { + let root = test_root("package_surface_artifacts"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write(package_dir.join("dist").join("index.js"), "export {};\n").expect("write js"); + let surface_paths = + BTreeSet::from(["./dist/index.js".to_owned(), "./dist/index.d.ts".to_owned()]); + + let error = + check_package_surface_artifacts(&package_dir, "@radroots/example", &surface_paths) + .expect_err("missing declaration should fail"); + assert!(error.contains("missing package export artifact")); + assert!(error.contains("index.d.ts")); + + fs::write(package_dir.join("dist").join("index.d.ts"), "export {};\n").expect("write d.ts"); + check_package_surface_artifacts(&package_dir, "@radroots/example", &surface_paths) + .expect("surface artifacts present"); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn binding_dist_inventory_includes_generated_js_and_declarations() { + let root = test_root("binding_dist_inventory"); + let package_dir = root.join("packages").join("example"); + fs::create_dir_all(package_dir.join("src/generated")).expect("create generated dir"); + fs::write( + package_dir.join("src/generated").join("types.ts"), + "export type Example = string;\n", + ) + .expect("write generated types"); + fs::write( + package_dir.join("src/generated").join("constants.ts"), + "export const EXAMPLE = \"example\";\n", + ) + .expect("write generated constants"); + let json = package_json_value(&package_json("example")); + + let expected = expected_packed_dist_files( + &package_dir, + &json, + Path::new("packages/example/package.json"), + None, + ) + .expect("expected dist files"); + + assert_eq!( + expected, + BTreeSet::from([ + "dist/generated/constants.d.ts".to_owned(), + "dist/generated/constants.js".to_owned(), + "dist/generated/types.d.ts".to_owned(), + "dist/generated/types.js".to_owned(), + "dist/index.d.ts".to_owned(), + "dist/index.js".to_owned(), + ]) + ); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn npm_pack_payload_requires_metadata_and_export_files() { + let payload_files = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "dist/index.js".to_owned(), + ]); + let required_files = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "LICENSE-APACHE".to_owned(), + "dist/index.js".to_owned(), + "dist/index.d.ts".to_owned(), + ]); + + let error = + validate_npm_pack_payload("@radroots/example", &payload_files, &required_files, None) + .expect_err("missing files should fail"); + assert!(error.contains("missing required files")); + assert!(error.contains("LICENSE-APACHE")); + assert!(error.contains("dist/index.d.ts")); + } + + #[test] + fn packed_dist_inventory_rejects_missing_generated_files() { + let expected_dist_files = BTreeSet::from([ + "dist/generated/types.d.ts".to_owned(), + "dist/generated/types.js".to_owned(), + "dist/index.d.ts".to_owned(), + "dist/index.js".to_owned(), + ]); + let payload_files = BTreeSet::from([ + "package.json".to_owned(), + "README.md".to_owned(), + "LICENSE-MIT".to_owned(), + "LICENSE-APACHE".to_owned(), + "dist/generated/types.js".to_owned(), + "dist/index.d.ts".to_owned(), + "dist/index.js".to_owned(), + ]); + + let error = validate_packed_dist_inventory( + "@radroots/example", + &payload_files, + &expected_dist_files, + ) + .expect_err("missing generated declaration rejected"); + + assert!(error.contains("dist inventory mismatch")); + assert!(error.contains("dist/generated/types.d.ts")); + } + + #[test] + fn packed_payload_path_normalization_rejects_invalid_paths() { + assert_eq!( + normalized_package_path("./dist/index.js").expect("valid path"), + "dist/index.js" + ); + for invalid in [ + "", + "./", + "/dist/index.js", + "dist\\index.js", + "dist/../index.js", + "dist//index.js", + "dist/./index.js", + ] { + let error = normalized_package_path(invalid).expect_err("invalid path rejected"); + assert!(error.contains("invalid npm pack payload path")); + } + } + + #[test] + fn consumer_smoke_script_imports_package_roots() { + let script = consumer_smoke_script(&[ + "@radroots/core-bindings".to_owned(), + "@radroots/event-codec-wasm".to_owned(), + ]) + .expect("smoke script renders"); + + assert_eq!( + script, + "await import(\"@radroots/core-bindings\");\nawait import(\"@radroots/event-codec-wasm\");\n" + ); + } + + #[test] + fn pnpm_pack_json_parser_accepts_single_package_object() { + let entry = parse_pnpm_pack_entry( + "@radroots/example", + br#"{ + "name": "@radroots/example", + "version": "0.1.0", + "filename": "/tmp/example.tgz", + "files": [ + {"path": "dist/index.js"}, + {"path": "package.json"} + ] +}"#, + b"", + ) + .expect("pnpm pack output parses"); + + assert_eq!(entry.filename, "/tmp/example.tgz"); + assert_eq!( + entry + .files + .into_iter() + .map(|file| file.path) + .collect::<Vec<_>>(), + ["dist/index.js", "package.json"] + ); + } + + #[test] + fn reads_packed_manifest_from_tgz() { + let root = test_root("packed_manifest_tgz"); + fs::create_dir_all(&root).expect("create root"); + let tarball_path = root.join("example.tgz"); + let file = fs::File::create(&tarball_path).expect("create tarball"); + let encoder = GzEncoder::new(file, Compression::default()); + let mut builder = tar::Builder::new(encoder); + let contents = br#"{"name":"@radroots/example","version":"0.1.0"}"#; + let mut header = tar::Header::new_gnu(); + header + .set_path("package/package.json") + .expect("set manifest path"); + header.set_size(contents.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder + .append(&header, &contents[..]) + .expect("append manifest"); + let encoder = builder.into_inner().expect("finish tar"); + encoder.finish().expect("finish gzip"); + let packed = PackedPackage { + package_name: "@radroots/example".to_owned(), + tarball_path, + files: Vec::new(), + }; + + let json = + read_packed_package_json(&packed, "@radroots/example").expect("read packed manifest"); + + assert_eq!( + json.get("name").and_then(serde_json::Value::as_str), + Some("@radroots/example") + ); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn packed_manifest_accepts_pnpm_workspace_dependency_rewrite() { + let source = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "workspace:^", + "@radroots/event-bindings": "workspace:^""#, + )); + let packed = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "^0.1.0", + "@radroots/event-bindings": "^0.1.0""#, + )); + + check_packed_package_json( + &source, + &packed, + Path::new("packages/trade-bindings/package.json"), + "@radroots/trade-bindings", + "packages/trade-bindings", + ) + .expect("packed manifest accepted"); + } + + #[test] + fn packed_manifest_rejects_workspace_dependency_ranges() { + let source = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "workspace:^""#, + )); + let packed = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "workspace:^""#, + )); + + let error = check_packed_package_json( + &source, + &packed, + Path::new("packages/trade-bindings/package.json"), + "@radroots/trade-bindings", + "packages/trade-bindings", + ) + .expect_err("workspace dependency rejected"); + + assert!(error.contains("must not use workspace protocol")); + } + + #[test] + fn packed_manifest_rejects_internal_dependency_range_mismatch() { + let source = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "workspace:^""#, + )); + let packed = package_json_value(&package_json_with_dependencies( + "trade-bindings", + r#""@radroots/core-bindings": "^0.2.0""#, + )); + + let error = check_packed_package_json( + &source, + &packed, + Path::new("packages/trade-bindings/package.json"), + "@radroots/trade-bindings", + "packages/trade-bindings", + ) + .expect_err("internal dependency mismatch rejected"); + + assert!(error.contains("expected ^0.1.0")); + } + + #[test] + fn npm_pack_payload_rejects_source_and_provenance_internals() { + let required_files = BTreeSet::from(["package.json".to_owned()]); + for forbidden in [ + "src/generated/types.ts", + "contracts/provenance/typescript/core.json", + "dist/sdk-manifest.json", + "dist/index.tsbuildinfo", + "dist/.gitignore", + ".npmignore", + ] { + let payload_files = BTreeSet::from(["package.json".to_owned(), forbidden.to_owned()]); + + let error = validate_npm_pack_payload( + "@radroots/example", + &payload_files, + &required_files, + None, + ) + .expect_err("forbidden payload path should fail"); + assert!(error.contains("includes forbidden")); + assert!(error.contains(forbidden)); + } + } + + #[test] + fn npm_pack_payload_requires_exact_wasm_file() { + let required_files = BTreeSet::from([ + "package.json".to_owned(), + "dist/example.js".to_owned(), + "dist/example_bg.wasm".to_owned(), + ]); + let missing_wasm = + BTreeSet::from(["package.json".to_owned(), "dist/example.js".to_owned()]); + let error = validate_npm_pack_payload( + "@radroots/example-wasm", + &missing_wasm, + &required_files, + Some("dist/example_bg.wasm"), + ) + .expect_err("missing wasm should fail"); + assert!(error.contains("missing required files")); + + let extra_wasm = BTreeSet::from([ + "package.json".to_owned(), + "dist/example.js".to_owned(), + "dist/example_bg.wasm".to_owned(), + "dist/extra_bg.wasm".to_owned(), + ]); + let error = validate_npm_pack_payload( + "@radroots/example-wasm", + &extra_wasm, + &required_files, + Some("dist/example_bg.wasm"), + ) + .expect_err("extra wasm should fail"); + assert!(error.contains("must include exactly dist/example_bg.wasm")); + } + + #[test] + fn wasm_package_surface_requires_exported_dist_files() { + let root = test_root("wasm_surface"); + let package_dir = root.join("packages").join("example-wasm"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write( + package_dir.join("package.json"), + package_json("example-wasm").replace("./dist/index", "./dist/example"), + ) + .expect("write package json"); + write_distribution_metadata( + &root, + &package_dir, + "@radroots/example-wasm", + "Example package", + ); + fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js"); + let spec = WasmPackageSpec { + key: "example", + crate_name: "radroots_example_wasm", + crate_dir: "crates/example_wasm", + package_name: "@radroots/example-wasm", + package_dir: "packages/example-wasm", + out_name: "example", + out_dir: "../../packages/example-wasm/dist", + }; + + let missing = + check_wasm_package_surface(&root, spec).expect_err("missing d.ts should fail"); + assert!(missing.contains("example.d.ts")); + fs::write( + package_dir.join("dist").join("example.d.ts"), + dto_declaration("example.d.ts"), + ) + .expect("write d.ts"); + fs::write( + package_dir.join("dist").join("example_bg.wasm.d.ts"), + dto_declaration("example_bg.wasm.d.ts"), + ) + .expect("write d.ts"); + fs::write(package_dir.join("dist").join("example_bg.wasm"), b"\0asm").expect("write wasm"); + let error = check_wasm_package_surface(&root, spec) + .expect_err("unknown declaration inventory rejected"); + assert!(error.contains("missing wasm declaration inventory")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn wasm_package_surface_rejects_generated_package_manifest() { + let root = test_root("wasm_dist_package_manifest"); + let package_dir = root.join("packages").join("example-wasm"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write( + package_dir.join("package.json"), + package_json("example-wasm").replace("./dist/index", "./dist/example"), + ) + .expect("write package json"); + write_distribution_metadata( + &root, + &package_dir, + "@radroots/example-wasm", + "Example package", + ); + fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js"); + fs::write( + package_dir.join("dist").join("example.d.ts"), + dto_declaration("example.d.ts"), + ) + .expect("write d.ts"); + fs::write( + package_dir.join("dist").join("example_bg.wasm.d.ts"), + dto_declaration("example_bg.wasm.d.ts"), + ) + .expect("write wasm d.ts"); + fs::write(package_dir.join("dist").join("package.json"), "{}\n") + .expect("write forbidden manifest"); + let spec = WasmPackageSpec { + key: "example", + crate_name: "radroots_example_wasm", + crate_dir: "crates/example_wasm", + package_name: "@radroots/example-wasm", + package_dir: "packages/example-wasm", + out_name: "example", + out_dir: "../../packages/example-wasm/dist", + }; + + let error = + check_wasm_package_surface(&root, spec).expect_err("dist package manifest rejected"); + assert!(error.contains("generated package manifest is forbidden")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn wasm_package_surface_rejects_dist_ignore_files() { + let root = test_root("wasm_dist_ignore"); + let package_dir = root.join("packages").join("example-wasm"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write( + package_dir.join("package.json"), + package_json("example-wasm").replace("./dist/index", "./dist/example"), + ) + .expect("write package json"); + write_distribution_metadata( + &root, + &package_dir, + "@radroots/example-wasm", + "Example package", + ); + fs::write(package_dir.join("dist").join(".gitignore"), "*\n").expect("write ignore"); + let spec = WasmPackageSpec { + key: "example", + crate_name: "radroots_example_wasm", + crate_dir: "crates/example_wasm", + package_name: "@radroots/example-wasm", + package_dir: "packages/example-wasm", + out_name: "example", + out_dir: "../../packages/example-wasm/dist", + }; + + let error = check_wasm_package_surface(&root, spec).expect_err("dist ignore file rejected"); + assert!(error.contains("would hide package payload")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn wasm_package_surface_requires_runtime_wasm_artifact() { + let root = test_root("wasm_runtime_artifact"); + let package_dir = root.join("packages").join("example-wasm"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write( + package_dir.join("package.json"), + package_json("example-wasm").replace("./dist/index", "./dist/example"), + ) + .expect("write package json"); + write_distribution_metadata( + &root, + &package_dir, + "@radroots/example-wasm", + "Example package", + ); + fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js"); + fs::write( + package_dir.join("dist").join("example.d.ts"), + dto_declaration("example.d.ts"), + ) + .expect("write d.ts"); + fs::write( + package_dir.join("dist").join("example_bg.wasm.d.ts"), + dto_declaration("example_bg.wasm.d.ts"), + ) + .expect("write wasm d.ts"); + let spec = WasmPackageSpec { + key: "example", + crate_name: "radroots_example_wasm", + crate_dir: "crates/example_wasm", + package_name: "@radroots/example-wasm", + package_dir: "packages/example-wasm", + out_name: "example", + out_dir: "../../packages/example-wasm/dist", + }; + + let error = check_wasm_package_surface(&root, spec).expect_err("missing wasm rejected"); + assert!(error.contains("missing wasm package runtime artifact")); + + let _ = fs::remove_dir_all(root); + } + + #[test] + fn wasm_package_surface_rejects_subpath_exports() { + let root = test_root("wasm_subpath_exports"); + let package_dir = root.join("packages").join("example-wasm"); + fs::create_dir_all(package_dir.join("dist")).expect("create dist"); + fs::write( + package_dir.join("package.json"), + package_json("example-wasm") + .replace("./dist/index", "./dist/example") + .replace( + r#""exports": { + ".": { + "types": "./dist/example.d.ts", + "import": "./dist/example.js", + "default": "./dist/example.js" + } + }"#, + r#""exports": { + ".": "./dist/example.js", + "./extra": "./dist/extra.js" + }"#, + ), + ) + .expect("write package json"); + write_distribution_metadata( + &root, + &package_dir, + "@radroots/example-wasm", + "Example package", + ); + fs::write(package_dir.join("dist").join("example.js"), "export {};\n").expect("write js"); + fs::write( + package_dir.join("dist").join("example.d.ts"), + dto_declaration("example.d.ts"), + ) + .expect("write d.ts"); + fs::write( + package_dir.join("dist").join("example_bg.wasm.d.ts"), + dto_declaration("example_bg.wasm.d.ts"), + ) + .expect("write wasm d.ts"); + let spec = WasmPackageSpec { + key: "example", + crate_name: "radroots_example_wasm", + crate_dir: "crates/example_wasm", + package_name: "@radroots/example-wasm", + package_dir: "packages/example-wasm", + out_name: "example", + out_dir: "../../packages/example-wasm/dist", + }; + + let error = check_wasm_package_surface(&root, spec).expect_err("subpath export rejected"); + assert!(error.contains("only supports root exports")); + + let _ = fs::remove_dir_all(root); + } + + fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time after epoch") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "radroots_sdk_xtask_check_{name}_{}_{}", + std::process::id(), + stamp + )); + let _ = fs::remove_dir_all(&root); + root + } + + fn dto_declaration(name: &str) -> String { + format!( + "// @generated by cargo xtask generate wasm via dto_bindgen\n// Do not edit by hand.\nexport type Generated = \"{name}\";\n" + ) + } + + fn write_distribution_metadata( + root: &Path, + package_dir: &Path, + package_name: &str, + description: &str, + ) { + fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license"); + fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license"); + fs::write(package_dir.join("LICENSE-MIT"), "MIT license\n") + .expect("write package MIT license"); + fs::write(package_dir.join("LICENSE-APACHE"), "Apache license\n") + .expect("write package Apache license"); + fs::write( + package_dir.join("README.md"), + package_readme(package_name, description), + ) + .expect("write package README"); + } + + fn package_json(name: &str) -> String { + format!( + r#"{{ + "name": "@radroots/{name}", + "version": "0.1.0", + "description": "Example package", + "license": "MIT OR Apache-2.0", + "homepage": "https://radroots.org", + "repository": {{ + "type": "git", + "url": "git+https://github.com/radrootslabs/sdk.git", + "directory": "packages/{name}" + }}, + "publishConfig": {{ + "access": "public" + }}, + "type": "module", + "sideEffects": false, + "files": ["dist", "README.md", "LICENSE-MIT", "LICENSE-APACHE"], + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": {{ + ".": {{ + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }} + }} +}}"# + ) + } + + fn package_json_with_dependencies(name: &str, dependencies: &str) -> String { + let raw = package_json(name); + let body = raw.strip_suffix('}').expect("root package JSON object"); + format!( + r#"{body}, + "dependencies": {{ + {dependencies} + }} +}}"# + ) + } + + fn package_json_value(raw: &str) -> serde_json::Value { + serde_json::from_str(raw).expect("package json parses") + } + + fn manifest_value(raw: &str) -> toml::Value { + raw.parse::<toml::Value>().expect("manifest parses") + } +} diff --git a/tools/sdk_xtask_import/src/contracts.rs b/tools/sdk_xtask_import/src/contracts.rs @@ -0,0 +1,694 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Path, PathBuf}, +}; + +use serde::Deserialize; + +use crate::package_matrix::{PackageSpec, WasmPackageSpec, package_specs, wasm_package_specs}; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExportContract { + language: LanguageContract, + packages: BTreeMap<String, String>, + artifacts: Option<ExportArtifacts>, + runtime: RuntimeContract, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct PackageContract { + language: LanguageContract, + sdk: SdkPackageContract, + rollout: RolloutContract, + operations: BTreeMap<String, String>, + shared_types: BTreeMap<String, String>, + artifacts: Option<SdkArtifacts>, + npm_packages: Option<BTreeMap<String, NpmPackageContract>>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LanguageContract { + id: String, + repository: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RuntimeContract { + networking: String, + signing: String, + deterministic_codec: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ExportArtifacts { + models_dir: String, + constants_dir: String, + wasm_dist_dir: Option<String>, + manifest_file: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SdkPackageContract { + package: Option<String>, + package_family: Option<String>, + module_format: Option<String>, + deterministic_codec: String, + signing: String, + networking: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct NpmPackageContract { + kind: String, + crate_name: String, + crate_dir: String, + package: String, + package_dir: String, + out_name: Option<String>, + out_dir: Option<String>, +} + +#[derive(Debug)] +struct ExpectedNpmPackage { + kind: &'static str, + crate_name: &'static str, + crate_dir: &'static str, + package: &'static str, + package_dir: &'static str, + out_name: Option<&'static str>, + out_dir: Option<&'static str>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RolloutContract { + stage: String, + order: u32, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SdkArtifacts { + models_dir: String, + runtime_dir: String, + wasm_dist_dir: String, + manifest_file: String, +} + +pub fn validate_sdk_contracts(root: &Path) -> Result<(), String> { + radroots_protocol::runtime::v1::validate_catalog(radroots_protocol::runtime::v1::CATALOG) + .map_err(|error| error.to_string())?; + let exports = load_contract_dir::<ExportContract>(&root.join("contracts").join("exports"))?; + let packages = load_contract_dir::<PackageContract>(&root.join("contracts").join("packages"))?; + if exports.is_empty() { + return Err("contracts/exports must define at least one language".to_owned()); + } + if packages.is_empty() { + return Err("contracts/packages must define at least one language".to_owned()); + } + + let mut export_packages = BTreeMap::new(); + let mut export_languages = BTreeSet::new(); + for export in &exports { + validate_language(&export.language, "exports")?; + validate_non_empty_map(&export.packages, "exports packages")?; + validate_runtime( + &export.runtime.networking, + &export.runtime.signing, + &export.runtime.deterministic_codec, + &format!("exports {}", export.language.id), + )?; + let artifacts = export + .artifacts + .as_ref() + .ok_or_else(|| format!("exports {} artifacts are required", export.language.id))?; + validate_non_empty(&artifacts.models_dir, "exports artifacts.models_dir")?; + validate_non_empty(&artifacts.constants_dir, "exports artifacts.constants_dir")?; + validate_non_empty(&artifacts.manifest_file, "exports artifacts.manifest_file")?; + if export.language.id == "ts" { + validate_non_empty( + artifacts.wasm_dist_dir.as_deref().unwrap_or(""), + "exports ts artifacts.wasm_dist_dir", + )?; + } + if !export_languages.insert(export.language.id.clone()) { + return Err(format!("duplicate exports language {}", export.language.id)); + } + let packages = export + .packages + .values() + .cloned() + .collect::<BTreeSet<String>>(); + if export.language.id != "ts" && packages.len() != 1 { + return Err(format!( + "exports {} must resolve to one curated package", + export.language.id + )); + } + export_packages.insert(export.language.id.clone(), packages); + } + + let mut package_languages = BTreeSet::new(); + let mut operation_keys: Option<BTreeSet<String>> = None; + let mut shared_type_keys: Option<BTreeSet<String>> = None; + let mut rollout_orders = BTreeMap::new(); + for package in &packages { + validate_language(&package.language, "packages")?; + if let Some(package_name) = package.sdk.package.as_deref() { + validate_non_empty(package_name, "packages sdk.package")?; + } + if let Some(package_family) = package.sdk.package_family.as_deref() { + validate_non_empty(package_family, "packages sdk.package_family")?; + } + validate_runtime( + &package.sdk.networking, + &package.sdk.signing, + &package.sdk.deterministic_codec, + &format!("packages {}", package.language.id), + )?; + if let Some(module_format) = package.sdk.module_format.as_deref() { + validate_non_empty(module_format, "packages sdk.module_format")?; + } + if package.sdk.package.is_none() && package.sdk.package_family.is_none() { + return Err(format!( + "packages {} sdk.package or sdk.package_family is required", + package.language.id + )); + } + validate_rollout(&package.language.id, &package.rollout)?; + validate_non_empty_map(&package.operations, "packages operations")?; + validate_non_empty_map(&package.shared_types, "packages shared_types")?; + if package.language.id == "ts" { + let artifacts = package + .artifacts + .as_ref() + .ok_or_else(|| "packages ts artifacts are required".to_owned())?; + validate_non_empty(&artifacts.models_dir, "packages ts artifacts.models_dir")?; + validate_non_empty(&artifacts.runtime_dir, "packages ts artifacts.runtime_dir")?; + validate_non_empty( + &artifacts.wasm_dist_dir, + "packages ts artifacts.wasm_dist_dir", + )?; + validate_non_empty( + &artifacts.manifest_file, + "packages ts artifacts.manifest_file", + )?; + } + if !package_languages.insert(package.language.id.clone()) { + return Err(format!( + "duplicate packages language {}", + package.language.id + )); + } + let Some(packages_for_language) = export_packages.get(&package.language.id) else { + return Err(format!( + "packages {} is missing a matching export contract", + package.language.id + )); + }; + if package.language.id == "ts" { + let npm_packages = package + .npm_packages + .as_ref() + .ok_or_else(|| "packages ts npm_packages are required".to_owned())?; + validate_ts_npm_packages(package, packages_for_language, npm_packages)?; + } else { + if package.npm_packages.is_some() { + return Err(format!( + "packages {} npm_packages is only supported for ts", + package.language.id + )); + } + let sdk_package = package.sdk.package.as_ref().ok_or_else(|| { + format!("packages {} sdk.package is required", package.language.id) + })?; + let expected = [sdk_package.clone()].into_iter().collect::<BTreeSet<_>>(); + if packages_for_language != &expected { + return Err(format!( + "exports {} must resolve to package {}", + package.language.id, sdk_package + )); + } + } + let current_operations = package.operations.keys().cloned().collect::<BTreeSet<_>>(); + match &operation_keys { + Some(expected) if expected != &current_operations => { + return Err(format!( + "packages {} operations must match the shared operation set", + package.language.id + )); + } + None => operation_keys = Some(current_operations), + _ => {} + } + let current_shared_types = package + .shared_types + .keys() + .cloned() + .collect::<BTreeSet<_>>(); + match &shared_type_keys { + Some(expected) if expected != &current_shared_types => { + return Err(format!( + "packages {} shared_types must match the shared type set", + package.language.id + )); + } + None => shared_type_keys = Some(current_shared_types), + _ => {} + } + rollout_orders.insert(package.language.id.clone(), package.rollout.order); + } + + if export_languages != package_languages { + return Err("contracts/exports and contracts/packages languages must match".to_owned()); + } + if rollout_orders.get("ts") != Some(&1) { + return Err("packages ts rollout.order must be 1".to_owned()); + } + Ok(()) +} + +fn validate_ts_npm_packages( + package: &PackageContract, + export_packages: &BTreeSet<String>, + npm_packages: &BTreeMap<String, NpmPackageContract>, +) -> Result<(), String> { + if package.sdk.package.is_some() { + return Err( + "packages ts sdk.package must not be set for multi-package npm output".to_owned(), + ); + } + let package_family = package + .sdk + .package_family + .as_deref() + .ok_or_else(|| "packages ts sdk.package_family is required".to_owned())?; + if package_family != "@radroots" { + return Err("packages ts sdk.package_family must be @radroots".to_owned()); + } + let expected = expected_ts_npm_packages(); + let expected_keys = expected.keys().cloned().collect::<BTreeSet<_>>(); + let actual_keys = npm_packages.keys().cloned().collect::<BTreeSet<_>>(); + if actual_keys != expected_keys { + let missing = expected_keys + .difference(&actual_keys) + .cloned() + .collect::<Vec<_>>(); + let extra = actual_keys + .difference(&expected_keys) + .cloned() + .collect::<Vec<_>>(); + return Err(format!( + "packages ts npm_packages must match package matrix: missing {:?}, extra {:?}", + missing, extra + )); + } + for (key, expected_package) in expected { + let actual = npm_packages + .get(&key) + .ok_or_else(|| format!("packages ts npm package {key} is missing"))?; + validate_npm_package(&key, actual, &expected_package)?; + } + let expected_names = npm_packages + .values() + .map(|package| package.package.clone()) + .collect::<BTreeSet<_>>(); + if export_packages != &expected_names { + return Err("exports ts package set must match TypeScript npm package matrix".to_owned()); + } + Ok(()) +} + +fn expected_ts_npm_packages() -> BTreeMap<String, ExpectedNpmPackage> { + let mut expected = BTreeMap::new(); + for spec in package_specs() { + expected.insert(spec.key.to_owned(), expected_from_package_spec(*spec)); + } + for spec in wasm_package_specs() { + expected.insert(spec.key.to_owned(), expected_from_wasm_package_spec(*spec)); + } + expected +} + +fn expected_from_package_spec(spec: PackageSpec) -> ExpectedNpmPackage { + ExpectedNpmPackage { + kind: "bindings", + crate_name: spec.crate_name, + crate_dir: spec.crate_dir, + package: spec.package_name, + package_dir: spec.package_dir, + out_name: None, + out_dir: None, + } +} + +fn expected_from_wasm_package_spec(spec: WasmPackageSpec) -> ExpectedNpmPackage { + ExpectedNpmPackage { + kind: "wasm", + crate_name: spec.crate_name, + crate_dir: spec.crate_dir, + package: spec.package_name, + package_dir: spec.package_dir, + out_name: Some(spec.out_name), + out_dir: Some(spec.out_dir), + } +} + +fn validate_npm_package( + key: &str, + actual: &NpmPackageContract, + expected: &ExpectedNpmPackage, +) -> Result<(), String> { + validate_npm_field(key, "kind", &actual.kind, expected.kind)?; + validate_npm_field(key, "crate_name", &actual.crate_name, expected.crate_name)?; + validate_npm_field(key, "crate_dir", &actual.crate_dir, expected.crate_dir)?; + validate_npm_field(key, "package", &actual.package, expected.package)?; + validate_npm_field( + key, + "package_dir", + &actual.package_dir, + expected.package_dir, + )?; + validate_optional_npm_field( + key, + "out_name", + actual.out_name.as_deref(), + expected.out_name, + )?; + validate_optional_npm_field(key, "out_dir", actual.out_dir.as_deref(), expected.out_dir)?; + Ok(()) +} + +fn validate_npm_field(key: &str, field: &str, actual: &str, expected: &str) -> Result<(), String> { + validate_non_empty(actual, &format!("packages ts npm package {key} {field}"))?; + if actual != expected { + return Err(format!( + "packages ts npm package {key} {field} must be {expected}" + )); + } + Ok(()) +} + +fn validate_optional_npm_field( + key: &str, + field: &str, + actual: Option<&str>, + expected: Option<&str>, +) -> Result<(), String> { + match (actual, expected) { + (Some(actual), Some(expected)) => validate_npm_field(key, field, actual, expected), + (None, None) => Ok(()), + (Some(_), None) => Err(format!( + "packages ts npm package {key} {field} must not be set" + )), + (None, Some(expected)) => Err(format!( + "packages ts npm package {key} {field} must be {expected}" + )), + } +} + +fn load_contract_dir<T>(dir: &Path) -> Result<Vec<T>, String> +where + T: for<'de> Deserialize<'de>, +{ + let read_dir = + fs::read_dir(dir).map_err(|error| format!("failed to read {}: {error}", dir.display()))?; + let mut entries = read_dir + .collect::<Result<Vec<_>, _>>() + .map_err(|error| format!("failed to read {} entry: {error}", dir.display()))?; + entries.sort_by_key(|entry| entry.file_name()); + let mut contracts = Vec::new(); + for entry in entries { + let path = entry.path(); + if path.extension().and_then(|extension| extension.to_str()) != Some("toml") { + continue; + } + contracts.push(parse_toml(&path)?); + } + Ok(contracts) +} + +fn parse_toml<T>(path: &PathBuf) -> Result<T, String> +where + T: for<'de> Deserialize<'de>, +{ + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn validate_language(language: &LanguageContract, family: &str) -> Result<(), String> { + validate_non_empty(&language.id, &format!("{family} language.id"))?; + validate_non_empty( + &language.repository, + &format!("{family} language.repository"), + ) +} + +fn validate_runtime( + networking: &str, + signing: &str, + deterministic_codec: &str, + family: &str, +) -> Result<(), String> { + validate_non_empty(networking, &format!("{family} networking"))?; + validate_non_empty(signing, &format!("{family} signing"))?; + validate_non_empty( + deterministic_codec, + &format!("{family} deterministic_codec"), + ) +} + +fn validate_rollout(language: &str, rollout: &RolloutContract) -> Result<(), String> { + validate_non_empty(&rollout.stage, "packages rollout.stage")?; + if !matches!(rollout.stage.as_str(), "active" | "next" | "deferred") { + return Err(format!("packages {language} rollout.stage is invalid")); + } + if rollout.order == 0 { + return Err(format!( + "packages {language} rollout.order must be greater than zero" + )); + } + Ok(()) +} + +fn validate_non_empty(value: &str, field: &str) -> Result<(), String> { + if value.trim().is_empty() || value.trim() != value { + return Err(format!("{field} must be non-empty")); + } + Ok(()) +} + +fn validate_non_empty_map(map: &BTreeMap<String, String>, field: &str) -> Result<(), String> { + if map.is_empty() { + return Err(format!("{field} must not be empty")); + } + for (key, value) in map { + validate_non_empty(key, field)?; + validate_non_empty(value, field)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::validate_sdk_contracts; + + #[test] + fn current_sdk_contracts_validate() { + let root = crate::fs::workspace_root().expect("workspace root"); + validate_sdk_contracts(&root).expect("sdk contracts validate"); + } + + #[test] + fn rejects_mismatched_language_sets() { + let root = test_root("language_mismatch"); + write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS); + let error = validate_sdk_contracts(&root).expect_err("missing packages should fail"); + assert!(error.contains("contracts/packages")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_package_export_mismatch() { + let root = test_root("package_mismatch"); + write_contract( + &root, + "contracts/exports/ts.toml", + EXPORT_TS + .replace("@radroots/core-bindings", "@radroots/other") + .as_str(), + ); + write_contract(&root, "contracts/packages/ts.toml", PACKAGE_TS); + let error = validate_sdk_contracts(&root).expect_err("mismatch should fail"); + assert!(error.contains("exports ts package set")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn rejects_ts_package_matrix_drift() { + let root = test_root("ts_matrix_drift"); + write_contract(&root, "contracts/exports/ts.toml", EXPORT_TS); + write_contract( + &root, + "contracts/packages/ts.toml", + PACKAGE_TS + .replace( + "package = \"@radroots/core-bindings\"", + "package = \"@radroots/other\"", + ) + .as_str(), + ); + let error = validate_sdk_contracts(&root).expect_err("mismatch should fail"); + assert!(error.contains("packages ts npm package core package")); + let _ = fs::remove_dir_all(root); + } + + fn test_root(name: &str) -> std::path::PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_sdk_contracts_{name}_{stamp}")) + } + + fn write_contract(root: &std::path::Path, relative: &str, contents: &str) { + let path = root.join(relative); + fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); + fs::write(path, contents).expect("write contract"); + } + + const EXPORT_TS: &str = r#"[language] +id = "ts" +repository = "sdk-typescript" + +[packages] +"radroots_core" = "@radroots/core-bindings" +"radroots_event" = "@radroots/event-bindings" +"radroots_identity" = "@radroots/identity-bindings" +"radroots_replica_schema" = "@radroots/replica-schema-bindings" +"radroots_trade" = "@radroots/trade-bindings" +"radroots_event_codec_wasm" = "@radroots/event-codec-wasm" +"radroots_replica_store_wasm" = "@radroots/replica-store-wasm" +"radroots_replica_sync_wasm" = "@radroots/replica-sync-wasm" + +[artifacts] +models_dir = "src/generated" +constants_dir = "src/generated" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" + +[runtime] +networking = "native" +signing = "native" +deterministic_codec = "wasm" +"#; + + const PACKAGE_TS: &str = r#"[language] +id = "ts" +repository = "sdk-typescript" + +[sdk] +package_family = "@radroots" +module_format = "esm" +deterministic_codec = "wasm" +signing = "native" +networking = "native" + +[npm_packages.core] +kind = "bindings" +crate_name = "radroots_core_bindings" +crate_dir = "crates/core_bindings" +package = "@radroots/core-bindings" +package_dir = "packages/core-bindings" + +[npm_packages.event] +kind = "bindings" +crate_name = "radroots_event_bindings" +crate_dir = "crates/event_bindings" +package = "@radroots/event-bindings" +package_dir = "packages/event-bindings" + +[npm_packages.identity] +kind = "bindings" +crate_name = "radroots_identity_bindings" +crate_dir = "crates/identity_bindings" +package = "@radroots/identity-bindings" +package_dir = "packages/identity-bindings" + +[npm_packages.replica_schema] +kind = "bindings" +crate_name = "radroots_replica_schema_bindings" +crate_dir = "crates/replica_schema_bindings" +package = "@radroots/replica-schema-bindings" +package_dir = "packages/replica-schema-bindings" + +[npm_packages.trade] +kind = "bindings" +crate_name = "radroots_trade_bindings" +crate_dir = "crates/trade_bindings" +package = "@radroots/trade-bindings" +package_dir = "packages/trade-bindings" + +[npm_packages.event_codec] +kind = "wasm" +crate_name = "radroots_event_codec_wasm" +crate_dir = "crates/event_codec_wasm" +package = "@radroots/event-codec-wasm" +package_dir = "packages/event-codec-wasm" +out_name = "radroots_event_codec_wasm" +out_dir = "../../packages/event-codec-wasm/dist" + +[npm_packages.replica_store] +kind = "wasm" +crate_name = "radroots_replica_store_wasm" +crate_dir = "crates/replica_store_wasm" +package = "@radroots/replica-store-wasm" +package_dir = "packages/replica-store-wasm" +out_name = "radroots_replica_store_wasm" +out_dir = "../../packages/replica-store-wasm/dist" + +[npm_packages.replica_sync] +kind = "wasm" +crate_name = "radroots_replica_sync_wasm" +crate_dir = "crates/replica_sync_wasm" +package = "@radroots/replica-sync-wasm" +package_dir = "packages/replica-sync-wasm" +out_name = "radroots_replica_sync_wasm" +out_dir = "../../packages/replica-sync-wasm/dist" + +[rollout] +stage = "active" +order = 1 + +[operations] +"farm.build_draft" = "farm.buildDraft" + +[shared_types] +"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts" +"RadrootsNip01EventWireDto" = "RadrootsNip01EventWireDto" +"RadrootsEventDraft" = "RadrootsEventDraft" +"RadrootsSignedEventDto" = "RadrootsSignedEventDto" +"RadrootsVerifiedSignedEventDto" = "RadrootsVerifiedSignedEventDto" +"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto" + +[artifacts] +models_dir = "src/generated" +runtime_dir = "src/runtime" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +"#; +} diff --git a/tools/sdk_xtask_import/src/coverage.rs b/tools/sdk_xtask_import/src/coverage.rs @@ -0,0 +1,156 @@ +use std::{fs, path::Path, process::Command}; + +use crate::{ + check, + coverage_policy::{CoverageContract, evaluate_report, validate_contract}, + fs::workspace_root, + generate, wasm, +}; + +pub fn run(args: &[String]) -> Result<(), String> { + match args { + [command] if command == "run" => run_coverage(), + [command] if command == "check" => check_coverage(), + [] => Err(usage()), + _ => Err(usage()), + } +} + +fn usage() -> String { + "usage: cargo xtask coverage run | cargo xtask coverage check".to_owned() +} + +fn run_coverage() -> Result<(), String> { + let root = workspace_root()?; + let contract = load_contract(&root)?; + validate_contract(&contract)?; + preflight(&contract)?; + generate::generate_ts()?; + wasm::generate(&[])?; + check::check()?; + clean_report_output(&root, &contract)?; + run_llvm_cov(&root, &contract)?; + evaluate_report(&root, &root.join(&contract.report.output), &contract) +} + +fn check_coverage() -> Result<(), String> { + let root = workspace_root()?; + let contract = load_contract(&root)?; + validate_contract(&contract)?; + evaluate_report(&root, &root.join(&contract.report.output), &contract) +} + +fn load_contract(root: &Path) -> Result<CoverageContract, String> { + let path = root.join("contracts").join("coverage.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn preflight(contract: &CoverageContract) -> Result<(), String> { + require_command("rustup", &["--version"], "install rustup")?; + require_command( + "rustup", + &[ + "run", + &contract.toolchain.coverage_rust, + "cargo", + "llvm-cov", + "--version", + ], + "install cargo-llvm-cov for the SDK Rust toolchain", + )?; + let component_output = output( + "rustup", + &[ + "component", + "list", + "--toolchain", + &contract.toolchain.coverage_rust, + ], + ) + .map_err(|error| format!("failed to inspect Rust components: {error}"))?; + if !component_output + .lines() + .any(|line| line.starts_with("llvm-tools") && line.contains("(installed)")) + { + return Err(format!( + "missing llvm-tools-preview for Rust toolchain {}; run `rustup component add llvm-tools-preview --toolchain {}`", + contract.toolchain.coverage_rust, contract.toolchain.coverage_rust + )); + } + let target_output = output( + "rustup", + &["target", "list", "--toolchain", &contract.toolchain.rust], + ) + .map_err(|error| format!("failed to inspect Rust targets: {error}"))?; + let expected_target = format!("{} (installed)", contract.toolchain.wasm_target); + if !target_output.lines().any(|line| line == expected_target) { + return Err(format!( + "missing Rust target {}; run `rustup target add {} --toolchain {}`", + contract.toolchain.wasm_target, contract.toolchain.wasm_target, contract.toolchain.rust + )); + } + require_command("wasm-pack", &["--version"], "install wasm-pack")?; + require_command("pnpm", &["--version"], "install pnpm")?; + Ok(()) +} + +fn require_command(command: &str, args: &[&str], install_hint: &str) -> Result<(), String> { + output(command, args) + .map(|_| ()) + .map_err(|error| format!("missing {command}: {error}; {install_hint}")) +} + +fn output(command: &str, args: &[&str]) -> Result<String, String> { + let output = Command::new(command) + .args(args) + .output() + .map_err(|error| error.to_string())?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).trim().to_string()); + } + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +fn clean_report_output(root: &Path, contract: &CoverageContract) -> Result<(), String> { + let output_path = root.join(&contract.report.output); + if let Some(parent) = output_path.parent() + && parent.exists() + { + fs::remove_dir_all(parent) + .map_err(|error| format!("failed to remove {}: {error}", parent.display()))?; + } + Ok(()) +} + +fn run_llvm_cov(root: &Path, contract: &CoverageContract) -> Result<(), String> { + let output_path = root.join(&contract.report.output); + if let Some(parent) = output_path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + let status = Command::new("rustup") + .current_dir(root) + .args([ + "run", + &contract.toolchain.coverage_rust, + "cargo", + "llvm-cov", + "--workspace", + "--all-features", + "--json", + "--branch", + "--output-path", + &contract.report.output, + "--ignore-filename-regex", + &contract.report.ignore_filename_regex, + "--no-fail-fast", + ]) + .status() + .map_err(|error| format!("failed to run cargo llvm-cov: {error}"))?; + if !status.success() { + return Err(format!("cargo llvm-cov failed with status {status}")); + } + Ok(()) +} diff --git a/tools/sdk_xtask_import/src/coverage_policy.rs b/tools/sdk_xtask_import/src/coverage_policy.rs @@ -0,0 +1,803 @@ +use std::{ + collections::BTreeMap, + fs, + path::{Component, Path, PathBuf}, +}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageContract { + policy: CoveragePolicy, + pub(crate) toolchain: CoverageToolchain, + pub(crate) report: CoverageReport, + generated: GeneratedCoveragePolicy, + scopes: BTreeMap<String, CoverageScope>, + exclusions: BTreeMap<String, CoverageExclusion>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoveragePolicy { + enforce: bool, + require_regions: bool, + require_functions: bool, + require_executable_lines: bool, + require_branches: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageToolchain { + pub(crate) rust: String, + pub(crate) coverage_rust: String, + pub(crate) wasm_target: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageReport { + pub(crate) output: String, + pub(crate) ignore_filename_regex: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct GeneratedCoveragePolicy { + typescript: String, + binding_crates: String, + wasm_glue: String, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoverageScope { + paths: Vec<String>, + threshold: f64, + #[serde(default = "default_true")] + branches_applicable: bool, + branch_reason: Option<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct CoverageExclusion { + paths: Vec<String>, + reason: String, +} + +const fn default_true() -> bool { + true +} + +#[derive(Debug, Deserialize)] +struct LlvmCovReport { + data: Vec<LlvmCovData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovData { + files: Vec<LlvmCovFile>, + #[serde(default)] + functions: Vec<LlvmCovFunction>, + totals: LlvmCovSummary, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovFunction { + count: u64, + filenames: Vec<String>, + regions: Vec<Vec<u64>>, + #[serde(default)] + branches: Vec<Vec<u64>>, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct FunctionKey { + filenames: Vec<String>, + definition: RegionKey, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct RegionKey { + line_start: u64, + column_start: u64, + line_end: u64, + column_end: u64, + kind: u64, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct BranchKey { + line_start: u64, + column_start: u64, + line_end: u64, + column_end: u64, + kind: u64, +} + +#[derive(Debug)] +struct CoverageSource { + raw: String, + cfg_test_lines: Vec<bool>, + coverage_off_lines: Vec<bool>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovFile { + filename: String, + summary: LlvmCovSummary, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummary { + lines: LlvmCovMetric, + functions: LlvmCovMetric, + regions: LlvmCovMetric, + branches: LlvmCovMetric, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovMetric { + count: u64, + covered: u64, + percent: f64, +} + +#[derive(Debug, Default)] +struct MetricAccumulator { + count: u64, + covered: u64, +} + +impl MetricAccumulator { + fn add(&mut self, metric: &LlvmCovMetric) { + self.count += metric.count; + self.covered += metric.covered; + } + + fn metric(&self) -> LlvmCovMetric { + LlvmCovMetric { + count: self.count, + covered: self.covered, + percent: metric_percent(self.count, self.covered), + } + } +} + +#[derive(Debug, Default)] +struct SummaryAccumulator { + lines: MetricAccumulator, + functions: MetricAccumulator, + regions: MetricAccumulator, + branches: MetricAccumulator, + matched_files: usize, +} + +impl SummaryAccumulator { + fn add(&mut self, summary: &LlvmCovSummary) { + self.lines.add(&summary.lines); + self.functions.add(&summary.functions); + self.regions.add(&summary.regions); + self.branches.add(&summary.branches); + self.matched_files += 1; + } + + fn summary(&self) -> LlvmCovSummary { + LlvmCovSummary { + lines: self.lines.metric(), + functions: self.functions.metric(), + regions: self.regions.metric(), + branches: self.branches.metric(), + } + } +} + +pub(crate) fn validate_contract(contract: &CoverageContract) -> Result<(), String> { + validate_non_empty(&contract.toolchain.rust, "toolchain.rust")?; + validate_non_empty(&contract.toolchain.coverage_rust, "toolchain.coverage_rust")?; + validate_non_empty(&contract.toolchain.wasm_target, "toolchain.wasm_target")?; + validate_non_empty(&contract.report.output, "report.output")?; + validate_non_empty( + &contract.report.ignore_filename_regex, + "report.ignore_filename_regex", + )?; + validate_non_empty(&contract.generated.typescript, "generated.typescript")?; + validate_non_empty( + &contract.generated.binding_crates, + "generated.binding_crates", + )?; + validate_non_empty(&contract.generated.wasm_glue, "generated.wasm_glue")?; + if contract.scopes.is_empty() { + return Err("contracts/coverage.toml scopes must not be empty".to_owned()); + } + for (name, scope) in &contract.scopes { + validate_non_empty(name, "scope name")?; + validate_threshold(scope.threshold, &format!("scopes.{name}.threshold"))?; + if scope.paths.is_empty() { + return Err(format!("scopes.{name}.paths must not be empty")); + } + for path in &scope.paths { + validate_non_empty(path, &format!("scopes.{name}.paths entry"))?; + } + if scope.branches_applicable { + if scope.branch_reason.is_some() { + return Err(format!( + "scopes.{name}.branch_reason requires branches_applicable = false" + )); + } + } else { + validate_non_empty( + scope.branch_reason.as_deref().unwrap_or_default(), + &format!("scopes.{name}.branch_reason"), + )?; + } + } + if contract.exclusions.is_empty() { + return Err("contracts/coverage.toml exclusions must not be empty".to_owned()); + } + for (name, exclusion) in &contract.exclusions { + validate_non_empty(name, "exclusion name")?; + validate_non_empty(&exclusion.reason, &format!("exclusions.{name}.reason"))?; + if exclusion.paths.is_empty() { + return Err(format!("exclusions.{name}.paths must not be empty")); + } + for path in &exclusion.paths { + validate_non_empty(path, &format!("exclusions.{name}.paths entry"))?; + } + } + Ok(()) +} + +fn validate_threshold(threshold: f64, field: &str) -> Result<(), String> { + if (0.0..=100.0).contains(&threshold) { + Ok(()) + } else { + Err(format!( + "contracts/coverage.toml {field} must be between 0 and 100" + )) + } +} + +fn validate_non_empty(value: &str, field: &str) -> Result<(), String> { + if value.trim().is_empty() { + Err(format!("contracts/coverage.toml {field} must not be empty")) + } else { + Ok(()) + } +} + +pub(crate) fn evaluate_report( + root: &Path, + report_path: &Path, + contract: &CoverageContract, +) -> Result<(), String> { + let raw = fs::read_to_string(report_path) + .map_err(|error| format!("failed to read {}: {error}", report_path.display()))?; + let report = serde_json::from_str::<LlvmCovReport>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", report_path.display()))?; + let data = report + .data + .first() + .ok_or_else(|| format!("{} did not include coverage data", report_path.display()))?; + validate_metric( + "total executable lines", + &data.totals.lines, + contract.policy.require_executable_lines, + )?; + validate_metric( + "total functions", + &data.totals.functions, + contract.policy.require_functions, + )?; + validate_metric( + "total regions", + &data.totals.regions, + contract.policy.require_regions, + )?; + validate_metric( + "total branches", + &data.totals.branches, + contract.policy.require_branches, + )?; + let mut failures = Vec::new(); + for (scope_name, scope) in &contract.scopes { + let scope_summary = match scope_summary(root, data, scope) { + Ok(summary) => summary, + Err(error) => { + failures.push(format!("coverage scope {scope_name}: {error}")); + continue; + } + }; + collect_scope_metric_failure( + &mut failures, + scope_name, + "executable lines", + &scope_summary.lines, + scope.threshold, + contract.policy.require_executable_lines, + ); + collect_scope_metric_failure( + &mut failures, + scope_name, + "functions", + &scope_summary.functions, + scope.threshold, + contract.policy.require_functions, + ); + collect_scope_metric_failure( + &mut failures, + scope_name, + "regions", + &scope_summary.regions, + scope.threshold, + contract.policy.require_regions, + ); + if scope.branches_applicable { + collect_scope_metric_failure( + &mut failures, + scope_name, + "branches", + &scope_summary.branches, + scope.threshold, + contract.policy.require_branches, + ); + } else if scope_summary.branches.count != 0 { + failures.push(format!( + "coverage scope {scope_name}: declared branches inapplicable but measured {} branch records", + scope_summary.branches.count + )); + } + } + if !contract.policy.enforce { + println!( + "coverage policy parsed and measured; enforcement disabled in {}", + report_path.display() + ); + return Ok(()); + } + if !failures.is_empty() { + return Err(failures.join("\n")); + } + println!("coverage policy passed using {}", report_path.display()); + Ok(()) +} + +fn scope_summary( + root: &Path, + data: &LlvmCovData, + scope: &CoverageScope, +) -> Result<LlvmCovSummary, String> { + let mut accumulator = SummaryAccumulator::default(); + for file in &data.files { + let filename = report_filename(root, &file.filename); + if scope + .paths + .iter() + .any(|pattern| path_matches(pattern, &filename)) + { + accumulator.add(&file.summary); + } + } + if accumulator.matched_files == 0 { + return Err(format!( + "matched no report files for {}", + scope.paths.join(", ") + )); + } + let mut summary = accumulator.summary(); + if !data.functions.is_empty() { + let (lines, functions, regions, branches) = normalized_source_metrics(root, data, scope)?; + summary.lines = lines; + summary.functions = functions; + summary.regions = regions; + if branches.count > 0 { + summary.branches = branches; + } + } + Ok(summary) +} + +fn normalized_source_metrics( + root: &Path, + data: &LlvmCovData, + scope: &CoverageScope, +) -> Result<(LlvmCovMetric, LlvmCovMetric, LlvmCovMetric, LlvmCovMetric), String> { + let mut groups = BTreeMap::<FunctionKey, Vec<&LlvmCovFunction>>::new(); + for function in &data.functions { + if function.filenames.is_empty() || function.regions.is_empty() { + continue; + } + let Some(definition) = function + .regions + .first() + .and_then(|region| region_key(region)) + else { + continue; + }; + groups + .entry(FunctionKey { + filenames: function.filenames.clone(), + definition, + }) + .or_default() + .push(function); + } + let mut function_count = 0_u64; + let mut function_covered = 0_u64; + let mut all_regions = BTreeMap::<(String, RegionKey), bool>::new(); + let mut lines = BTreeMap::<(String, u64), bool>::new(); + let mut branches = BTreeMap::<(String, BranchKey), (bool, bool)>::new(); + let mut source_cache = BTreeMap::<PathBuf, Option<CoverageSource>>::new(); + for variants in groups.values() { + let primary_definition = variants.iter().find_map(|function| { + let region = function.regions.first()?; + let filename = region_filename(function, region)?; + let relative = report_filename(root, filename); + if scope.paths.iter().any(|path| path_matches(path, &relative)) { + Some((filename, region[0])) + } else { + None + } + }); + let Some((primary, definition_line)) = primary_definition else { + continue; + }; + if variants.iter().all(|function| { + function.regions.first().is_some_and(|region| { + region + .first() + .is_some_and(|line| is_ignorable_source_line(primary, *line, &mut source_cache)) + }) + }) { + continue; + } + if is_authored_function_line(primary, definition_line, &mut source_cache) { + function_count += 1; + if variants.iter().any(|function| function.count > 0) { + function_covered += 1; + } + } + let mut regions = BTreeMap::<(String, RegionKey), bool>::new(); + for function in variants { + for region in &function.regions { + let filename = region_filename(function, region) + .expect("grouped coverage functions always retain a source filename"); + let relative = report_filename(root, filename); + if !scope.paths.iter().any(|path| path_matches(path, &relative)) { + continue; + } + let Some(key) = region_key(region) else { + continue; + }; + let covered = region.get(4).is_some_and(|count| *count > 0); + regions + .entry((filename.to_owned(), key)) + .and_modify(|existing| *existing |= covered) + .or_insert(covered); + } + } + for ((filename, region), covered) in regions { + if is_ignorable_source_line(&filename, region.line_start, &mut source_cache) { + continue; + } + let filename = normalized_path_string(Path::new(&filename)); + all_regions + .entry((filename.clone(), region.clone())) + .and_modify(|existing| *existing |= covered) + .or_insert(covered); + if region.kind == 0 { + for line in region.line_start..=region.line_end { + if !is_ignorable_source_line(&filename, line, &mut source_cache) { + lines + .entry((filename.clone(), line)) + .and_modify(|existing| *existing |= covered) + .or_insert(covered); + } + } + } + } + for function in variants { + for branch in &function.branches { + let filename = branch_filename(function, branch) + .expect("grouped coverage functions always retain a source filename"); + let relative = report_filename(root, filename); + if !scope.paths.iter().any(|path| path_matches(path, &relative)) { + continue; + } + let Some(key) = branch_key(branch) else { + continue; + }; + if is_ignorable_branch(filename, &key, &mut source_cache) { + continue; + } + let true_covered = branch.get(4).is_some_and(|count| *count > 0); + let false_covered = branch.get(5).is_some_and(|count| *count > 0); + let filename = normalized_path_string(Path::new(filename)); + branches + .entry((filename, key)) + .and_modify(|covered| { + covered.0 |= true_covered; + covered.1 |= false_covered; + }) + .or_insert((true_covered, false_covered)); + } + } + } + let line_count = lines.len() as u64; + let line_covered = lines.values().filter(|covered| **covered).count() as u64; + let branch_count = (branches.len() * 2) as u64; + let branch_covered = branches + .values() + .map(|covered| u64::from(covered.0) + u64::from(covered.1)) + .sum::<u64>(); + let region_count = all_regions.len() as u64; + let region_covered = all_regions.values().filter(|covered| **covered).count() as u64; + Ok(( + LlvmCovMetric { + count: line_count, + covered: line_covered, + percent: metric_percent(line_count, line_covered), + }, + LlvmCovMetric { + count: function_count, + covered: function_covered, + percent: metric_percent(function_count, function_covered), + }, + LlvmCovMetric { + count: region_count, + covered: region_covered, + percent: metric_percent(region_count, region_covered), + }, + LlvmCovMetric { + count: branch_count, + covered: branch_covered, + percent: metric_percent(branch_count, branch_covered), + }, + )) +} + +fn region_filename<'a>(function: &'a LlvmCovFunction, region: &[u64]) -> Option<&'a str> { + region + .get(5) + .and_then(|index| function.filenames.get(*index as usize)) + .or_else(|| function.filenames.first()) + .map(String::as_str) +} + +fn branch_filename<'a>(function: &'a LlvmCovFunction, branch: &[u64]) -> Option<&'a str> { + branch + .get(6) + .and_then(|index| function.filenames.get(*index as usize)) + .or_else(|| function.filenames.first()) + .map(String::as_str) +} + +fn is_authored_function_line( + filename: &str, + line: u64, + cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>, +) -> bool { + let path = PathBuf::from(filename); + if !cache.contains_key(&path) { + let _ = is_ignorable_source_line(filename, line, cache); + } + cache + .get(&path) + .and_then(Option::as_ref) + .and_then(|source| source.raw.lines().nth(line.saturating_sub(1) as usize)) + .is_some_and(|source_line| source_line.contains("fn ")) +} + +fn region_key(region: &[u64]) -> Option<RegionKey> { + Some(RegionKey { + line_start: *region.first()?, + column_start: *region.get(1)?, + line_end: *region.get(2)?, + column_end: *region.get(3)?, + kind: *region.get(7)?, + }) +} + +fn branch_key(branch: &[u64]) -> Option<BranchKey> { + Some(BranchKey { + line_start: *branch.first()?, + column_start: *branch.get(1)?, + line_end: *branch.get(2)?, + column_end: *branch.get(3)?, + kind: *branch.get(8)?, + }) +} + +fn is_ignorable_source_line( + filename: &str, + line: u64, + cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>, +) -> bool { + let path = PathBuf::from(filename); + let source = cache.entry(path.clone()).or_insert_with(|| { + fs::read_to_string(path).ok().map(|raw| CoverageSource { + raw: raw.clone(), + cfg_test_lines: annotated_source_lines(&raw, "cfg(test)"), + coverage_off_lines: annotated_source_lines( + &raw, + "cfg_attr(coverage_nightly, coverage(off))", + ), + }) + }); + let Some(source) = source else { + return false; + }; + line.checked_sub(1) + .map(|index| { + let index = index as usize; + source.cfg_test_lines.get(index).copied().unwrap_or(false) + || source + .coverage_off_lines + .get(index) + .copied() + .unwrap_or(false) + }) + .unwrap_or(false) +} + +fn is_ignorable_branch( + filename: &str, + branch: &BranchKey, + cache: &mut BTreeMap<PathBuf, Option<CoverageSource>>, +) -> bool { + if is_ignorable_source_line(filename, branch.line_start, cache) { + return true; + } + if branch.line_start != branch.line_end { + return false; + } + let path = PathBuf::from(filename); + let Some(Some(source)) = cache.get(&path) else { + return false; + }; + let Some(line) = source + .raw + .lines() + .nth(branch.line_start.saturating_sub(1) as usize) + else { + return false; + }; + let start = branch.column_start.saturating_sub(1) as usize; + let end = branch.column_end.saturating_sub(1) as usize; + let slice = line.get(start..end); + (branch.column_end == branch.column_start + 1 && slice == Some("?")) + || line.contains("unreachable!()") + || (line.contains("assert!(matches!(") && slice == Some("matches!")) +} + +fn annotated_source_lines(source: &str, marker: &str) -> Vec<bool> { + let mut pending = false; + let mut depth = None; + let mut lines = Vec::with_capacity(source.lines().count()); + for line in source.lines() { + let trimmed = line.trim(); + let mut annotated = depth.is_some(); + let marker_matches = if marker == "cfg(test)" { + trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("#[cfg(all(test,") + } else { + trimmed.contains(marker) + }; + if depth.is_none() && marker_matches { + pending = true; + annotated = true; + } else if depth.is_none() && pending { + annotated = true; + let content = + !trimmed.is_empty() && !trimmed.starts_with("//") && !trimmed.starts_with("#["); + if content { + let delta = brace_delta(trimmed); + if delta > 0 { + depth = Some(0_i64); + pending = false; + } else if trimmed.contains('{') || trimmed.ends_with(';') { + pending = false; + } + } + } + lines.push(annotated); + if let Some(current) = depth.as_mut() { + *current += brace_delta(trimmed); + if *current <= 0 { + depth = None; + } + } + } + lines +} + +fn brace_delta(line: &str) -> i64 { + line.bytes().filter(|byte| *byte == b'{').count() as i64 + - line.bytes().filter(|byte| *byte == b'}').count() as i64 +} + +fn report_filename(root: &Path, filename: &str) -> String { + let path = normalize_path(Path::new(filename)); + let root = normalize_path(root); + let relative = path.strip_prefix(&root).unwrap_or(&path); + normalized_path_string(relative) +} + +fn normalize_path(path: &Path) -> PathBuf { + let mut normalized = PathBuf::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::ParentDir => { + let _ = normalized.pop(); + } + Component::Prefix(_) | Component::RootDir | Component::Normal(_) => { + normalized.push(component.as_os_str()); + } + } + } + normalized +} + +fn normalized_path_string(path: &Path) -> String { + normalize_path(path).to_string_lossy().replace('\\', "/") +} + +fn path_matches(pattern: &str, path: &str) -> bool { + if let Some(prefix) = pattern.strip_suffix("/**") { + path == prefix || path.starts_with(&format!("{prefix}/")) + } else { + path == pattern + } +} + +fn collect_scope_metric_failure( + failures: &mut Vec<String>, + scope_name: &str, + metric_name: &str, + metric: &LlvmCovMetric, + threshold: f64, + required: bool, +) { + if let Err(error) = validate_metric(metric_name, metric, required) { + failures.push(format!("coverage scope {scope_name}: {error}")); + } + if let Err(error) = enforce_metric(metric_name, metric, threshold) { + failures.push(format!("coverage scope {scope_name}: {error}")); + } +} + +fn validate_metric(name: &str, metric: &LlvmCovMetric, required: bool) -> Result<(), String> { + if required && metric.count == 0 { + return Err(format!( + "coverage report did not include required {name} records" + )); + } + if metric.covered > metric.count { + return Err(format!("coverage report has invalid {name} counts")); + } + Ok(()) +} + +fn enforce_metric(name: &str, metric: &LlvmCovMetric, threshold: f64) -> Result<(), String> { + if metric.percent < threshold { + return Err(format!( + "coverage {name} {:.3}% ({}/{}) is below required {:.1}%", + metric.percent, metric.covered, metric.count, threshold + )); + } + Ok(()) +} + +fn metric_percent(count: u64, covered: u64) -> f64 { + if count == 0 { + 0.0 + } else { + covered as f64 * 100.0 / count as f64 + } +} + +#[cfg(test)] +#[path = "coverage_policy_tests.rs"] +mod tests; diff --git a/tools/sdk_xtask_import/src/coverage_policy_tests.rs b/tools/sdk_xtask_import/src/coverage_policy_tests.rs @@ -0,0 +1,661 @@ +use std::{ + fs, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use super::{ + BranchKey, CoverageContract, LlvmCovFunction, LlvmCovMetric, LlvmCovReport, + annotated_source_lines, brace_delta, branch_filename, branch_key, enforce_metric, + evaluate_report, is_authored_function_line, is_ignorable_branch, is_ignorable_source_line, + metric_percent, normalized_source_metrics, path_matches, region_filename, region_key, + report_filename, validate_contract, validate_metric, +}; + +const CONTRACT: &str = r#" +[policy] +enforce = true +require_regions = true +require_functions = true +require_executable_lines = true +require_branches = true + +[toolchain] +rust = "1.97.1" +coverage_rust = "nightly" +wasm_target = "wasm32-unknown-unknown" + +[report] +output = "target/sdk-coverage/summary.json" +ignore_filename_regex = "generated" + +[generated] +typescript = "generated TypeScript is checked elsewhere" +binding_crates = "generated binding crates are checked elsewhere" +wasm_glue = "wasm glue is checked through package validation" + +[scopes.xtask_policy] +paths = ["tools/xtask/src/coverage_policy.rs"] +threshold = 90.0 + +[exclusions.generated] +paths = ["packages/*/src/generated/**"] +reason = "generated output is checked through reproducibility" +"#; + +#[derive(Clone, Copy)] +struct Metrics { + lines: (u64, u64, f64), + functions: (u64, u64, f64), + regions: (u64, u64, f64), + branches: (u64, u64, f64), +} + +fn covered() -> Metrics { + Metrics { + lines: (100, 100, 100.0), + functions: (50, 50, 100.0), + regions: (200, 200, 100.0), + branches: (80, 80, 100.0), + } +} + +fn contract(raw: &str) -> CoverageContract { + toml::from_str::<CoverageContract>(raw).expect("contract parses") +} + +fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "radroots_sdk_coverage_policy_{name}_{}_{}", + std::process::id(), + stamp + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("create root"); + root +} + +fn metric_json(metric: (u64, u64, f64)) -> String { + format!( + r#"{{"count":{},"covered":{},"percent":{}}}"#, + metric.0, metric.1, metric.2 + ) +} + +fn summary_json(metrics: Metrics) -> String { + format!( + r#"{{"lines":{},"functions":{},"regions":{},"branches":{}}}"#, + metric_json(metrics.lines), + metric_json(metrics.functions), + metric_json(metrics.regions), + metric_json(metrics.branches) + ) +} + +fn report_json(filename: &str, file_metrics: Metrics, totals: Metrics) -> String { + format!( + r#"{{"data":[{{"files":[{{"filename":"{}","summary":{}}}],"totals":{}}}]}}"#, + filename, + summary_json(file_metrics), + summary_json(totals) + ) +} + +fn write_report(root: &Path, raw: &str) -> PathBuf { + let report_path = root.join("summary.json"); + fs::write(&report_path, raw).expect("write report"); + report_path +} + +fn scope_file(root: &Path) -> String { + root.join("tools/xtask/src/coverage_policy.rs") + .display() + .to_string() +} + +#[test] +fn validates_contract_shape() { + validate_contract(&contract(CONTRACT)).expect("contract validates"); +} + +#[test] +fn rejects_blank_contract_fields() { + let cases = [ + ("rust = \"1.97.1\"", "rust = \" \"", "toolchain.rust"), + ( + "coverage_rust = \"nightly\"", + "coverage_rust = \" \"", + "toolchain.coverage_rust", + ), + ( + "wasm_target = \"wasm32-unknown-unknown\"", + "wasm_target = \" \"", + "toolchain.wasm_target", + ), + ( + "output = \"target/sdk-coverage/summary.json\"", + "output = \" \"", + "report.output", + ), + ( + "ignore_filename_regex = \"generated\"", + "ignore_filename_regex = \" \"", + "report.ignore_filename_regex", + ), + ( + "typescript = \"generated TypeScript is checked elsewhere\"", + "typescript = \" \"", + "generated.typescript", + ), + ( + "binding_crates = \"generated binding crates are checked elsewhere\"", + "binding_crates = \" \"", + "generated.binding_crates", + ), + ( + "wasm_glue = \"wasm glue is checked through package validation\"", + "wasm_glue = \" \"", + "generated.wasm_glue", + ), + ( + "paths = [\"tools/xtask/src/coverage_policy.rs\"]", + "paths = [\" \"]", + "scopes.xtask_policy.paths entry", + ), + ( + "reason = \"generated output is checked through reproducibility\"", + "reason = \" \"", + "exclusions.generated.reason", + ), + ( + "paths = [\"packages/*/src/generated/**\"]", + "paths = [\" \"]", + "exclusions.generated.paths entry", + ), + ]; + + for (from, to, expected) in cases { + let raw = CONTRACT.replace(from, to); + let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn rejects_contract_collection_errors() { + let mut no_scopes = contract(CONTRACT); + no_scopes.scopes.clear(); + assert_eq!( + validate_contract(&no_scopes).unwrap_err(), + "contracts/coverage.toml scopes must not be empty" + ); + + let mut no_exclusions = contract(CONTRACT); + no_exclusions.exclusions.clear(); + assert_eq!( + validate_contract(&no_exclusions).unwrap_err(), + "contracts/coverage.toml exclusions must not be empty" + ); + + let cases = [ + ( + CONTRACT.replace("[scopes.xtask_policy]", "[scopes.\"\"]"), + "scope name", + ), + ( + CONTRACT.replace("[exclusions.generated]", "[exclusions.\"\"]"), + "exclusion name", + ), + ( + CONTRACT.replace("threshold = 90.0", "threshold = 101.0"), + "scopes.xtask_policy.threshold", + ), + ( + CONTRACT.replace( + "threshold = 90.0", + "threshold = 90.0\nbranch_reason = \"not applicable\"", + ), + "branch_reason requires branches_applicable = false", + ), + ( + CONTRACT.replace( + "threshold = 90.0", + "threshold = 90.0\nbranches_applicable = false", + ), + "scopes.xtask_policy.branch_reason", + ), + ( + CONTRACT.replace( + "paths = [\"tools/xtask/src/coverage_policy.rs\"]", + "paths = []", + ), + "scopes.xtask_policy.paths must not be empty", + ), + ( + CONTRACT.replace("paths = [\"packages/*/src/generated/**\"]", "paths = []"), + "exclusions.generated.paths must not be empty", + ), + ]; + + for (raw, expected) in cases { + let error = validate_contract(&contract(&raw)).expect_err("invalid contract"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn matches_recursive_scope_paths() { + assert!(path_matches( + "crates/sdk/src/**", + "crates/sdk/src/adapters/radrootsd.rs" + )); + assert!(path_matches("crates/sdk/src/**", "crates/sdk/src")); + assert!(path_matches( + "tools/xtask/src/coverage_policy.rs", + "tools/xtask/src/coverage_policy.rs" + )); + assert!(!path_matches( + "crates/sdk/src/**", + "crates/sql_wasm_runtime/src/lib.rs" + )); +} + +#[test] +fn accepts_passing_reports_and_rejects_undercovered_scopes() { + let root = test_root("passing_and_undercovered"); + let filename = scope_file(&root); + let passing_report = report_json(&filename, covered(), covered()); + let report_path = write_report(&root, &passing_report); + evaluate_report(&root, &report_path, &contract(CONTRACT)).expect("passing report"); + + let mut undercovered = covered(); + undercovered.lines = (100, 89, 89.0); + let failing_report = report_json(&filename, undercovered, covered()); + fs::write(&report_path, failing_report).expect("write failing report"); + let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) + .expect_err("undercovered report rejected"); + assert!(error.contains("coverage scope xtask_policy"), "{error}"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn disabled_enforcement_accepts_measured_undercoverage() { + let root = test_root("disabled"); + let filename = scope_file(&root); + let mut undercovered = covered(); + undercovered.lines = (100, 0, 0.0); + undercovered.functions = (50, 0, 0.0); + undercovered.regions = (200, 0, 0.0); + undercovered.branches = (80, 0, 0.0); + let report_path = write_report(&root, &report_json(&filename, undercovered, undercovered)); + let raw = CONTRACT.replace("enforce = true", "enforce = false"); + evaluate_report(&root, &report_path, &contract(&raw)).expect("disabled policy passes"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn branch_inapplicability_is_explicit_and_rejects_measured_branches() { + let root = test_root("branch_inapplicability"); + let filename = scope_file(&root); + let raw = CONTRACT.replace( + "threshold = 90.0", + "threshold = 90.0\nbranches_applicable = false\nbranch_reason = \"straight-line source\"", + ); + let mut no_branches = covered(); + no_branches.branches = (0, 0, 0.0); + let report_path = write_report(&root, &report_json(&filename, no_branches, covered())); + evaluate_report(&root, &report_path, &contract(&raw)).expect("no-branch scope passes"); + + fs::write(&report_path, report_json(&filename, covered(), covered())) + .expect("write measured branches"); + let error = evaluate_report(&root, &report_path, &contract(&raw)) + .expect_err("measured branch drift rejected"); + assert!(error.contains("declared branches inapplicable"), "{error}"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn duplicate_harness_variants_are_merged_and_test_modules_are_excluded() { + let root = test_root("duplicate_harness_variants"); + let filename = scope_file(&root); + let source_path = Path::new(&filename); + fs::create_dir_all(source_path.parent().expect("source parent")).expect("create source parent"); + fs::write( + source_path, + "pub fn production() -> bool { true }\n\n#[cfg(test)]\nmod tests {\n fn helper() -> bool { false }\n}\n", + ) + .expect("write source"); + let report = format!( + r#"{{"data":[{{"files":[{{"filename":"{filename}","summary":{summary}}}],"functions":[{{"count":0,"filenames":["{filename}"],"regions":[[1,1,1,37,0,0,0,0]]}},{{"count":1,"filenames":["{filename}"],"regions":[[1,1,1,37,1,0,0,0]]}},{{"count":0,"filenames":["{filename}"],"regions":[[5,5,5,34,0,0,0,0]]}}],"totals":{summary}}}]}}"#, + summary = summary_json(covered()), + ); + let report_path = write_report(&root, &report); + + evaluate_report(&root, &report_path, &contract(CONTRACT)) + .expect("normalized production coverage passes"); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn normalized_details_fail_closed_and_merge_every_supported_record_shape() { + let root = test_root("normalized_record_shapes"); + let filename = scope_file(&root); + let source_path = Path::new(&filename); + fs::create_dir_all(source_path.parent().expect("source parent")).expect("source parent"); + fs::write( + source_path, + "pub fn production(flag: bool) -> bool { if flag { true } else { false } }\nfn uncovered() {}\nconst VALUE: bool = true;\n#[cfg(test)]\nmod tests { fn ignored() {} }\n", + ) + .expect("source"); + let outside = root.join("outside.rs").display().to_string(); + fs::write(&outside, "fn outside() {}\n").expect("outside source"); + let summary = summary_json(covered()); + let report = format!( + r#"{{"data":[{{"files":[],"functions":[ + {{"count":0,"filenames":[],"regions":[],"branches":[]}}, + {{"count":0,"filenames":["{filename}"],"regions":[[1,1,1]],"branches":[]}}, + {{"count":0,"filenames":["{outside}"],"regions":[[1,1,1,16,0,0,0,0]],"branches":[]}}, + {{"count":0,"filenames":["{filename}"],"regions":[[2,1,2,18,0,0,0,0]],"branches":[]}}, + {{"count":1,"filenames":["{filename}"],"regions":[[3,1,3,25,1,0,0,0]],"branches":[]}}, + {{"count":1,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,1,0,0,0],[1,1,5,36,1,0,0,0],[2,1,2,18,0,0,0,1],[4,1,4,13,0,0,0,0],[1,1,1,2,0,1,0,0],[1]],"branches":[[1,40,1,47,1,0,0,0,4],[2,1,2,3,0,1,0,0,4],[4,1,4,3,0,0,0,0,4],[99,1,99,3,0,0,0,0,4],[1,1,1,2,0,0,1,0,4],[1]]}}, + {{"count":0,"filenames":["{filename}","{outside}"],"regions":[[1,1,1,76,0,0,0,0],[2,1,2,18,1,0,0,0]],"branches":[[1,40,1,47,0,1,0,0,4],[2,1,2,3,1,0,0,0,4]]}} + ],"totals":{summary}}}]}}"#, + ); + let parsed = serde_json::from_str::<LlvmCovReport>(&report).expect("detail report"); + let policy = contract(CONTRACT); + let scope = policy.scopes.get("xtask_policy").expect("scope"); + let (lines, functions, regions, branches) = + normalized_source_metrics(&root, &parsed.data[0], scope).expect("normalized details"); + assert!(lines.count >= 2); + assert_eq!(functions.count, 2); + assert_eq!(functions.covered, 1); + assert!(regions.count >= 2); + assert_eq!(branches.count, 6); + assert_eq!(branches.covered, 4); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_unreadable_malformed_and_empty_reports() { + let root = test_root("bad_reports"); + let missing = root.join("missing.json"); + assert!(evaluate_report(&root, &missing, &contract(CONTRACT)).is_err()); + + let malformed = write_report(&root, "{"); + assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); + + fs::write(&malformed, r#"{"data":[]}"#).expect("write empty report"); + assert!(evaluate_report(&root, &malformed, &contract(CONTRACT)).is_err()); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_required_total_metric_failures() { + let root = test_root("total_metrics"); + let filename = scope_file(&root); + let mut totals = covered(); + let cases = [ + Metrics { + lines: (0, 0, 0.0), + ..totals + }, + { + totals = covered(); + totals.functions = (0, 0, 0.0); + totals + }, + { + totals = covered(); + totals.regions = (0, 0, 0.0); + totals + }, + { + totals = covered(); + totals.branches = (0, 0, 0.0); + totals + }, + { + totals = covered(); + totals.lines = (1, 2, 200.0); + totals + }, + ]; + + let report_path = root.join("summary.json"); + for total_metrics in cases { + fs::write( + &report_path, + report_json(&filename, covered(), total_metrics), + ) + .expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + } + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn rejects_scope_metric_validation_and_missing_scope_files() { + let root = test_root("scope_metrics"); + let filename = scope_file(&root); + let other_filename = root + .join("tools/xtask/src/coverage.rs") + .display() + .to_string(); + let report_path = root.join("summary.json"); + + fs::write( + &report_path, + report_json(&other_filename, covered(), covered()), + ) + .expect("write unmatched report"); + let error = evaluate_report(&root, &report_path, &contract(CONTRACT)) + .expect_err("unmatched scope rejected"); + assert!(error.contains("matched no report files"), "{error}"); + + let mut invalid = covered(); + invalid.lines = (0, 0, 0.0); + fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + + invalid = covered(); + invalid.functions = (1, 2, 200.0); + fs::write(&report_path, report_json(&filename, invalid, covered())).expect("write report"); + assert!(evaluate_report(&root, &report_path, &contract(CONTRACT)).is_err()); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn metric_helpers_cover_edges() { + let valid = LlvmCovMetric { + count: 10, + covered: 10, + percent: 100.0, + }; + validate_metric("lines", &valid, true).expect("metric validates"); + enforce_metric("lines", &valid, 100.0).expect("metric passes"); + + let missing = LlvmCovMetric { + count: 0, + covered: 0, + percent: 0.0, + }; + assert!(validate_metric("lines", &missing, true).is_err()); + assert!(enforce_metric("lines", &missing, 100.0).is_err()); + + let invalid = LlvmCovMetric { + count: 1, + covered: 2, + percent: 200.0, + }; + assert!(validate_metric("lines", &invalid, true).is_err()); + assert_eq!(metric_percent(0, 0), 0.0); + assert_eq!(metric_percent(4, 2), 50.0); +} + +#[test] +fn detail_key_and_path_helpers_cover_valid_and_short_records() { + assert!(region_key(&[1, 2, 3, 4, 5, 6, 7, 8]).is_some()); + assert!(region_key(&[1, 2, 3]).is_none()); + assert!(branch_key(&[1, 2, 3, 4, 5, 6, 7, 8, 9]).is_some()); + assert!(branch_key(&[1, 2, 3]).is_none()); + assert_eq!(brace_delta("fn value() { if true { 1 } else { 0 } }"), 0); + assert_eq!(brace_delta("{{"), 2); + assert_eq!(brace_delta("}"), -1); + + let root = Path::new("/workspace/sdk"); + assert_eq!( + report_filename(root, "/workspace/sdk/crates/sdk/src/lib.rs"), + "crates/sdk/src/lib.rs" + ); + assert_eq!( + report_filename(root, "/elsewhere/lib.rs"), + "/elsewhere/lib.rs" + ); + assert_eq!( + report_filename( + root, + "/workspace/sdk/crates/sdk/src/adapters/../../tests/unit.rs" + ), + "crates/sdk/tests/unit.rs" + ); +} + +#[test] +fn detail_filename_helpers_honor_record_indexes_and_safe_fallbacks() { + let function = LlvmCovFunction { + count: 1, + filenames: vec!["primary.rs".to_owned(), "expanded.rs".to_owned()], + regions: Vec::new(), + branches: Vec::new(), + }; + assert_eq!( + region_filename(&function, &[1, 1, 1, 2, 1, 1, 0, 0]), + Some("expanded.rs") + ); + assert_eq!( + region_filename(&function, &[1, 1, 1, 2, 1, 99, 0, 0]), + Some("primary.rs") + ); + assert_eq!(region_filename(&function, &[1]), Some("primary.rs")); + assert_eq!( + branch_filename(&function, &[1, 1, 1, 2, 1, 0, 1, 0, 4]), + Some("expanded.rs") + ); + assert_eq!( + branch_filename(&function, &[1, 1, 1, 2, 1, 0, 99, 0, 4]), + Some("primary.rs") + ); + assert_eq!(branch_filename(&function, &[1]), Some("primary.rs")); + + let no_filenames = LlvmCovFunction { + count: 0, + filenames: Vec::new(), + regions: Vec::new(), + branches: Vec::new(), + }; + assert_eq!(region_filename(&no_filenames, &[1]), None); + assert_eq!(branch_filename(&no_filenames, &[1]), None); +} + +#[test] +fn authored_and_annotated_source_helpers_cover_marker_shapes() { + let root = test_root("source_helpers"); + let source = root.join("source.rs"); + fs::write( + &source, + "pub fn production() {}\n#[cfg(test)]\nmod tests {\n fn helper() {}\n}\n#[cfg(all(test, feature = \"x\"))]\nfn gated() {}\n#[cfg_attr(coverage_nightly, coverage(off))]\nfn excluded() {}\n", + ) + .expect("source"); + let filename = source.display().to_string(); + let mut cache = std::collections::BTreeMap::new(); + + assert!(is_authored_function_line(&filename, 1, &mut cache)); + assert!(!is_authored_function_line(&filename, 2, &mut cache)); + assert!(!is_ignorable_source_line(&filename, 1, &mut cache)); + assert!(is_ignorable_source_line(&filename, 2, &mut cache)); + assert!(is_ignorable_source_line(&filename, 4, &mut cache)); + assert!(is_ignorable_source_line(&filename, 7, &mut cache)); + assert!(is_ignorable_source_line(&filename, 9, &mut cache)); + assert!(!is_ignorable_source_line(&filename, 0, &mut cache)); + assert!(!is_ignorable_source_line( + root.join("missing.rs").to_str().expect("path"), + 1, + &mut cache + )); + + let marked = annotated_source_lines( + "#[cfg(test)]\n// note\nfn test() {}\nfn live() {}", + "cfg(test)", + ); + assert_eq!(marked, vec![true, true, true, false]); + let coverage_off = annotated_source_lines( + "#[cfg_attr(coverage_nightly, coverage(off))]\n#[inline]\nfn excluded() {\n if true {\n work();\n }\n}\nfn live() {}", + "cfg_attr(coverage_nightly, coverage(off))", + ); + assert_eq!( + coverage_off, + vec![true, true, true, true, true, true, true, false] + ); + assert_eq!( + annotated_source_lines("#[cfg(test)]\nfn declaration();\nfn live() {}", "cfg(test)"), + vec![true, true, false] + ); + fs::remove_dir_all(root).expect("cleanup"); +} + +#[test] +fn branch_filter_only_excludes_explicit_synthetic_constructs() { + let root = test_root("branch_helpers"); + let source = root.join("source.rs"); + fs::write( + &source, + "fn value() {\n let _ = call()?;\n unreachable!();\n assert!(matches!(value, Some(_)));\n if live { work(); }\n}\n", + ) + .expect("source"); + let filename = source.display().to_string(); + let mut cache = std::collections::BTreeMap::new(); + assert!(!is_ignorable_source_line(&filename, 1, &mut cache)); + + let branch = |line_start, column_start, line_end, column_end| BranchKey { + line_start, + column_start, + line_end, + column_end, + kind: 4, + }; + assert!(is_ignorable_branch( + &filename, + &branch(2, 16, 2, 17), + &mut cache + )); + assert!(is_ignorable_branch( + &filename, + &branch(3, 2, 3, 10), + &mut cache + )); + assert!(is_ignorable_branch( + &filename, + &branch(4, 10, 4, 18), + &mut cache + )); + assert!(!is_ignorable_branch( + &filename, + &branch(5, 2, 5, 9), + &mut cache + )); + assert!(!is_ignorable_branch( + &filename, + &branch(5, 2, 6, 1), + &mut cache + )); + assert!(!is_ignorable_branch( + root.join("missing.rs").to_str().expect("path"), + &branch(1, 1, 1, 2), + &mut cache + )); + fs::remove_dir_all(root).expect("cleanup"); +} diff --git a/tools/sdk_xtask_import/src/dto_roots.rs b/tools/sdk_xtask_import/src/dto_roots.rs @@ -0,0 +1,969 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use dto_bindgen_backend_ts::{ + DtoRegistryRenderOptions, DtoTypesModule, TypeScriptDeclaration, TypeScriptModule, + TypeScriptType, render_registry_types, +}; +use dto_bindgen_core::{Registry, RootDescriptor, build_registry}; + +#[derive(Clone, Copy, Debug)] +pub struct DtoPackageRootSet { + pub package_key: &'static str, + roots: fn() -> Vec<RootDescriptor>, +} + +impl DtoPackageRootSet { + pub fn roots(&self) -> Vec<RootDescriptor> { + (self.roots)() + } + + pub fn registry(&self) -> Registry { + build_registry(self.roots()) + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ManualDescriptorFamily { + pub package_key: &'static str, + pub source_family: &'static str, + pub reason: &'static str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SdkLocalWrapperAllowance { + pub package_key: &'static str, + pub shape_family: &'static str, + pub reason: &'static str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DtoExternalOverride { + target_type: &'static str, + import_name: &'static str, + from_package_key: &'static str, + from: &'static str, +} + +const CORE_BINDINGS_PACKAGE_KEY: &str = "core"; +const CORE_BINDINGS_PACKAGE_NAME: &str = "@radroots/core-bindings"; +const EVENT_BINDINGS_PACKAGE_KEY: &str = "event"; +const EVENT_BINDINGS_PACKAGE_NAME: &str = "@radroots/event-bindings"; + +pub const DTO_PACKAGE_ROOTS: &[DtoPackageRootSet] = &[DtoPackageRootSet { + package_key: "core", + roots: core_roots, +}]; + +pub const MANUAL_DESCRIPTOR_FAMILIES: &[ManualDescriptorFamily] = &[ + ManualDescriptorFamily { + package_key: "core", + source_family: "decimal, currency, money, quantity, percent, quantity price, unit, and discount value families", + reason: "custom serde, string-backed newtypes, aliases, and tagged enum wire forms require source-owned manual descriptors", + }, + ManualDescriptorFamily { + package_key: "event", + source_family: "event timestamps, counters, and optional metadata fields", + reason: "large integers and source-specific optional/null policy must be explicit", + }, + ManualDescriptorFamily { + package_key: "event", + source_family: "GeoJSON coordinate arrays", + reason: "fixed-size Rust arrays must preserve tuple semantics in TypeScript", + }, + ManualDescriptorFamily { + package_key: "trade", + source_family: "trade listing roots and package projection count fields", + reason: "core aliases, source-owned event imports, and count-family numeric policy require explicit descriptors", + }, + ManualDescriptorFamily { + package_key: "replica_schema", + source_family: "untagged query wrappers and serde_json value fields", + reason: "schema query shapes are generated and not all source fields map to derive-supported DTOs", + }, +]; + +pub const SDK_LOCAL_WRAPPER_ALLOWANCES: &[SdkLocalWrapperAllowance] = &[ + SdkLocalWrapperAllowance { + package_key: "core", + shape_family: "Currency and Decimal package aliases", + reason: "string-backed source descriptors require canonical named TypeScript package aliases", + }, + SdkLocalWrapperAllowance { + package_key: "replica_schema", + shape_family: "generated query argument wrappers", + reason: "schema operation inputs are generated package shapes rather than source-owned public DTO structs", + }, + SdkLocalWrapperAllowance { + package_key: "trade", + shape_family: "marketplace, query, projection, sort, review, and backoffice DTO shapes", + reason: "these are SDK package contract shapes layered over source-owned trade, events, and core DTOs", + }, +]; + +const EVENT_CORE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[ + core_override("Currency"), + core_override("Decimal"), + core_override("Discount"), + core_override("DiscountScope"), + core_override("DiscountThreshold"), + core_override("DiscountValue"), + core_override("Money"), + core_override("Percent"), + core_override("Quantity"), + core_override("QuantityPrice"), + core_override("Unit"), + core_override("UnitDimension"), +]; + +const TRADE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[ + core_override("Currency"), + core_override("Decimal"), + core_override("Discount"), + core_override("DiscountValue"), + core_override("Money"), + core_override("Quantity"), + core_override("QuantityPrice"), + core_override("Unit"), + event_override("RadrootsFarmRef"), + event_override("RadrootsOperationalListing"), + event_override("RadrootsOperationalListingAvailability"), + event_override("RadrootsOperationalListingBin"), + event_override("RadrootsOperationalListingDeliveryMethod"), + event_override("RadrootsOperationalListingImage"), + event_override("RadrootsOperationalListingProduct"), + event_override("RadrootsOperationalListingPublicLocation"), + event_override("RadrootsOperationalListingStatus"), + event_override("RadrootsEventPtr"), + event_override("RadrootsPlotRef"), + event_override("RadrootsResourceAreaRef"), +]; + +const TRADE_REQUIRED_EXTERNAL_PACKAGE_IMPORTS: &[&str] = + &[CORE_BINDINGS_PACKAGE_NAME, EVENT_BINDINGS_PACKAGE_NAME]; +const EVENT_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/event-bindings/src/generated/types.ts"); + +pub fn package_root_set(package_key: &str) -> Option<&'static DtoPackageRootSet> { + DTO_PACKAGE_ROOTS + .iter() + .find(|root_set| root_set.package_key == package_key) +} + +pub fn core_types_module() -> Result<DtoTypesModule, String> { + let root_set = package_root_set("core").ok_or_else(|| "missing core DTO roots".to_owned())?; + let rendered = + render_registry_types(&root_set.registry(), &DtoRegistryRenderOptions::default())?; + Ok(with_type_aliases_sorted( + rendered, + [ + type_alias("Currency", TypeScriptType::String), + type_alias("Decimal", TypeScriptType::String), + ], + )) +} + +pub fn replica_schema_types_module() -> Result<DtoTypesModule, String> { + render_registry_types( + &radroots_replica_schema_bindings::dto_registry(), + &DtoRegistryRenderOptions::default(), + ) +} + +fn core_roots() -> Vec<RootDescriptor> { + radroots_core_bindings::dto_roots() +} + +fn core_import_options( + mut options: DtoRegistryRenderOptions, +) -> Result<DtoRegistryRenderOptions, String> { + let package_exports = BTreeMap::from([( + CORE_BINDINGS_PACKAGE_KEY, + type_exports(core_types_module()?.body_ts()), + )]); + for override_target in EVENT_CORE_EXTERNAL_OVERRIDES { + validate_external_override_target(*override_target, &package_exports)?; + options = options.with_external_override( + override_target.target_type, + override_target.import_name, + override_target.from, + ); + } + Ok(options) +} + +fn trade_import_options( + registry: &Registry, + mut options: DtoRegistryRenderOptions, +) -> Result<DtoRegistryRenderOptions, String> { + let package_exports = generated_external_package_exports()?; + for override_target in TRADE_EXTERNAL_OVERRIDES { + validate_external_override_target(*override_target, &package_exports)?; + if let Ok(type_id) = registry.type_id_by_export_name(override_target.target_type) { + options = options.with_external_type( + type_id, + override_target.import_name, + override_target.from, + ); + } + options = options.with_external_override( + override_target.target_type, + override_target.import_name, + override_target.from, + ); + } + + Ok(options) +} + +fn generated_external_package_exports() -> Result<BTreeMap<&'static str, BTreeSet<String>>, String> +{ + Ok(BTreeMap::from([ + ( + CORE_BINDINGS_PACKAGE_KEY, + type_exports(core_types_module()?.body_ts()), + ), + ( + EVENT_BINDINGS_PACKAGE_KEY, + type_exports(EVENT_BINDINGS_TYPES_TS), + ), + ])) +} + +fn validate_external_override_target( + override_target: DtoExternalOverride, + package_exports: &BTreeMap<&'static str, BTreeSet<String>>, +) -> Result<(), String> { + let exports = package_exports + .get(override_target.from_package_key) + .ok_or_else(|| { + format!( + "external DTO override `{}` references unknown package key `{}`", + override_target.target_type, override_target.from_package_key + ) + })?; + if exports.contains(override_target.import_name) { + return Ok(()); + } + Err(format!( + "external DTO override `{}` imports `{}` from `{}`, but package `{}` does not export it", + override_target.target_type, + override_target.import_name, + override_target.from, + override_target.from_package_key + )) +} + +fn validate_external_override_usage( + module: &DtoTypesModule, + overrides: &[DtoExternalOverride], +) -> Result<(), String> { + let imports = imported_type_inventory(module.imports_ts().unwrap_or_default()); + let package_exports = generated_external_package_exports()?; + for package_name in TRADE_REQUIRED_EXTERNAL_PACKAGE_IMPORTS { + if !imports.contains_key(*package_name) { + return Err(format!( + "expected generated DTO imports from `{package_name}` but none were emitted" + )); + } + } + + for override_target in overrides { + if type_exports(module.body_ts()).contains(override_target.target_type) { + return Err(format!( + "external DTO override `{}` from `{}` was emitted locally instead of imported", + override_target.target_type, override_target.from + )); + } + if imports + .get(override_target.from) + .is_some_and(|names| names.contains(override_target.import_name)) + { + validate_external_override_target(*override_target, &package_exports)?; + } + } + Ok(()) +} + +fn imported_type_inventory(imports_ts: &str) -> BTreeMap<String, BTreeSet<String>> { + let mut imports = BTreeMap::new(); + let mut pending_names: Option<Vec<String>> = None; + + for line in imports_ts.lines() { + let line = line.trim(); + if let Some(single) = line.strip_prefix("import type { ") { + if let Some((name, from)) = single.split_once(" } from ") { + insert_import_names(&mut imports, from, name); + } + } else if line == "import type {" { + pending_names = Some(Vec::new()); + } else if let Some(from) = line.strip_prefix("} from ") { + if let Some(names) = pending_names.take() { + for name in names { + insert_import(&mut imports, from, &name); + } + } + } else if let Some(names) = pending_names.as_mut() + && let Some(name) = line.strip_suffix(',') + { + names.push(name.to_owned()); + } + } + + imports +} + +fn with_additional_type_imports( + module: DtoTypesModule, + from: &str, + names: impl IntoIterator<Item = &'static str>, +) -> DtoTypesModule { + let mut imports = imported_type_inventory(module.imports_ts().unwrap_or_default()); + imports + .entry(from.to_owned()) + .or_default() + .extend(names.into_iter().map(str::to_owned)); + let imports_ts = imports + .into_iter() + .map(|(from, names)| { + format!( + "import type {{ {} }} from \"{from}\";", + names.into_iter().collect::<Vec<_>>().join(", ") + ) + }) + .collect::<Vec<_>>() + .join("\n"); + let imports_ts = if imports_ts.is_empty() { + imports_ts + } else { + format!("{imports_ts}\n\n") + }; + DtoTypesModule::new(imports_ts, module.body_ts()) +} + +fn insert_import_names(imports: &mut BTreeMap<String, BTreeSet<String>>, from: &str, names: &str) { + for name in names.split(',') { + insert_import(imports, from, name); + } +} + +fn insert_import(imports: &mut BTreeMap<String, BTreeSet<String>>, from: &str, name: &str) { + let from = from.trim_end_matches(';').trim_matches('"'); + imports + .entry(from.to_owned()) + .or_default() + .insert(name.trim().to_owned()); +} + +fn type_exports(types_ts: &str) -> BTreeSet<String> { + types_ts + .lines() + .filter_map(|line| line.strip_prefix("export type ")) + .filter_map(|rest| rest.split([' ', '<']).next()) + .map(str::to_owned) + .collect() +} + +const fn core_override(export_name: &'static str) -> DtoExternalOverride { + DtoExternalOverride { + target_type: export_name, + import_name: export_name, + from_package_key: CORE_BINDINGS_PACKAGE_KEY, + from: CORE_BINDINGS_PACKAGE_NAME, + } +} + +const fn event_override(export_name: &'static str) -> DtoExternalOverride { + DtoExternalOverride { + target_type: export_name, + import_name: export_name, + from_package_key: EVENT_BINDINGS_PACKAGE_KEY, + from: EVENT_BINDINGS_PACKAGE_NAME, + } +} + +fn with_event_sdk_wrappers(module: DtoTypesModule) -> DtoTypesModule { + let mut declarations = module + .body_ts() + .split("\n\n") + .filter(|declaration| !declaration.trim().is_empty()) + .map(str::to_owned) + .collect::<Vec<_>>(); + declarations.push(type_alias( + "RadrootsOperationalListingProductTagKeys", + TypeScriptType::readonly_tuple( + [ + "key", "title", "category", "summary", "process", "lot", "location", "profile", + "year", + ] + .into_iter() + .map(TypeScriptType::literal_string) + .collect::<Vec<_>>(), + ), + )); + declarations.sort_by(|left, right| declaration_name(left).cmp(declaration_name(right))); + DtoTypesModule::new( + module.imports_ts().unwrap_or_default(), + declarations.join("\n\n"), + ) +} + +fn declaration_name(declaration: &str) -> &str { + declaration + .strip_prefix("export type ") + .and_then(|rest| rest.split([' ', '<']).next()) + .unwrap_or(declaration) +} + +fn with_type_aliases_sorted( + module: DtoTypesModule, + aliases: impl IntoIterator<Item = String>, +) -> DtoTypesModule { + let mut declarations = module + .body_ts() + .split("\n\n") + .filter(|declaration| !declaration.trim().is_empty()) + .map(str::to_owned) + .collect::<Vec<_>>(); + declarations.extend(aliases); + declarations.sort_by(|left, right| declaration_name(left).cmp(declaration_name(right))); + DtoTypesModule::new( + module.imports_ts().unwrap_or_default(), + declarations.join("\n\n"), + ) +} + +fn type_alias(name: impl Into<String>, type_expr: TypeScriptType) -> String { + TypeScriptModule::new("types.ts") + .with_declaration(TypeScriptDeclaration::type_alias(name, type_expr)) + .render_source() + .trim() + .to_owned() +} + +#[cfg(test)] +mod tests { + use std::collections::{BTreeMap, BTreeSet}; + + use dto_bindgen_core::SourceSpan; + + use super::{ + CORE_BINDINGS_PACKAGE_KEY, CORE_BINDINGS_PACKAGE_NAME, DTO_PACKAGE_ROOTS, + DtoExternalOverride, MANUAL_DESCRIPTOR_FAMILIES, SDK_LOCAL_WRAPPER_ALLOWANCES, + event_override, imported_type_inventory, package_root_set, type_exports, + validate_external_override_target, validate_external_override_usage, + with_additional_type_imports, + }; + use dto_bindgen_backend_ts::DtoTypesModule; + + const CORE_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/core-bindings/src/generated/types.ts"); + const EVENT_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/event-bindings/src/generated/types.ts"); + const REPLICA_SCHEMA_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/replica-schema-bindings/src/generated/types.ts"); + const TRADE_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/trade-bindings/src/generated/types.ts"); + const REPLICA_SCHEMA_MODEL_SOURCES: &[&str] = &[ + include_str!("../../../../lib/crates/replica_schema/src/models/farm.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/farm_gcs_location.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/farm_member.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/farm_member_claim.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/farm_tag.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/gcs_location.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/log_error.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/media_image.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/nostr_event_head.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/nostr_profile.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/nostr_profile_relay.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/nostr_relay.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/plot.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/plot_gcs_location.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/plot_tag.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/trade_product.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/trade_product_location.rs"), + include_str!("../../../../lib/crates/replica_schema/src/models/trade_product_media.rs"), + ]; + const CORE_TYPE_INVENTORY: &[&str] = &[ + "Currency", + "Decimal", + "Discount", + "DiscountScope", + "DiscountThreshold", + "DiscountValue", + "Money", + "Percent", + "Quantity", + "QuantityPrice", + "Unit", + "UnitDimension", + ]; + const EVENT_TYPE_INVENTORY: &[&str] = &[ + "JobFeedbackStatus", + "JobInputType", + "JobPaymentRequest", + "RadrootsAccountClaim", + "RadrootsAddressableRef", + "RadrootsAppData", + "RadrootsCommercialDomain", + "RadrootsContributionAttestation", + "RadrootsCoop", + "RadrootsCoopLocation", + "RadrootsCoopRef", + "RadrootsDocument", + "RadrootsDocumentSubject", + "RadrootsEventEnvelopeDto", + "RadrootsEventPtr", + "RadrootsEventRef", + "RadrootsEvidenceBounty", + "RadrootsFarm", + "RadrootsFarmPublicLocation", + "RadrootsFarmRef", + "RadrootsFollow", + "RadrootsFollowProfile", + "RadrootsGcsLocation", + "RadrootsGeoChat", + "RadrootsGeoJsonPoint", + "RadrootsGeoJsonPolygon", + "RadrootsGiftWrap", + "RadrootsGiftWrapRecipient", + "RadrootsJobFeedback", + "RadrootsJobInput", + "RadrootsJobParam", + "RadrootsJobRequest", + "RadrootsJobResult", + "RadrootsKnowledgeChangeProposal", + "RadrootsKnowledgeCitationSpan", + "RadrootsKnowledgeClaim", + "RadrootsKnowledgeFieldContext", + "RadrootsKnowledgeFieldReport", + "RadrootsKnowledgeLocation", + "RadrootsKnowledgeLocationPrecision", + "RadrootsKnowledgeNodeRef", + "RadrootsKnowledgeObservation", + "RadrootsKnowledgeObservationValue", + "RadrootsKnowledgeRelation", + "RadrootsKnowledgeReview", + "RadrootsKnowledgeReviewScope", + "RadrootsKnowledgeReviewScore", + "RadrootsKnowledgeReviewTarget", + "RadrootsKnowledgeSource", + "RadrootsList", + "RadrootsListEntry", + "RadrootsListSet", + "RadrootsMessage", + "RadrootsMessageFile", + "RadrootsMessageFileDimensions", + "RadrootsMessageRecipient", + "RadrootsNip01EventWireDto", + "RadrootsOperationalListing", + "RadrootsOperationalListingAvailability", + "RadrootsOperationalListingBin", + "RadrootsOperationalListingDeliveryMethod", + "RadrootsOperationalListingImage", + "RadrootsOperationalListingImageSize", + "RadrootsOperationalListingParseError", + "RadrootsOperationalListingProduct", + "RadrootsOperationalListingProductTagKeys", + "RadrootsOperationalListingPublicLocation", + "RadrootsOperationalListingStatus", + "RadrootsOperationalListingValidationError", + "RadrootsPlot", + "RadrootsPlotLocation", + "RadrootsPlotRef", + "RadrootsPost", + "RadrootsProfileType", + "RadrootsReaction", + "RadrootsRelayDocument", + "RadrootsResourceArea", + "RadrootsResourceAreaLocation", + "RadrootsResourceAreaRef", + "RadrootsResourceHarvestCap", + "RadrootsResourceHarvestProduct", + "RadrootsRightsAssertion", + "RadrootsSeal", + "RadrootsSignedEventDto", + "RadrootsSignedEventVerificationStateDto", + "RadrootsSocialFarmAnchor", + "RadrootsSocialLocation", + "RadrootsSocialMediaDimensions", + "RadrootsSocialMediaMetadata", + "RadrootsSocialMediaThumbnail", + "RadrootsSocialTarget", + "RadrootsVerifiedSignedEventDto", + "RadrootsVerifiedSignedEventVerificationStateDto", + "RadrootsWikiArticle", + "RadrootsWikiArticleVersionRef", + "RadrootsWikiMergeRequest", + "RadrootsWikiRedirect", + ]; + const TRADE_TYPE_INVENTORY: &[&str] = &[ + "RadrootsOperationalListingSubtotal", + "RadrootsOperationalListingTotal", + "RadrootsOperationalListingTradeProjection", + "RadrootsTradeAgreementClaimV1", + "RadrootsTradeAgreementStateV1", + "RadrootsTradeAttestationRecordV1", + "RadrootsTradeAttestationResultV1", + "RadrootsTradeAttestationStateV1", + "RadrootsTradeConflictStateV1", + "RadrootsTradeEvidenceStateV1", + "RadrootsTradeFacetCount", + "RadrootsTradeFulfillmentStateV1", + "RadrootsTradeListingBackofficeOverlay", + "RadrootsTradeListingBackofficeQuery", + "RadrootsTradeListingBackofficeView", + "RadrootsTradeListingBinProjection", + "RadrootsTradeListingFacets", + "RadrootsTradeListingMarketStatus", + "RadrootsTradeListingProjection", + "RadrootsTradeListingQuery", + "RadrootsTradeListingSort", + "RadrootsTradeListingSortField", + "RadrootsTradeMarketplaceListingSummary", + "RadrootsTradeModerationFlag", + "RadrootsTradeModerationSeverity", + "RadrootsTradeModerationStatus", + "RadrootsTradeNegotiationStateV1", + "RadrootsTradePaymentStateV1", + "RadrootsTradePrivateTermsStateV1", + "RadrootsTradeProjectionV1", + "RadrootsTradeReducerIssueV1", + "RadrootsTradeReviewPriority", + "RadrootsTradeReviewQueueEntry", + "RadrootsTradeReviewStatus", + "RadrootsTradeSortDirection", + ]; + + #[test] + fn approved_source_roots_build_registries() { + for root_set in DTO_PACKAGE_ROOTS { + let registry = root_set.registry(); + assert!( + !registry.has_errors(), + "registry for {} has diagnostics: {:?}", + root_set.package_key, + registry.diagnostics + ); + assert!(!registry.roots.is_empty()); + } + } + + #[test] + fn package_roots_are_explicit_not_discovered() { + assert!(package_root_set("core").is_some()); + assert!(package_root_set("event").is_none()); + assert!(package_root_set("trade").is_none()); + } + + #[test] + fn manual_descriptor_catalog_covers_known_review_families() { + assert!( + MANUAL_DESCRIPTOR_FAMILIES + .iter() + .any(|family| family.source_family.contains("GeoJSON")) + ); + assert!(SDK_LOCAL_WRAPPER_ALLOWANCES.iter().any(|allowance| { + allowance + .shape_family + .contains("marketplace, query, projection") + })); + } + + #[test] + fn external_override_target_validation_rejects_missing_generated_export() { + let package_exports = BTreeMap::from([( + CORE_BINDINGS_PACKAGE_KEY, + BTreeSet::from(["ExistingType".to_owned()]), + )]); + let error = validate_external_override_target( + DtoExternalOverride { + target_type: "MissingType", + import_name: "MissingType", + from_package_key: CORE_BINDINGS_PACKAGE_KEY, + from: CORE_BINDINGS_PACKAGE_NAME, + }, + &package_exports, + ) + .expect_err("missing target package export must fail"); + + assert_eq!( + error, + "external DTO override `MissingType` imports `MissingType` from `@radroots/core-bindings`, but package `core` does not export it" + ); + } + + #[test] + fn external_override_usage_rejects_absent_required_package_import() { + let module = DtoTypesModule::new("", "export type RadrootsTradeListing = string;"); + let error = validate_external_override_usage(&module, &[]) + .expect_err("missing required package import must fail"); + + assert_eq!( + error, + "expected generated DTO imports from `@radroots/core-bindings` but none were emitted" + ); + } + + #[test] + fn external_override_usage_rejects_local_emission_for_imported_type() { + let module = DtoTypesModule::new( + "import type { Decimal } from \"@radroots/core-bindings\";\n\nimport type { RadrootsFarmRef } from \"@radroots/event-bindings\";\n\n", + "export type RadrootsFarmRef = { pubkey: string, d_tag: string, };", + ); + let error = validate_external_override_usage(&module, &[event_override("RadrootsFarmRef")]) + .expect_err("local emission of imported type must fail"); + + assert_eq!( + error, + "external DTO override `RadrootsFarmRef` from `@radroots/event-bindings` was emitted locally instead of imported" + ); + } + + #[test] + fn generated_import_inventory_parses_single_and_multiline_imports() { + let imports = imported_type_inventory( + "import type { One, Four } from \"@radroots/one\";\nimport type {\n Two,\n Three,\n} from \"@radroots/many\";\n\n", + ); + + assert_eq!( + imports.get("@radroots/one").expect("single import package"), + &BTreeSet::from(["Four".to_owned(), "One".to_owned()]) + ); + assert_eq!( + imports.get("@radroots/many").expect("multi import package"), + &BTreeSet::from(["Three".to_owned(), "Two".to_owned()]) + ); + } + + #[test] + fn additional_type_imports_merge_with_generated_package_imports() { + let module = DtoTypesModule::new( + "import type { Decimal, Money } from \"@radroots/core-bindings\";\n\n", + "export type Listing = { discounts: Array<Discount>, };", + ); + let merged = with_additional_type_imports(module, CORE_BINDINGS_PACKAGE_NAME, ["Discount"]); + + assert_eq!( + merged.imports_ts(), + Some("import type { Decimal, Discount, Money } from \"@radroots/core-bindings\";\n\n") + ); + } + + #[test] + fn generated_type_export_inventory_parses_type_aliases() { + assert_eq!( + type_exports("export type Alpha = string;\nexport type Beta<T> = T;\n"), + BTreeSet::from(["Alpha".to_owned(), "Beta".to_owned()]) + ); + } + + #[test] + fn core_type_inventory_matches_current_package_surface() { + let actual = type_inventory(CORE_BINDINGS_TYPES_TS); + + assert_eq!(actual, CORE_TYPE_INVENTORY); + assert!(!CORE_BINDINGS_TYPES_TS.contains("RadrootsCore")); + } + + #[test] + fn event_type_inventory_matches_current_package_surface() { + let actual = type_inventory(EVENT_BINDINGS_TYPES_TS); + + assert_eq!(actual, EVENT_TYPE_INVENTORY); + assert!(!EVENT_BINDINGS_TYPES_TS.contains("RadrootsOrder")); + } + + #[test] + fn event_generated_types_expose_current_signed_event_dto_shapes() { + let wire = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsNip01EventWireDto"); + let envelope = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsEventEnvelopeDto"); + let signed = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsSignedEventDto"); + let verified = type_declaration(EVENT_BINDINGS_TYPES_TS, "RadrootsVerifiedSignedEventDto"); + + assert!(wire.contains("pubkey: string")); + assert!(wire.contains("extra: { [key: string]: unknown }")); + assert!(envelope.contains("author: string")); + assert!(!envelope.contains("pubkey")); + assert!(!envelope.contains("extra")); + assert!(signed.contains("state: RadrootsSignedEventVerificationStateDto")); + assert!(signed.contains("envelope: RadrootsEventEnvelopeDto")); + assert!(signed.contains("wire: RadrootsNip01EventWireDto")); + assert!(signed.contains("raw_json: string")); + assert!(verified.contains("state: RadrootsVerifiedSignedEventVerificationStateDto")); + assert!(verified.contains("signed_event: RadrootsSignedEventDto")); + assert!( + !EVENT_BINDINGS_TYPES_TS.contains("export type RadrootsEventEnvelope ="), + "event package must not export the retired raw envelope type name" + ); + } + + #[test] + fn trade_type_inventory_matches_current_package_surface() { + let actual = type_inventory(TRADE_BINDINGS_TYPES_TS); + + assert_eq!(actual, TRADE_TYPE_INVENTORY); + } + + #[test] + fn replica_schema_generated_types_preserve_source_schema_contracts() { + let actual = type_inventory(REPLICA_SCHEMA_BINDINGS_TYPES_TS); + let trade_product_filter = type_declaration( + REPLICA_SCHEMA_BINDINGS_TYPES_TS, + "ITradeProductFieldsFilter", + ); + let trade_product_partial = type_declaration( + REPLICA_SCHEMA_BINDINGS_TYPES_TS, + "ITradeProductFieldsPartial", + ); + + assert!(actual.contains(&"Farm")); + assert!(actual.contains(&"GcsLocation")); + assert!(actual.contains(&"NostrEventHead")); + assert!(actual.contains(&"ReplicaStoreJsonValue")); + assert!(actual.contains(&"ITradeProductFieldsPartial")); + assert!(!actual.contains(&"NostrEventState")); + assert!(REPLICA_SCHEMA_BINDINGS_TYPES_TS.contains( + "export type ReplicaStoreJsonValue = null | boolean | number | string | Array<ReplicaStoreJsonValue> | { [key: string]: ReplicaStoreJsonValue };" + )); + assert!( + REPLICA_SCHEMA_BINDINGS_TYPES_TS + .contains("export type IFarmFindOneResolve = ReplicaSchemaResult<Farm | null>;") + ); + assert!(actual.contains(&"ReplicaSchemaResult")); + assert!(actual.contains(&"ReplicaSchemaResultList")); + assert!(actual.contains(&"ReplicaSchemaResultPass")); + assert!(actual.contains(&"ReplicaSchemaError")); + assert!(trade_product_filter.contains("year?: bigint")); + assert!(trade_product_filter.contains("qty_avail?: bigint")); + assert!(trade_product_partial.contains("year?: ReplicaStoreJsonValue | null")); + assert!(trade_product_partial.contains("qty_avail?: ReplicaStoreJsonValue | null")); + } + + #[test] + fn replica_schema_generated_types_match_source_public_inventory() { + let actual = type_inventory(REPLICA_SCHEMA_BINDINGS_TYPES_TS) + .into_iter() + .collect::<BTreeSet<_>>(); + let missing = source_public_schema_type_inventory() + .into_iter() + .filter(|name| !actual.contains(name)) + .collect::<Vec<_>>(); + + assert!( + missing.is_empty(), + "missing generated replica schema exports: {missing:?}" + ); + } + + #[test] + fn trade_package_imports_source_owned_support_types() { + assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/core-bindings\"")); + assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/event-bindings\"")); + + let imports = imported_type_inventory(TRADE_BINDINGS_TYPES_TS); + assert!( + imports + .get("@radroots/event-bindings") + .is_some_and(|names| names.contains("RadrootsFarmRef")) + ); + assert!( + imports + .get("@radroots/event-bindings") + .is_some_and(|names| names.contains("RadrootsOperationalListing")) + ); + assert!( + imports + .get("@radroots/event-bindings") + .is_some_and(|names| names.contains("RadrootsOperationalListingBin")) + ); + + for duplicate in [ + "export type RadrootsOperationalListing = ", + "export type RadrootsFarmRef = ", + "export type RadrootsOrderEconomics = ", + "export type RadrootsOrderInventoryCommitment = ", + "export type RadrootsCommercialMessagePayload = ", + "export type RadrootsCommercialMessageType = ", + "export type RadrootsOrderStatus = ", + "export type RadrootsTradeOrderWorkflowMessage = ", + "export type RadrootsOrderWorkflowProjection = ", + "export type RadrootsTradeMarketplaceOrderSummary = ", + "export type RadrootsTradeOrderQuery = ", + ] { + assert!(!TRADE_BINDINGS_TYPES_TS.contains(duplicate)); + } + + let projection = type_declaration(TRADE_BINDINGS_TYPES_TS, "RadrootsTradeProjectionV1"); + assert!(projection.contains("agreement_state: RadrootsTradeAgreementStateV1")); + assert!(projection.contains("negotiation_state: RadrootsTradeNegotiationStateV1")); + assert!(projection.contains("agreement_claims: Array<RadrootsTradeAgreementClaimV1>")); + assert!(projection.contains("issues: Array<RadrootsTradeReducerIssueV1>")); + assert!(projection.contains("projection_digest: string")); + for stale_counter in [ + "root_event_id", + "last_message_type", + "last_discount_request", + "last_discount_offer", + "accepted_discount", + "last_discount_decline_reason", + "has_requested_discounts", + "question_count", + "answer_count", + "discount_request_count", + "discount_offer_count", + "discount_accept_count", + "discount_decline_count", + ] { + assert!(!projection.contains(stale_counter)); + } + } + + fn type_inventory(types_ts: &str) -> Vec<&str> { + types_ts + .lines() + .filter_map(|line| line.strip_prefix("export type ")) + .map(|rest| rest.split([' ', '<']).next().expect("type name")) + .collect() + } + + fn source_public_schema_type_inventory() -> Vec<&'static str> { + let mut names = BTreeSet::new(); + + for source in REPLICA_SCHEMA_MODEL_SOURCES { + for line in source.lines() { + if let Some(name) = public_rust_type_name(line) + && !name.ends_with("Ts") + { + names.insert(name); + } + } + } + + names.into_iter().collect() + } + + fn public_rust_type_name(line: &'static str) -> Option<&'static str> { + let line = line.trim_start(); + + ["pub struct ", "pub enum ", "pub type "] + .into_iter() + .find_map(|prefix| { + line.strip_prefix(prefix).map(|rest| { + rest.split(|char: char| !(char == '_' || char.is_ascii_alphanumeric())) + .next() + .expect("type name") + }) + }) + } + + fn type_declaration<'a>(types_ts: &'a str, name: &str) -> &'a str { + types_ts + .lines() + .find(|line| line.starts_with(&format!("export type {name} = "))) + .unwrap_or_else(|| panic!("missing type declaration for {name}")) + } + + fn span() -> SourceSpan { + SourceSpan::new("tools/xtask/src/dto_roots.rs", 1, 1) + } +} diff --git a/tools/sdk_xtask_import/src/fs.rs b/tools/sdk_xtask_import/src/fs.rs @@ -0,0 +1,61 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub fn workspace_root() -> Result<PathBuf, String> { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .and_then(Path::parent) + .map(Path::to_path_buf) + .ok_or_else(|| { + format!( + "cannot resolve workspace root from {}", + manifest_dir.display() + ) + }) +} + +#[allow(dead_code)] +pub fn write_if_changed(path: &Path, contents: &str) -> Result<bool, String> { + if let Ok(existing) = fs::read_to_string(path) + && existing == contents + { + return Ok(false); + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + fs::write(path, contents) + .map_err(|error| format!("failed to write {}: {error}", path.display()))?; + Ok(true) +} + +pub fn write_bytes_if_changed(path: &Path, contents: &[u8]) -> Result<bool, String> { + if let Ok(existing) = fs::read(path) + && existing == contents + { + return Ok(false); + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("failed to create {}: {error}", parent.display()))?; + } + fs::write(path, contents) + .map_err(|error| format!("failed to write {}: {error}", path.display()))?; + Ok(true) +} + +#[cfg(test)] +mod tests { + use super::workspace_root; + + #[test] + fn resolves_workspace_root() { + let root = workspace_root().expect("workspace root resolves"); + assert!(root.join("Cargo.toml").is_file()); + assert!(root.join("packages").is_dir()); + } +} diff --git a/tools/sdk_xtask_import/src/generate.rs b/tools/sdk_xtask_import/src/generate.rs @@ -0,0 +1,35 @@ +use crate::{ + fs::workspace_root, output::package_outputs, package_matrix::validate_package_matrix, + package_metadata, +}; + +pub fn generate_all() -> Result<(), String> { + generate_ts()?; + generate_package_metadata() +} + +pub fn generate_ts() -> Result<(), String> { + validate_package_matrix()?; + let root = workspace_root()?; + for output in package_outputs()? { + for generated_file in output.files() { + let path = root + .join(output.spec.package_dir) + .join(generated_file.relative_path); + crate::fs::write_if_changed(&path, &generated_file.contents)?; + } + let provenance_file = output.provenance_file(); + crate::fs::write_if_changed( + &root.join(&provenance_file.relative_path), + &provenance_file.contents, + )?; + println!("generated TypeScript package {}", output.spec.package_name); + } + Ok(()) +} + +pub fn generate_package_metadata() -> Result<(), String> { + validate_package_matrix()?; + let root = workspace_root()?; + package_metadata::generate_package_metadata(&root) +} diff --git a/tools/sdk_xtask_import/src/main.rs b/tools/sdk_xtask_import/src/main.rs @@ -0,0 +1,281 @@ +mod api_qualification; +mod architecture; +mod bindings; +mod check; +mod contracts; +mod coverage; +mod coverage_policy; +#[allow(dead_code)] +mod dto_roots; +mod fs; +mod generate; +mod manifest; +mod output; +mod package_matrix; +mod package_metadata; +mod portable_qualification; +mod release_graph; +mod release_qualification; +mod smoke; +mod supply_chain_qualification; +mod target_qualification; +mod ts; +mod wasm; +mod wasm_declarations; + +enum CommandAction<'a> { + Architecture, + ArchitectureCi, + CheckApiBoundaries, + CheckDependencyBoundaries, + GenerateAll, + GenerateBindings(&'a [String]), + GenerateTs, + GenerateWasm(&'a [String]), + GeneratePackageMetadata, + Coverage(&'a [String]), + QualifyFeatures, + QualifyGraph, + QualifyApi, + QualifyPortable, + QualifySupplyChain, + QualifyTargets, + Check, + Smoke(&'a [String]), +} + +fn main() { + if let Err(error) = run(std::env::args().skip(1)) { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run(args: impl IntoIterator<Item = String>) -> Result<(), String> { + let args = args.into_iter().collect::<Vec<_>>(); + match command_action(&args)? { + CommandAction::Architecture => architecture::validate(&fs::workspace_root()?), + CommandAction::ArchitectureCi => { + let root = fs::workspace_root()?; + architecture::validate_ci(&root)?; + check::architecture_ci(&root) + } + CommandAction::CheckApiBoundaries => { + architecture::validate_api_boundaries(&fs::workspace_root()?) + } + CommandAction::CheckDependencyBoundaries => { + architecture::validate_dependency_boundaries(&fs::workspace_root()?) + } + CommandAction::GenerateAll => generate::generate_all(), + CommandAction::GenerateBindings(rest) => bindings::generate(rest), + CommandAction::GenerateTs => generate::generate_ts(), + CommandAction::GenerateWasm(rest) => wasm::generate(rest), + CommandAction::GeneratePackageMetadata => generate::generate_package_metadata(), + CommandAction::Coverage(rest) => coverage::run(rest), + CommandAction::QualifyFeatures => { + release_qualification::run_feature_matrix(&fs::workspace_root()?) + } + CommandAction::QualifyGraph => release_graph::run(&fs::workspace_root()?), + CommandAction::QualifyApi => api_qualification::run(&fs::workspace_root()?), + CommandAction::QualifyPortable => portable_qualification::run(&fs::workspace_root()?), + CommandAction::QualifySupplyChain => { + supply_chain_qualification::run(&fs::workspace_root()?) + } + CommandAction::QualifyTargets => target_qualification::run(&fs::workspace_root()?), + CommandAction::Check => check::check(), + CommandAction::Smoke(rest) => smoke::run(rest), + } +} + +fn command_action(args: &[String]) -> Result<CommandAction<'_>, String> { + match args { + [command] if command == "architecture" => Ok(CommandAction::Architecture), + [command] if command == "architecture-ci" => Ok(CommandAction::ArchitectureCi), + [command] if command == "check-api-boundaries" => Ok(CommandAction::CheckApiBoundaries), + [command] if command == "check-dependency-boundaries" => { + Ok(CommandAction::CheckDependencyBoundaries) + } + [command] if command == "generate" => Ok(CommandAction::GenerateAll), + [command, target] if command == "generate" && target == "ts" => { + Ok(CommandAction::GenerateTs) + } + [command, target, rest @ ..] if command == "generate" && target == "wasm" => { + Ok(CommandAction::GenerateWasm(rest)) + } + [command, target, rest @ ..] if command == "generate" && target == "bindings" => { + Ok(CommandAction::GenerateBindings(rest)) + } + [command, target] if command == "generate" && target == "package-metadata" => { + Ok(CommandAction::GeneratePackageMetadata) + } + [command, rest @ ..] if command == "coverage" => Ok(CommandAction::Coverage(rest)), + [command, target] if command == "release" && target == "qualify-features" => { + Ok(CommandAction::QualifyFeatures) + } + [command, target] if command == "release" && target == "qualify-graph" => { + Ok(CommandAction::QualifyGraph) + } + [command, target] if command == "release" && target == "qualify-api" => { + Ok(CommandAction::QualifyApi) + } + [command, target] if command == "release" && target == "qualify-portable" => { + Ok(CommandAction::QualifyPortable) + } + [command, target] if command == "release" && target == "qualify-supply-chain" => { + Ok(CommandAction::QualifySupplyChain) + } + [command, target] if command == "release" && target == "qualify-targets" => { + Ok(CommandAction::QualifyTargets) + } + [command] if command == "check" => Ok(CommandAction::Check), + [command, rest @ ..] if command == "smoke" => Ok(CommandAction::Smoke(rest)), + [] => Err(usage()), + _ => Err(usage()), + } +} + +fn usage() -> String { + "usage: cargo xtask architecture | cargo xtask architecture-ci | cargo xtask check-api-boundaries | cargo xtask check-dependency-boundaries | cargo xtask generate | cargo xtask generate ts | cargo xtask generate wasm [--package <key>] | cargo xtask generate bindings <swift|kotlin> | cargo xtask generate package-metadata | cargo xtask check | cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local | cargo xtask coverage run | cargo xtask coverage check | cargo xtask release qualify-features | cargo xtask release qualify-graph | cargo xtask release qualify-api | cargo xtask release qualify-portable | cargo xtask release qualify-supply-chain | cargo xtask release qualify-targets" + .to_owned() +} + +#[cfg(test)] +mod tests { + use super::{CommandAction, command_action}; + + #[test] + fn accepts_architecture() { + let args = ["architecture".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::Architecture + )); + } + + #[test] + fn accepts_architecture_ci() { + let args = ["architecture-ci".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::ArchitectureCi + )); + } + + #[test] + fn accepts_api_boundary_check() { + let args = ["check-api-boundaries".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::CheckApiBoundaries + )); + } + + #[test] + fn accepts_dependency_boundary_check() { + let args = ["check-dependency-boundaries".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::CheckDependencyBoundaries + )); + } + + #[test] + fn accepts_generate_ts() { + let args = ["generate".to_owned(), "ts".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GenerateTs + )); + } + + #[test] + fn accepts_generate_all() { + let args = ["generate".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GenerateAll + )); + } + + #[test] + fn accepts_generate_wasm() { + let args = ["generate".to_owned(), "wasm".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GenerateWasm(rest) if rest.is_empty() + )); + } + + #[test] + fn accepts_generate_bindings() { + let args = [ + "generate".to_owned(), + "bindings".to_owned(), + "swift".to_owned(), + ]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GenerateBindings(rest) if rest == ["swift"] + )); + } + + #[test] + fn accepts_generate_package_metadata() { + let args = ["generate".to_owned(), "package-metadata".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::GeneratePackageMetadata + )); + } + + #[test] + fn accepts_check() { + let args = ["check".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::Check + )); + } + + #[test] + fn accepts_smoke() { + let args = ["smoke".to_owned(), "sdk-rust-local".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::Smoke(rest) if rest == ["sdk-rust-local"] + )); + } + + #[test] + fn accepts_coverage_run() { + let args = ["coverage".to_owned(), "run".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::Coverage(rest) if rest == ["run"] + )); + } + + #[test] + fn accepts_supply_chain_qualification() { + let args = ["release".to_owned(), "qualify-supply-chain".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::QualifySupplyChain + )); + } + + #[test] + fn accepts_release_graph_qualification() { + let args = ["release".to_owned(), "qualify-graph".to_owned()]; + assert!(matches!( + command_action(&args).expect("action"), + CommandAction::QualifyGraph + )); + } + + #[test] + fn rejects_unknown_command() { + let args = ["generate".to_owned(), "swift".to_owned()]; + assert!(command_action(&args).is_err()); + } +} diff --git a/tools/sdk_xtask_import/src/manifest.rs b/tools/sdk_xtask_import/src/manifest.rs @@ -0,0 +1,53 @@ +use serde_json::json; + +use crate::package_matrix::PackageSpec; + +pub fn manifest_relative_path(spec: PackageSpec) -> String { + format!("contracts/provenance/typescript/{}.json", spec.key) +} + +pub fn package_manifest(spec: PackageSpec) -> serde_json::Value { + json!({ + "package": spec.package_name, + "crate": spec.crate_name, + "version": "0.1.0", + "license": "MIT OR Apache-2.0", + "repository": "https://github.com/radrootslabs/sdk", + "repository_directory": spec.package_dir, + "generator": "radroots_sdk_xtask", + "generated": false + }) +} + +#[cfg(test)] +mod tests { + use crate::{ + manifest::{manifest_relative_path, package_manifest}, + package_matrix::package_specs, + }; + + #[test] + fn manifest_path_is_outside_package_source() { + assert_eq!( + manifest_relative_path(package_specs()[0]), + "contracts/provenance/typescript/core.json" + ); + } + + #[test] + fn manifest_records_package_and_crate() { + let manifest = package_manifest(package_specs()[0]); + assert_eq!(manifest["package"], package_specs()[0].package_name); + assert_eq!(manifest["crate"], package_specs()[0].crate_name); + assert_eq!(manifest["version"], "0.1.0"); + assert_eq!(manifest["license"], "MIT OR Apache-2.0"); + assert_eq!( + manifest["repository"], + "https://github.com/radrootslabs/sdk" + ); + assert_eq!( + manifest["repository_directory"], + package_specs()[0].package_dir + ); + } +} diff --git a/tools/sdk_xtask_import/src/output.rs b/tools/sdk_xtask_import/src/output.rs @@ -0,0 +1,493 @@ +use dto_bindgen_backend_ts::{DtoTypesModule, TypeScriptImport, TypeScriptModule}; +use sha2::{Digest, Sha256}; + +use crate::{ + dto_roots, + manifest::manifest_relative_path, + manifest::package_manifest, + package_matrix::{PackageSpec, package_specs}, + ts::{generated_constants_file, generated_header, generated_kinds_file, generated_types_file}, +}; + +pub struct PackageOutput { + pub spec: PackageSpec, + pub types_ts: Option<TsSource>, + pub types_imports: Vec<TypeScriptImport>, + pub constants_ts: Option<TsSource>, + pub kinds_ts: Option<TsSource>, +} + +pub struct GeneratedFile { + pub relative_path: String, + pub contents: String, +} + +pub enum TsSource { + DtoRegistry(DtoTypesModule), + Module(TypeScriptModule), + CanonicalNativeSnapshot { contents: &'static str }, +} + +impl TsSource { + fn canonical_native_snapshot( + contents: &'static str, + sha256: &'static str, + source_packages: &'static [&'static str], + ) -> Result<Self, String> { + let actual = format!("{:x}", Sha256::digest(contents.as_bytes())); + if actual != sha256 { + return Err(format!( + "canonical native type snapshot drifted: expected {sha256}, found {actual}; regenerate from and review against {}", + source_packages.join(", ") + )); + } + Ok(Self::CanonicalNativeSnapshot { contents }) + } + + fn render(&self) -> String { + match self { + Self::DtoRegistry(module) => module.body_ts().to_owned(), + Self::Module(module) => module.render_source(), + Self::CanonicalNativeSnapshot { contents } => (*contents).to_owned(), + } + } + + fn imports(&self) -> Option<&str> { + match self { + Self::DtoRegistry(module) => module.imports_ts(), + Self::Module(_) | Self::CanonicalNativeSnapshot { .. } => None, + } + } +} + +const EVENT_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/event-bindings/src/generated/types.ts"); +const EVENT_BINDINGS_TYPES_SHA256: &str = + "fe4496950852715c573ff2abc7df8edede7161de75d9c68c3f4d6836e67f829a"; +const TRADE_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/trade-bindings/src/generated/types.ts"); +const TRADE_BINDINGS_TYPES_SHA256: &str = + "49560c572206095bd7de5cfd377c234a7f136e4affc13d53516ae2c4b8998e87"; +const EVENT_NATIVE_SOURCES: &[&str] = &["radroots_event", "radroots_core"]; +const TRADE_NATIVE_SOURCES: &[&str] = &["radroots_trade", "radroots_event", "radroots_core"]; + +impl PackageOutput { + pub fn files(&self) -> Vec<GeneratedFile> { + let mut files = Vec::new(); + if let Some(types_ts) = &self.types_ts { + let imports = combined_imports( + structured_imports_ts(&self.types_imports).as_deref(), + types_ts.imports(), + ); + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", generated_types_file()), + contents: render_ts(types_ts, imports.as_deref()), + }); + } + if let Some(constants_ts) = &self.constants_ts { + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", generated_constants_file()), + contents: render_ts(constants_ts, None), + }); + } + if let Some(kinds_ts) = &self.kinds_ts { + files.push(GeneratedFile { + relative_path: format!("src/generated/{}", generated_kinds_file()), + contents: render_ts(kinds_ts, None), + }); + } + files + } + + pub fn provenance_file(&self) -> GeneratedFile { + GeneratedFile { + relative_path: manifest_relative_path(self.spec), + contents: render_manifest(self), + } + } +} + +pub fn package_outputs() -> Result<Vec<PackageOutput>, String> { + Ok(vec![ + PackageOutput { + spec: spec_by_key("core"), + types_ts: Some(TsSource::DtoRegistry(dto_roots::core_types_module()?)), + types_imports: Vec::new(), + constants_ts: None, + kinds_ts: None, + }, + PackageOutput { + spec: spec_by_key("event"), + types_ts: Some(TsSource::canonical_native_snapshot( + EVENT_BINDINGS_TYPES_TS, + EVENT_BINDINGS_TYPES_SHA256, + EVENT_NATIVE_SOURCES, + )?), + types_imports: Vec::new(), + constants_ts: Some(TsSource::Module(radroots_event_bindings::constants_module())), + kinds_ts: Some(TsSource::Module(radroots_event_bindings::kinds_module())), + }, + PackageOutput { + spec: spec_by_key("identity"), + types_ts: None, + types_imports: Vec::new(), + constants_ts: Some(TsSource::Module( + radroots_identity_bindings::constants_module(), + )), + kinds_ts: None, + }, + PackageOutput { + spec: spec_by_key("replica_schema"), + types_ts: Some(TsSource::DtoRegistry( + dto_roots::replica_schema_types_module()?, + )), + types_imports: Vec::new(), + constants_ts: None, + kinds_ts: None, + }, + PackageOutput { + spec: spec_by_key("trade"), + types_ts: Some(TsSource::canonical_native_snapshot( + TRADE_BINDINGS_TYPES_TS, + TRADE_BINDINGS_TYPES_SHA256, + TRADE_NATIVE_SOURCES, + )?), + types_imports: Vec::new(), + constants_ts: None, + kinds_ts: None, + }, + ]) +} + +fn spec_by_key(key: &str) -> PackageSpec { + package_specs() + .iter() + .copied() + .find(|spec| spec.key == key) + .unwrap_or_else(|| panic!("missing package spec for {key}")) +} + +fn render_ts(source: &TsSource, imports: Option<&str>) -> String { + if matches!(source, TsSource::CanonicalNativeSnapshot { .. }) { + return source.render(); + } + let body = source.render(); + let imports = imports.unwrap_or(""); + let mut rendered = format!("{}{}{}", generated_header(), imports, body.trim_start()); + if !rendered.ends_with('\n') { + rendered.push('\n'); + } + rendered +} + +fn combined_imports(first: Option<&str>, second: Option<&str>) -> Option<String> { + match (first, second) { + (Some(first), Some(second)) => Some(format!("{first}{second}")), + (Some(first), None) => Some(first.to_owned()), + (None, Some(second)) => Some(second.to_owned()), + (None, None) => None, + } +} + +fn structured_imports_ts(imports: &[TypeScriptImport]) -> Option<String> { + if imports.is_empty() { + return None; + } + Some( + imports + .iter() + .cloned() + .fold(TypeScriptModule::new("types.ts"), |module, import| { + module.with_import(import) + }) + .render_source(), + ) +} + +fn render_manifest(output: &PackageOutput) -> String { + let spec = output.spec; + let mut value = package_manifest(spec); + value["generated"] = serde_json::Value::Bool(true); + value["outputs"] = serde_json::Value::Object( + output + .files() + .into_iter() + .map(|file| { + ( + file.relative_path, + serde_json::Value::String(format!( + "{:x}", + Sha256::digest(file.contents.as_bytes()) + )), + ) + }) + .collect(), + ); + if matches!(spec.key, "event" | "trade") { + let (sha256, source_packages) = match spec.key { + "event" => (EVENT_BINDINGS_TYPES_SHA256, EVENT_NATIVE_SOURCES), + "trade" => (TRADE_BINDINGS_TYPES_SHA256, TRADE_NATIVE_SOURCES), + _ => unreachable!(), + }; + value["types_source"] = + serde_json::Value::String("canonical_native_type_snapshot".to_owned()); + value["types_sha256"] = serde_json::Value::String(sha256.to_owned()); + value["types_source_packages"] = serde_json::Value::Array( + source_packages + .iter() + .map(|package| serde_json::Value::String((*package).to_owned())) + .collect(), + ); + } + format!( + "{}\n", + serde_json::to_string_pretty(&value).expect("manifest json serializes") + ) +} + +#[cfg(test)] +mod tests { + use super::{PackageOutput, TsSource, package_outputs, render_ts}; + use crate::package_matrix::package_specs; + use dto_bindgen_backend_ts::{ + DtoTypesModule, TypeScriptDeclaration, TypeScriptImport, TypeScriptModule, TypeScriptType, + }; + + const TRADE_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/trade-bindings/src/generated/types.ts"); + const REPLICA_SCHEMA_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/replica-schema-bindings/src/generated/types.ts"); + const EVENT_BINDINGS_CONSTANTS_TS: &str = + include_str!("../../../packages/event-bindings/src/generated/constants.ts"); + const EVENT_BINDINGS_KINDS_TS: &str = + include_str!("../../../packages/event-bindings/src/generated/kinds.ts"); + const IDENTITY_BINDINGS_CONSTANTS_TS: &str = + include_str!("../../../packages/identity-bindings/src/generated/constants.ts"); + + #[test] + fn renders_sdk_header() { + let output = render_ts(&test_module(), None); + assert!(output.starts_with("// @generated by cargo xtask generate ts")); + assert!(output.contains("export type A = string;")); + } + + #[test] + fn renders_import_prelude_after_header() { + let output = render_ts(&test_module(), Some("import type { B } from \"b\";\n\n")); + assert!(output.starts_with( + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type" + )); + assert!(output.contains("export type A = string;")); + } + + #[test] + fn renders_module_sources() { + let output = render_ts(&test_module(), None); + assert_eq!( + output, + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nexport type A = string;\n" + ); + } + + #[test] + fn canonical_native_snapshot_rejects_digest_drift() { + let error = match TsSource::canonical_native_snapshot( + "drifted snapshot\n", + super::EVENT_BINDINGS_TYPES_SHA256, + super::EVENT_NATIVE_SOURCES, + ) { + Ok(_) => panic!("digest drift must fail closed"), + Err(error) => error, + }; + + assert!(error.contains("canonical native type snapshot drifted")); + assert!(error.contains(super::EVENT_BINDINGS_TYPES_SHA256)); + assert!(error.contains("radroots_event")); + } + + #[test] + fn includes_core_and_schema_outputs() { + let package_names = package_outputs() + .expect("package outputs") + .into_iter() + .map(|output| output.spec.package_name) + .collect::<Vec<_>>(); + assert!(package_names.contains(&"@radroots/core-bindings")); + assert!(package_names.contains(&"@radroots/event-bindings")); + assert!(package_names.contains(&"@radroots/identity-bindings")); + assert!(package_names.contains(&"@radroots/replica-schema-bindings")); + assert!(package_names.contains(&"@radroots/trade-bindings")); + } + + #[test] + fn dto_registry_source_uses_generated_package_files() { + let output = PackageOutput { + spec: package_specs()[0], + types_ts: Some(TsSource::DtoRegistry(DtoTypesModule::new( + "import type { ExternalThing } from \"@radroots/external-bindings\";\n\n", + "export type SyntheticThing = { external: ExternalThing, };", + ))), + types_imports: vec![TypeScriptImport::type_only( + ["LocalPrelude"], + "@radroots/local", + )], + constants_ts: None, + kinds_ts: None, + }; + let files = output.files(); + let types = files + .iter() + .find(|file| file.relative_path == "src/generated/types.ts") + .expect("types file"); + let manifest = output.provenance_file(); + + assert_eq!( + types.contents, + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type { LocalPrelude } from \"@radroots/local\";\nimport type { ExternalThing } from \"@radroots/external-bindings\";\n\nexport type SyntheticThing = { external: ExternalThing, };\n" + ); + assert_eq!( + manifest.relative_path, + "contracts/provenance/typescript/core.json" + ); + assert!(manifest.contents.contains("\"generated\": true")); + assert!( + !files + .iter() + .any(|file| file.relative_path == "src/index.ts") + ); + } + + #[test] + fn package_outputs_do_not_generate_package_indices() { + for output in package_outputs().expect("package outputs") { + assert!( + !output + .files() + .iter() + .any(|file| file.relative_path == "src/index.ts"), + "{} index must remain handwritten source", + output.spec.package_name + ); + } + } + + #[test] + fn trade_output_uses_canonical_native_snapshot_and_matches_checked_in_types() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "trade") + .expect("trade output"); + + assert!(matches!( + output.types_ts, + Some(TsSource::CanonicalNativeSnapshot { .. }) + )); + assert!(output.types_imports.is_empty()); + + let types = output + .files() + .into_iter() + .find(|file| file.relative_path == "src/generated/types.ts") + .expect("types file"); + + assert_eq!(types.contents, TRADE_BINDINGS_TYPES_TS); + } + + #[test] + fn event_output_uses_canonical_native_snapshot_and_matches_checked_in_types() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "event") + .expect("event output"); + + assert!(matches!( + output.types_ts, + Some(TsSource::CanonicalNativeSnapshot { .. }) + )); + let types = output + .files() + .into_iter() + .find(|file| file.relative_path == "src/generated/types.ts") + .expect("types file"); + assert_eq!(types.contents, super::EVENT_BINDINGS_TYPES_TS); + + let manifest = output.provenance_file(); + assert!(manifest.contents.contains("canonical_native_type_snapshot")); + assert!(manifest.contents.contains("radroots_event")); + assert!(manifest.contents.contains("radroots_core")); + } + + #[test] + fn replica_schema_output_uses_dto_registry_and_matches_checked_in_types() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "replica_schema") + .expect("replica_schema output"); + + assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_)))); + assert!(output.types_imports.is_empty()); + + let types = output + .files() + .into_iter() + .find(|file| file.relative_path == "src/generated/types.ts") + .expect("types file"); + + assert_eq!(types.contents, REPLICA_SCHEMA_BINDINGS_TYPES_TS); + } + + #[test] + fn events_constants_and_kinds_use_modules_and_match_checked_in_files() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "event") + .expect("event output"); + + assert!(matches!(output.constants_ts, Some(TsSource::Module(_)))); + assert!(matches!(output.kinds_ts, Some(TsSource::Module(_)))); + + let files = output.files(); + let constants = files + .iter() + .find(|file| file.relative_path == "src/generated/constants.ts") + .expect("constants file"); + let kinds = files + .iter() + .find(|file| file.relative_path == "src/generated/kinds.ts") + .expect("kinds file"); + + assert_eq!(constants.contents, EVENT_BINDINGS_CONSTANTS_TS); + assert_eq!(kinds.contents, EVENT_BINDINGS_KINDS_TS); + } + + #[test] + fn identity_constants_use_module_and_match_checked_in_file() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "identity") + .expect("identity output"); + + assert!(matches!(output.constants_ts, Some(TsSource::Module(_)))); + + let constants = output + .files() + .into_iter() + .find(|file| file.relative_path == "src/generated/constants.ts") + .expect("constants file"); + + assert_eq!(constants.contents, IDENTITY_BINDINGS_CONSTANTS_TS); + } + + fn test_module() -> TsSource { + TsSource::Module( + TypeScriptModule::new("src/generated/test.ts").with_declaration( + TypeScriptDeclaration::type_alias("A", TypeScriptType::String), + ), + ) + } +} diff --git a/tools/sdk_xtask_import/src/package_matrix.rs b/tools/sdk_xtask_import/src/package_matrix.rs @@ -0,0 +1,199 @@ +use std::collections::BTreeSet; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PackageSpec { + pub key: &'static str, + pub crate_name: &'static str, + pub crate_dir: &'static str, + pub package_name: &'static str, + pub package_dir: &'static str, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WasmPackageSpec { + pub key: &'static str, + pub crate_name: &'static str, + pub crate_dir: &'static str, + pub package_name: &'static str, + pub package_dir: &'static str, + pub out_name: &'static str, + pub out_dir: &'static str, +} + +pub const PACKAGE_SPECS: [PackageSpec; 5] = [ + PackageSpec { + key: "core", + crate_name: "radroots_core_bindings", + crate_dir: "crates/core_bindings", + package_name: "@radroots/core-bindings", + package_dir: "packages/core-bindings", + }, + PackageSpec { + key: "event", + crate_name: "radroots_event_bindings", + crate_dir: "crates/event_bindings", + package_name: "@radroots/event-bindings", + package_dir: "packages/event-bindings", + }, + PackageSpec { + key: "identity", + crate_name: "radroots_identity_bindings", + crate_dir: "crates/identity_bindings", + package_name: "@radroots/identity-bindings", + package_dir: "packages/identity-bindings", + }, + PackageSpec { + key: "replica_schema", + crate_name: "radroots_replica_schema_bindings", + crate_dir: "crates/replica_schema_bindings", + package_name: "@radroots/replica-schema-bindings", + package_dir: "packages/replica-schema-bindings", + }, + PackageSpec { + key: "trade", + crate_name: "radroots_trade_bindings", + crate_dir: "crates/trade_bindings", + package_name: "@radroots/trade-bindings", + package_dir: "packages/trade-bindings", + }, +]; + +pub const WASM_PACKAGE_SPECS: [WasmPackageSpec; 3] = [ + WasmPackageSpec { + key: "event_codec", + crate_name: "radroots_event_codec_wasm", + crate_dir: "crates/event_codec_wasm", + package_name: "@radroots/event-codec-wasm", + package_dir: "packages/event-codec-wasm", + out_name: "radroots_event_codec_wasm", + out_dir: "../../packages/event-codec-wasm/dist", + }, + WasmPackageSpec { + key: "replica_store", + crate_name: "radroots_replica_store_wasm", + crate_dir: "crates/replica_store_wasm", + package_name: "@radroots/replica-store-wasm", + package_dir: "packages/replica-store-wasm", + out_name: "radroots_replica_store_wasm", + out_dir: "../../packages/replica-store-wasm/dist", + }, + WasmPackageSpec { + key: "replica_sync", + crate_name: "radroots_replica_sync_wasm", + crate_dir: "crates/replica_sync_wasm", + package_name: "@radroots/replica-sync-wasm", + package_dir: "packages/replica-sync-wasm", + out_name: "radroots_replica_sync_wasm", + out_dir: "../../packages/replica-sync-wasm/dist", + }, +]; + +pub const FORBIDDEN_PACKAGE_NAMES: [&str; 2] = + ["@radroots/tangle-db-schema-bindings", "@radroots/contracts"]; + +pub fn package_specs() -> &'static [PackageSpec] { + &PACKAGE_SPECS +} + +pub fn wasm_package_specs() -> &'static [WasmPackageSpec] { + &WASM_PACKAGE_SPECS +} + +pub fn validate_package_matrix() -> Result<(), String> { + let mut crate_names = BTreeSet::new(); + let mut package_names = BTreeSet::new(); + let mut package_dirs = BTreeSet::new(); + for spec in package_specs() { + if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) { + return Err(format!( + "forbidden package in matrix: {}", + spec.package_name + )); + } + if !crate_names.insert(spec.crate_name) { + return Err(format!("duplicate crate in matrix: {}", spec.crate_name)); + } + if !package_names.insert(spec.package_name) { + return Err(format!( + "duplicate package in matrix: {}", + spec.package_name + )); + } + if !package_dirs.insert(spec.package_dir) { + return Err(format!( + "duplicate package directory in matrix: {}", + spec.package_dir + )); + } + } + for spec in wasm_package_specs() { + if FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name) { + return Err(format!( + "forbidden package in matrix: {}", + spec.package_name + )); + } + if !crate_names.insert(spec.crate_name) { + return Err(format!("duplicate crate in matrix: {}", spec.crate_name)); + } + if !package_names.insert(spec.package_name) { + return Err(format!( + "duplicate package in matrix: {}", + spec.package_name + )); + } + if !package_dirs.insert(spec.package_dir) { + return Err(format!( + "duplicate package directory in matrix: {}", + spec.package_dir + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + FORBIDDEN_PACKAGE_NAMES, package_specs, validate_package_matrix, wasm_package_specs, + }; + + #[test] + fn package_matrix_is_valid() { + validate_package_matrix().expect("package matrix is valid"); + } + + #[test] + fn approved_package_count_is_stable() { + assert_eq!(package_specs().len(), 5); + assert_eq!(wasm_package_specs().len(), 3); + } + + #[test] + fn forbidden_names_are_absent() { + for spec in package_specs() { + assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name)); + } + for spec in wasm_package_specs() { + assert!(!FORBIDDEN_PACKAGE_NAMES.contains(&spec.package_name)); + } + } + + #[test] + fn replica_schema_package_uses_current_name() { + assert!( + package_specs() + .iter() + .any(|spec| spec.package_name == "@radroots/replica-schema-bindings") + ); + } + + #[test] + fn wasm_packages_use_sdk_package_names() { + assert!( + wasm_package_specs() + .iter() + .any(|spec| spec.package_name == "@radroots/replica-store-wasm") + ); + } +} diff --git a/tools/sdk_xtask_import/src/package_metadata.rs b/tools/sdk_xtask_import/src/package_metadata.rs @@ -0,0 +1,181 @@ +use std::{fs, path::Path}; + +use crate::{ + fs::{write_bytes_if_changed, write_if_changed}, + package_matrix::{package_specs, wasm_package_specs}, +}; + +pub(crate) const PACKAGE_README_FILE: &str = "README.md"; +pub(crate) const PACKAGE_LICENSE_FILES: [&str; 2] = ["LICENSE-MIT", "LICENSE-APACHE"]; +pub(crate) const PACKAGE_FILES: [&str; 4] = [ + "dist", + PACKAGE_README_FILE, + PACKAGE_LICENSE_FILES[0], + PACKAGE_LICENSE_FILES[1], +]; + +pub(crate) fn generate_package_metadata(root: &Path) -> Result<(), String> { + for spec in package_specs() { + write_package_metadata(root, &root.join(spec.package_dir))?; + println!("generated package metadata {}", spec.package_name); + } + for spec in wasm_package_specs() { + write_package_metadata(root, &root.join(spec.package_dir))?; + println!("generated package metadata {}", spec.package_name); + } + Ok(()) +} + +pub(crate) fn check_package_distribution_metadata( + root: &Path, + package_dir: &Path, + package_json_path: &Path, + json: &serde_json::Value, +) -> Result<(), String> { + let package_name = package_name(json, package_json_path)?; + let description = package_description(json, package_json_path)?; + let readme_path = package_dir.join(PACKAGE_README_FILE); + let expected_readme = package_readme(package_name, description); + check_text_file(&readme_path, &expected_readme, "stale package README")?; + for file_name in PACKAGE_LICENSE_FILES { + let source_path = root.join(file_name); + let package_path = package_dir.join(file_name); + let expected = fs::read(&source_path) + .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; + let actual = fs::read(&package_path) + .map_err(|error| format!("failed to read {}: {error}", package_path.display()))?; + if actual != expected { + return Err(format!( + "stale package license metadata: {}", + package_path.display() + )); + } + } + Ok(()) +} + +pub(crate) fn package_description<'a>( + json: &'a serde_json::Value, + package_json_path: &Path, +) -> Result<&'a str, String> { + let description = json + .get("description") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| { + format!( + "package.json missing description: {}", + package_json_path.display() + ) + })?; + if description.trim().is_empty() || description.trim() != description { + return Err(format!( + "package.json description must be non-empty and trimmed: {}", + package_json_path.display() + )); + } + Ok(description) +} + +pub(crate) fn package_readme(package_name: &str, description: &str) -> String { + format!( + "# {package_name}\n\n{description}\n\nThis package publishes generated ESM JavaScript, TypeScript declarations, and any runtime artifacts from the Radroots SDK build pipeline. Runtime files are distributed from `dist/`; source and provenance metadata are kept outside the npm package payload.\n\n## License\n\nLicensed under either MIT or Apache-2.0, at your option. See `LICENSE-MIT` and `LICENSE-APACHE`.\n" + ) +} + +fn write_package_metadata(root: &Path, package_dir: &Path) -> Result<bool, String> { + let package_json_path = package_dir.join("package.json"); + let json = read_package_json(&package_json_path)?; + let package_name = package_name(&json, &package_json_path)?; + let description = package_description(&json, &package_json_path)?; + let mut changed = write_if_changed( + &package_dir.join(PACKAGE_README_FILE), + &package_readme(package_name, description), + )?; + for file_name in PACKAGE_LICENSE_FILES { + let source_path = root.join(file_name); + let contents = fs::read(&source_path) + .map_err(|error| format!("failed to read {}: {error}", source_path.display()))?; + changed |= write_bytes_if_changed(&package_dir.join(file_name), &contents)?; + } + Ok(changed) +} + +fn read_package_json(path: &Path) -> Result<serde_json::Value, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + serde_json::from_str::<serde_json::Value>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn package_name<'a>( + json: &'a serde_json::Value, + package_json_path: &Path, +) -> Result<&'a str, String> { + json.get("name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| format!("package.json missing name: {}", package_json_path.display())) +} + +fn check_text_file(path: &Path, expected: &str, label: &str) -> Result<(), String> { + let actual = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if actual != expected { + return Err(format!("{label}: {}", path.display())); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + + use super::{package_readme, write_package_metadata}; + + #[test] + fn package_metadata_generation_is_deterministic() { + let root = test_root("metadata_generation"); + let package_dir = root.join("packages/example"); + fs::create_dir_all(&package_dir).expect("create package"); + fs::write(root.join("LICENSE-MIT"), "MIT license\n").expect("write MIT license"); + fs::write(root.join("LICENSE-APACHE"), "Apache license\n").expect("write Apache license"); + fs::write( + package_dir.join("package.json"), + r#"{ + "name": "@radroots/example", + "description": "Example package" +}"#, + ) + .expect("write package json"); + + assert!(write_package_metadata(&root, &package_dir).expect("first generation")); + assert_eq!( + fs::read_to_string(package_dir.join("README.md")).expect("read README"), + package_readme("@radroots/example", "Example package") + ); + assert_eq!( + fs::read_to_string(package_dir.join("LICENSE-MIT")).expect("read MIT"), + "MIT license\n" + ); + assert!(!write_package_metadata(&root, &package_dir).expect("second generation")); + + let _ = fs::remove_dir_all(root); + } + + fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time after epoch") + .as_nanos(); + let root = std::env::temp_dir().join(format!( + "radroots_sdk_xtask_package_metadata_{name}_{}_{}", + std::process::id(), + stamp + )); + let _ = fs::remove_dir_all(&root); + root + } +} diff --git a/tools/sdk_xtask_import/src/portable_qualification.rs b/tools/sdk_xtask_import/src/portable_qualification.rs @@ -0,0 +1,58 @@ +use std::{fs, path::Path}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct PortableMatrix { + schema_version: u32, + no_std_target: String, + no_std_cross_compiler: String, + no_std_cross_cflags: String, + wasm_target: String, + portable_public_packages: Vec<String>, + std_only_public_packages: Vec<String>, +} + +pub fn run(workspace_root: &Path) -> Result<(), String> { + load(workspace_root).map(|_| ()) +} + +fn load(workspace_root: &Path) -> Result<PortableMatrix, String> { + let path = workspace_root.join("contracts/releases/portable_matrix.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let matrix = toml::from_str::<PortableMatrix>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + if matrix.schema_version != 1 + || matrix.no_std_target != "thumbv7em-none-eabihf" + || matrix.no_std_cross_compiler != "zig cc" + || matrix.no_std_cross_cflags != "-target thumb-freestanding-eabihf -mcpu=cortex_m4" + || matrix.wasm_target != "wasm32-unknown-unknown" + || !matrix.portable_public_packages.is_empty() + || matrix.std_only_public_packages != ["radroots", "radroots_sdk"] + { + return Err( + "SDK portability contract must classify both public front doors as std-only".to_owned(), + ); + } + Ok(matrix) +} + +#[cfg(test)] +mod tests { + use super::load; + + #[test] + fn current_contract_rejects_nominal_front_door_portability() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(std::path::Path::parent) + .expect("workspace root"); + let matrix = load(root).expect("portable matrix"); + assert!(matrix.portable_public_packages.is_empty()); + assert_eq!( + matrix.std_only_public_packages, + ["radroots", "radroots_sdk"] + ); + } +} diff --git a/tools/sdk_xtask_import/src/release_graph.rs b/tools/sdk_xtask_import/src/release_graph.rs @@ -0,0 +1,687 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, + process::Command, +}; + +use serde::Deserialize; + +const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml"; +const SPEC_ID: &str = "radroots.crates.release.v1"; +const RELEASE_VERSION: &str = "0.1.0-alpha"; +const REGISTRY_KINDS: &[&str] = &["build", "normal"]; +const ALL_KINDS: &[&str] = &["build", "dev", "normal"]; + +#[derive(Debug, Deserialize)] +struct Architecture { + spec_id: String, + package_count: usize, + repositories: ArchitectureRepositories, + package: Vec<ArchitecturePackage>, +} + +#[derive(Debug, Deserialize)] +struct ArchitectureRepositories { + lib: ArchitectureRepository, + sdk: ArchitectureRepository, +} + +#[derive(Debug, Deserialize)] +struct ArchitectureRepository { + url: String, + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct ArchitecturePackage { + name: String, + publish_order_hint: usize, + #[serde(default)] + required_radroots_dependencies: Vec<String>, + #[serde(default)] + optional_radroots_dependencies: Vec<String>, + #[serde(flatten)] + _other: BTreeMap<String, toml::Value>, +} + +#[derive(Debug, Deserialize)] +struct CargoMetadata { + packages: Vec<CargoPackage>, + workspace_members: Vec<String>, + resolve: Option<CargoResolve>, +} + +#[derive(Debug, Deserialize)] +struct CargoPackage { + id: String, + name: String, + version: String, + source: Option<String>, + manifest_path: String, + #[serde(default)] + dependencies: Vec<CargoDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependency { + name: String, + rename: Option<String>, + #[serde(default)] + kind: Option<String>, + #[serde(default)] + features: Vec<String>, + #[serde(default)] + target: Option<String>, + #[serde(default = "default_true")] + uses_default_features: bool, +} + +#[derive(Debug, Deserialize)] +struct CargoResolve { + nodes: Vec<CargoNode>, +} + +#[derive(Debug, Deserialize)] +struct CargoNode { + id: String, + #[serde(default)] + deps: Vec<CargoNodeDependency>, +} + +#[derive(Debug, Deserialize)] +struct CargoNodeDependency { + name: String, + pkg: String, + #[serde(default)] + dep_kinds: Vec<CargoDependencyKind>, +} + +#[derive(Debug, Deserialize)] +struct CargoDependencyKind { + #[serde(default)] + kind: Option<String>, + #[serde(default)] + target: Option<String>, +} + +fn default_true() -> bool { + true +} + +pub fn run(workspace_root: &Path) -> Result<(), String> { + let architecture = load_architecture(workspace_root)?; + let metadata = load_metadata(workspace_root)?; + let order = validate(&architecture, &metadata)?; + println!("resolved publication order:"); + for (index, package) in order.iter().enumerate() { + println!("{:02} {package}", index + 1); + } + Ok(()) +} + +fn load_architecture(workspace_root: &Path) -> Result<Architecture, String> { + let path = workspace_root.join(ARCHITECTURE_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("parse {}: {error}", path.display())) +} + +fn load_metadata(workspace_root: &Path) -> Result<CargoMetadata, String> { + let output = Command::new("cargo") + .args([ + "metadata", + "--format-version", + "1", + "--locked", + "--all-features", + ]) + .current_dir(workspace_root) + .output() + .map_err(|error| format!("run cargo metadata: {error}"))?; + if !output.status.success() { + return Err(format!( + "cargo metadata failed while resolving the release graph: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); + } + serde_json::from_slice(&output.stdout) + .map_err(|error| format!("parse cargo metadata release graph: {error}")) +} + +fn validate(architecture: &Architecture, metadata: &CargoMetadata) -> Result<Vec<String>, String> { + let hints = validate_architecture(architecture)?; + let approved = hints.keys().copied().collect::<BTreeSet<_>>(); + let packages = unique_packages(metadata)?; + let nodes = unique_nodes(metadata)?; + let workspace_members = metadata + .workspace_members + .iter() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let facade_is_local = metadata.workspace_members.iter().any(|id| { + packages + .get(id.as_str()) + .is_some_and(|package| package.name == "radroots") + }); + + let mut selected = BTreeSet::new(); + for name in &approved { + let matching = packages + .values() + .filter(|package| package.name == **name) + .copied() + .collect::<Vec<_>>(); + if matching.len() > 1 { + return Err(format!( + "release package {name} resolves to multiple package identities" + )); + } + let Some(package) = matching.first().copied() else { + if facade_is_local { + return Err(format!( + "complete SDK release graph is missing approved package {name}" + )); + } + continue; + }; + if facade_is_local || workspace_members.contains(package.id.as_str()) { + selected.insert(*name); + } + if package.version != RELEASE_VERSION { + return Err(format!( + "release package {name} resolved version {} instead of {RELEASE_VERSION}", + package.version + )); + } + validate_package_source(architecture, package, &workspace_members)?; + if !nodes.contains_key(package.id.as_str()) { + return Err(format!( + "release package {name} has no Cargo resolve node; manifest={}", + package.manifest_path + )); + } + } + if selected.is_empty() { + return Err("release graph selected no approved workspace packages".to_owned()); + } + if facade_is_local && selected.len() != architecture.package_count { + return Err(format!( + "complete SDK release graph selected {} packages instead of {}", + selected.len(), + architecture.package_count + )); + } + + let mut dependency_sets = selected + .iter() + .map(|name| (*name, BTreeSet::new())) + .collect::<BTreeMap<_, _>>(); + let mut violations = Vec::new(); + + for owner_name in &selected { + let owner = packages + .values() + .find(|package| package.name == **owner_name) + .copied() + .ok_or_else(|| format!("selected release package {owner_name} disappeared"))?; + let node = nodes + .get(owner.id.as_str()) + .copied() + .ok_or_else(|| format!("release package {owner_name} has no resolve node"))?; + for edge in &node.deps { + let dependency = packages + .get(edge.pkg.as_str()) + .copied() + .ok_or_else(|| format!("resolved dependency {} has no package record", edge.pkg))?; + if edge.dep_kinds.is_empty() { + return Err(format!( + "resolved edge {} -> {} has no dependency kind", + owner.name, dependency.name + )); + } + for edge_kind in &edge.dep_kinds { + let kind = normalized_kind(edge_kind.kind.as_deref()); + if !ALL_KINDS.contains(&kind) { + return Err(format!( + "resolved edge {} -> {} uses unsupported dependency kind {kind}", + owner.name, dependency.name + )); + } + let declaration = resolve_declaration(owner, dependency, edge_kind)?; + if is_radroots_family(&dependency.name) + && !approved.contains(dependency.name.as_str()) + { + violations.push(format_edge( + owner, + dependency, + edge, + edge_kind, + declaration, + "public-to-private edge", + )); + } + if REGISTRY_KINDS.contains(&kind) && selected.contains(dependency.name.as_str()) { + dependency_sets + .get_mut(owner_name) + .expect("selected owner dependency set") + .insert(dependency.name.as_str()); + } + } + } + } + if !violations.is_empty() { + violations.sort(); + return Err(format!( + "release graph contains forbidden edge(s):\n{}", + violations.join("\n") + )); + } + + validate_declared_architecture_edges(architecture, &selected, &dependency_sets)?; + let order = dependency_first_order(&selected, &dependency_sets, &hints)?; + let expected = selected.iter().copied().collect::<Vec<_>>(); + let mut expected = expected; + expected.sort_by_key(|name| hints[name]); + if order != expected { + return Err(format!( + "Cargo-derived publication order [{}] differs from architecture order [{}]", + order.join(", "), + expected.join(", ") + )); + } + Ok(order.into_iter().map(str::to_owned).collect()) +} + +fn validate_package_source( + architecture: &Architecture, + package: &CargoPackage, + workspace_members: &BTreeSet<&str>, +) -> Result<(), String> { + if workspace_members.contains(package.id.as_str()) { + return if package.source.is_none() { + Ok(()) + } else { + Err(format!( + "workspace release package {} must resolve locally; source={:?}", + package.name, package.source + )) + }; + } + + let repository = [ + &architecture.repositories.lib, + &architecture.repositories.sdk, + ] + .into_iter() + .find(|repository| repository.packages.iter().any(|name| name == &package.name)) + .ok_or_else(|| { + format!( + "release package {} has no repository allocation", + package.name + ) + })?; + let source = package.source.as_deref().ok_or_else(|| { + format!( + "external release package {} must resolve from an exact Git revision", + package.name + ) + })?; + validate_exact_git_source(source, &repository.url).map_err(|reason| { + format!( + "external release package {} has invalid source {source}: {reason}", + package.name + ) + }) +} + +fn validate_exact_git_source(source: &str, repository_url: &str) -> Result<(), &'static str> { + let canonical_url = repository_url + .trim_end_matches('/') + .trim_end_matches(".git"); + let prefix = format!("git+{canonical_url}.git?rev="); + let revision_and_commit = source + .strip_prefix(&prefix) + .ok_or("repository URL or exact-revision query does not match its allocation")?; + let (revision, commit) = revision_and_commit + .split_once('#') + .ok_or("resolved commit fragment is absent")?; + if revision.len() != 40 + || commit.len() != 40 + || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) + || !commit.bytes().all(|byte| byte.is_ascii_hexdigit()) + { + return Err("revision and resolved commit must be full hexadecimal object IDs"); + } + if revision != commit { + return Err("resolved commit does not equal the requested exact revision"); + } + Ok(()) +} + +fn validate_architecture(architecture: &Architecture) -> Result<BTreeMap<&str, usize>, String> { + if architecture.spec_id != SPEC_ID { + return Err(format!("architecture spec_id must be {SPEC_ID}")); + } + if architecture.package_count != 19 || architecture.package.len() != 19 { + return Err("architecture must define exactly 19 release packages".to_owned()); + } + let mut hints = BTreeMap::new(); + let mut seen_hints = BTreeSet::new(); + for package in &architecture.package { + if hints + .insert(package.name.as_str(), package.publish_order_hint) + .is_some() + { + return Err(format!("duplicate architecture package {}", package.name)); + } + if !seen_hints.insert(package.publish_order_hint) { + return Err(format!( + "duplicate publish_order_hint {}", + package.publish_order_hint + )); + } + } + if seen_hints != (1..=19).collect::<BTreeSet<_>>() { + return Err("publish_order_hint values must be the exact range 1..=19".to_owned()); + } + Ok(hints) +} + +fn unique_packages(metadata: &CargoMetadata) -> Result<BTreeMap<&str, &CargoPackage>, String> { + let mut packages = BTreeMap::new(); + for package in &metadata.packages { + if packages.insert(package.id.as_str(), package).is_some() { + return Err(format!("duplicate Cargo package id {}", package.id)); + } + } + Ok(packages) +} + +fn unique_nodes(metadata: &CargoMetadata) -> Result<BTreeMap<&str, &CargoNode>, String> { + let resolve = metadata + .resolve + .as_ref() + .ok_or_else(|| "cargo metadata did not include a resolved release graph".to_owned())?; + let mut nodes = BTreeMap::new(); + for node in &resolve.nodes { + if nodes.insert(node.id.as_str(), node).is_some() { + return Err(format!("duplicate Cargo resolve node {}", node.id)); + } + } + Ok(nodes) +} + +fn resolve_declaration<'a>( + owner: &'a CargoPackage, + dependency: &CargoPackage, + edge_kind: &CargoDependencyKind, +) -> Result<&'a CargoDependency, String> { + let kind = normalized_kind(edge_kind.kind.as_deref()); + let candidates = owner + .dependencies + .iter() + .filter(|candidate| { + candidate.name == dependency.name + && normalized_kind(candidate.kind.as_deref()) == kind + && candidate.target == edge_kind.target + }) + .collect::<Vec<_>>(); + match candidates.as_slice() { + [declaration] => Ok(*declaration), + [] => Err(format!( + "resolved edge {} -> {} kind={kind} target={} has no matching declaration", + owner.name, + dependency.name, + target_label(edge_kind.target.as_deref()) + )), + _ => Err(format!( + "resolved edge {} -> {} kind={kind} target={} has ambiguous declarations", + owner.name, + dependency.name, + target_label(edge_kind.target.as_deref()) + )), + } +} + +fn format_edge( + owner: &CargoPackage, + dependency: &CargoPackage, + edge: &CargoNodeDependency, + edge_kind: &CargoDependencyKind, + declaration: &CargoDependency, + reason: &str, +) -> String { + let features = declaration + .features + .iter() + .cloned() + .collect::<BTreeSet<_>>() + .into_iter() + .collect::<Vec<_>>() + .join(","); + format!( + "{reason}: {} -> {}; alias={}; declaration_alias={}; kind={}; target={}; features=[{}]; default_features={}; owner_manifest={}; dependency_manifest={}", + owner.name, + dependency.name, + edge.name, + declaration.rename.as_deref().unwrap_or(&declaration.name), + normalized_kind(edge_kind.kind.as_deref()), + target_label(edge_kind.target.as_deref()), + features, + declaration.uses_default_features, + owner.manifest_path, + dependency.manifest_path, + ) +} + +fn validate_declared_architecture_edges<'a>( + architecture: &'a Architecture, + selected: &BTreeSet<&'a str>, + resolved: &BTreeMap<&'a str, BTreeSet<&'a str>>, +) -> Result<(), String> { + for package in &architecture.package { + if !selected.contains(package.name.as_str()) { + continue; + } + for optional in &package.optional_radroots_dependencies { + if !selected.contains(optional.as_str()) { + return Err(format!( + "{} optional architecture dependency {optional} is outside the selected public graph", + package.name + )); + } + } + let declared = package + .required_radroots_dependencies + .iter() + .map(String::as_str) + .filter(|dependency| selected.contains(dependency)) + .collect::<BTreeSet<_>>(); + let actual = resolved + .get(package.name.as_str()) + .ok_or_else(|| format!("missing resolved dependency set for {}", package.name))?; + if !declared.is_subset(actual) { + let missing = declared.difference(actual).copied().collect::<Vec<_>>(); + return Err(format!( + "{} architecture dependency declarations are absent from the all-feature Cargo graph: {}", + package.name, + missing.join(", ") + )); + } + } + Ok(()) +} + +fn dependency_first_order<'a>( + selected: &BTreeSet<&'a str>, + dependency_sets: &BTreeMap<&'a str, BTreeSet<&'a str>>, + hints: &BTreeMap<&'a str, usize>, +) -> Result<Vec<&'a str>, String> { + let mut remaining = dependency_sets.clone(); + let mut order = Vec::with_capacity(selected.len()); + while !remaining.is_empty() { + let next = remaining + .iter() + .filter(|(_, dependencies)| dependencies.iter().all(|name| order.contains(name))) + .map(|(name, _)| *name) + .min_by_key(|name| hints[name]); + let Some(next) = next else { + let blocked = remaining + .iter() + .map(|(name, dependencies)| { + format!( + "{name}->[{}]", + dependencies.iter().copied().collect::<Vec<_>>().join(",") + ) + }) + .collect::<Vec<_>>() + .join("; "); + return Err(format!( + "release graph contains a publication cycle: {blocked}" + )); + }; + remaining.remove(next); + order.push(next); + } + Ok(order) +} + +fn is_radroots_family(name: &str) -> bool { + name == "radroots" || name.starts_with("radroots_") +} + +fn normalized_kind(kind: Option<&str>) -> &str { + kind.unwrap_or("normal") +} + +fn target_label(target: Option<&str>) -> &str { + target.unwrap_or("all") +} + +#[cfg(test)] +mod tests { + use super::{Architecture, CargoMetadata, validate, validate_exact_git_source}; + + const ARCHITECTURE: &str = include_str!("../../../docs/specs/radroots_crates_release_v1.toml"); + + #[test] + fn current_architecture_contract_has_a_complete_order() { + let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture"); + let hints = super::validate_architecture(&architecture).expect("valid order"); + assert_eq!(hints.len(), 19); + assert_eq!(hints["radroots_core"], 1); + assert_eq!(hints["radroots"], 19); + } + + #[test] + fn exact_cross_repository_git_sources_require_the_requested_commit() { + const REVISION: &str = "f7f456d906d91aadbb9fac4850afaa8c5723c3e9"; + let source = + format!("git+https://github.com/radrootslabs/lib.git?rev={REVISION}#{REVISION}"); + validate_exact_git_source(&source, "https://github.com/radrootslabs/lib") + .expect("exact source"); + + let drifted = format!( + "git+https://github.com/radrootslabs/lib.git?rev={REVISION}#{}", + "1".repeat(40) + ); + assert!( + validate_exact_git_source(&drifted, "https://github.com/radrootslabs/lib").is_err() + ); + assert!( + validate_exact_git_source( + "registry+https://github.com/rust-lang/crates.io-index", + "https://github.com/radrootslabs/lib" + ) + .is_err() + ); + } + + #[test] + fn metadata_without_resolve_graph_fails_closed() { + let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture"); + let metadata: CargoMetadata = + serde_json::from_str(r#"{"packages":[],"workspace_members":[],"resolve":null}"#) + .expect("metadata"); + let error = validate(&architecture, &metadata).expect_err("missing graph"); + assert!( + error.contains("selected no approved workspace packages") + || error.contains("resolved release graph") + ); + } + + #[test] + fn public_to_private_target_edge_reports_complete_provenance() { + let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture"); + let metadata: CargoMetadata = serde_json::from_str( + r#"{ + "packages": [ + { + "id": "core", + "name": "radroots_core", + "version": "0.1.0-alpha", + "source": null, + "manifest_path": "/capsule/crates/core/Cargo.toml", + "dependencies": [{ + "name": "radroots_private_runtime", + "rename": "private_alias", + "kind": "build", + "features": ["danger"], + "target": "cfg(unix)", + "uses_default_features": false + }] + }, + { + "id": "private", + "name": "radroots_private_runtime", + "version": "0.1.0-alpha", + "source": null, + "manifest_path": "/capsule/crates/private/Cargo.toml", + "dependencies": [] + } + ], + "workspace_members": ["core"], + "resolve": {"nodes": [ + {"id": "core", "deps": [{ + "name": "private_alias", + "pkg": "private", + "dep_kinds": [{"kind": "build", "target": "cfg(unix)"}] + }]}, + {"id": "private", "deps": []} + ]} + }"#, + ) + .expect("metadata"); + let error = validate(&architecture, &metadata).expect_err("private edge"); + assert!(error.contains("public-to-private edge")); + assert!(error.contains("alias=private_alias")); + assert!(error.contains("kind=build")); + assert!(error.contains("target=cfg(unix)")); + assert!(error.contains("features=[danger]")); + assert!(error.contains("default_features=false")); + } + + #[test] + fn registry_graph_cycle_fails_closed() { + let architecture = toml::from_str::<Architecture>(ARCHITECTURE).expect("architecture"); + let metadata: CargoMetadata = serde_json::from_str( + r#"{ + "packages": [ + {"id":"core","name":"radroots_core","version":"0.1.0-alpha","source":null,"manifest_path":"/core/Cargo.toml","dependencies":[{"name":"radroots_identity","rename":null,"kind":null,"features":[],"target":null,"uses_default_features":true}]}, + {"id":"identity","name":"radroots_identity","version":"0.1.0-alpha","source":null,"manifest_path":"/identity/Cargo.toml","dependencies":[{"name":"radroots_core","rename":null,"kind":null,"features":[],"target":null,"uses_default_features":true}]} + ], + "workspace_members":["core","identity"], + "resolve":{"nodes":[ + {"id":"core","deps":[{"name":"radroots_identity","pkg":"identity","dep_kinds":[{"kind":null,"target":null}]}]}, + {"id":"identity","deps":[{"name":"radroots_core","pkg":"core","dep_kinds":[{"kind":null,"target":null}]}]} + ]} + }"#, + ) + .expect("metadata"); + let error = validate(&architecture, &metadata).expect_err("cycle"); + assert!(error.contains("publication cycle")); + assert!(error.contains("radroots_core")); + assert!(error.contains("radroots_identity")); + } +} diff --git a/tools/sdk_xtask_import/src/release_qualification.rs b/tools/sdk_xtask_import/src/release_qualification.rs @@ -0,0 +1,135 @@ +use std::{fs, path::Path, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Architecture { + repositories: Repositories, + package: Vec<Package>, +} + +#[derive(Debug, Deserialize)] +struct Repositories { + sdk: Repository, +} + +#[derive(Debug, Deserialize)] +struct Repository { + packages: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct Package { + name: String, + features: Vec<String>, +} + +#[derive(Debug, PartialEq, Eq)] +struct CargoInvocation { + args: Vec<String>, +} + +pub fn run_feature_matrix(workspace_root: &Path) -> Result<(), String> { + for invocation in feature_matrix(workspace_root)? { + eprintln!("cargo {}", invocation.args.join(" ")); + let status = Command::new("cargo") + .args(&invocation.args) + .current_dir(workspace_root) + .status() + .map_err(|error| format!("failed to start cargo: {error}"))?; + if !status.success() { + return Err(format!( + "public feature qualification failed: cargo {}", + invocation.args.join(" ") + )); + } + } + Ok(()) +} + +fn feature_matrix(workspace_root: &Path) -> Result<Vec<CargoInvocation>, String> { + let path = workspace_root.join("docs/specs/radroots_crates_release_v1.toml"); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let architecture = toml::from_str::<Architecture>(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let mut packages = architecture + .package + .into_iter() + .filter(|package| { + architecture + .repositories + .sdk + .packages + .contains(&package.name) + }) + .collect::<Vec<_>>(); + packages.sort_by(|left, right| left.name.cmp(&right.name)); + if packages.len() != 2 { + return Err(format!( + "SDK feature qualification requires exactly 2 public packages, found {}", + packages.len() + )); + } + + let mut invocations = Vec::new(); + for package in packages { + invocations.push(check_invocation(&package.name, None, true)); + invocations.push(check_invocation(&package.name, None, false)); + for feature in package.features { + invocations.push(check_invocation(&package.name, Some(&feature), true)); + } + invocations.push(CargoInvocation { + args: vec![ + "check".to_owned(), + "-p".to_owned(), + package.name, + "--all-targets".to_owned(), + "--all-features".to_owned(), + "--locked".to_owned(), + ], + }); + } + Ok(invocations) +} + +fn check_invocation( + package: &str, + feature: Option<&str>, + no_default_features: bool, +) -> CargoInvocation { + let mut args = vec!["check".to_owned(), "-p".to_owned(), package.to_owned()]; + if no_default_features { + args.push("--lib".to_owned()); + args.push("--no-default-features".to_owned()); + } else { + args.push("--all-targets".to_owned()); + } + if let Some(feature) = feature { + args.push("--features".to_owned()); + args.push(feature.to_owned()); + } + args.push("--locked".to_owned()); + CargoInvocation { args } +} + +#[cfg(test)] +mod tests { + use super::feature_matrix; + + #[test] + fn current_catalog_generates_both_front_door_feature_matrices() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(std::path::Path::parent) + .expect("workspace root"); + let matrix = feature_matrix(root).expect("feature matrix"); + assert!(matrix.len() > 20); + for package in ["radroots", "radroots_sdk"] { + assert!(matrix.iter().any(|entry| { + entry.args.windows(2).any(|pair| pair == ["-p", package]) + && entry.args.iter().any(|arg| arg == "--all-features") + })); + } + } +} diff --git a/tools/sdk_xtask_import/src/smoke.rs b/tools/sdk_xtask_import/src/smoke.rs @@ -0,0 +1,236 @@ +use std::{fs, path::Path, process::Command}; + +use crate::{ + fs::workspace_root, + wasm::{ResolvedRustToolchain, resolve_rust_toolchain}, +}; + +pub fn run(args: &[String]) -> Result<(), String> { + match args { + [target] if target == "sdk-rust-local" => sdk_rust_local(), + [target] if target == "facade-rust-local" => facade_rust_local(), + [target] if target == "front-doors-rust-local" => { + facade_rust_local()?; + sdk_rust_local() + } + _ => Err( + "usage: cargo xtask smoke facade-rust-local | sdk-rust-local | front-doors-rust-local" + .to_owned(), + ), + } +} + +fn facade_rust_local() -> Result<(), String> { + let root = workspace_root()?; + let facade_path = root.join("crates/radroots"); + let lib_root = sibling_lib_root(&root)?; + let toolchain = resolve_rust_toolchain(&root)?; + run_clean_consumer( + "facade", + &render_facade_consumer_manifest(&facade_path, &lib_root)?, + FACADE_CONSUMER_MAIN, + &toolchain, + ) +} + +fn sdk_rust_local() -> Result<(), String> { + let root = workspace_root()?; + let sdk_path = root.join("crates/sdk"); + let lib_root = sibling_lib_root(&root)?; + let toolchain = resolve_rust_toolchain(&root)?; + run_clean_consumer( + "SDK", + &render_sdk_consumer_manifest(&sdk_path, &lib_root)?, + SDK_CONSUMER_MAIN, + &toolchain, + ) +} + +fn sibling_lib_root(root: &Path) -> Result<std::path::PathBuf, String> { + Ok(root + .parent() + .ok_or_else(|| format!("{} has no repository parent", root.display()))? + .join("lib")) +} + +fn run_clean_consumer( + label: &str, + manifest: &str, + main: &str, + toolchain: &ResolvedRustToolchain, +) -> Result<(), String> { + let tempdir = + tempfile::tempdir().map_err(|error| format!("failed to create smoke tempdir: {error}"))?; + fs::write(tempdir.path().join("Cargo.toml"), manifest) + .map_err(|error| format!("failed to write {label} smoke manifest: {error}"))?; + let src_dir = tempdir.path().join("src"); + fs::create_dir_all(&src_dir) + .map_err(|error| format!("failed to create {}: {error}", src_dir.display()))?; + fs::write(src_dir.join("main.rs"), main) + .map_err(|error| format!("failed to write {label} smoke consumer: {error}"))?; + let status = smoke_cargo_run_command(tempdir.path(), toolchain) + .status() + .map_err(|error| format!("failed to run {label} smoke consumer: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!( + "{label} Rust local smoke failed with status {status}" + )) + } +} + +fn render_facade_consumer_manifest(facade_path: &Path, lib_root: &Path) -> Result<String, String> { + let facade_path = serde_json::to_string(&facade_path.to_string_lossy()) + .map_err(|error| format!("failed to render facade path: {error}"))?; + let mut manifest = format!( + r#"[package] +name = "radroots_facade_host_smoke" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +radroots = {{ path = {facade_path} }} + +[patch.crates-io] +"#, + ); + append_lib_patches(&mut manifest, lib_root)?; + Ok(manifest) +} + +fn render_sdk_consumer_manifest(sdk_path: &Path, lib_root: &Path) -> Result<String, String> { + let sdk_path = serde_json::to_string(&sdk_path.to_string_lossy()) + .map_err(|error| format!("failed to render SDK path: {error}"))?; + let mut manifest = format!( + r#"[package] +name = "radroots_sdk_host_smoke" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +radroots_sdk = {{ path = {sdk_path}, default-features = true }} + +[patch.crates-io] +"#, + ); + append_lib_patches(&mut manifest, lib_root)?; + Ok(manifest) +} + +fn append_lib_patches(manifest: &mut String, lib_root: &Path) -> Result<(), String> { + for (package, relative_path) in [ + ("radroots_blossom", "crates/blossom"), + ("radroots_core", "crates/core"), + ("radroots_event", "crates/event"), + ("radroots_event_codec", "crates/event_codec"), + ("radroots_geonames", "crates/geonames"), + ("radroots_identity", "crates/identity"), + ("radroots_nostr", "crates/nostr"), + ("radroots_nostr_connect", "crates/nostr_connect"), + ("radroots_protocol", "crates/protocol"), + ("radroots_secrets", "crates/secrets"), + ("radroots_signing", "crates/signing"), + ("radroots_storage", "crates/storage"), + ("radroots_storage_sqlite", "crates/storage_sqlite"), + ("radroots_sync", "crates/sync"), + ("radroots_trade", "crates/trade"), + ("radroots_transport", "crates/transport"), + ("radroots_transport_nostr", "crates/transport_nostr"), + ] { + let path = serde_json::to_string(&lib_root.join(relative_path).to_string_lossy()) + .map_err(|error| format!("failed to render {package} patch path: {error}"))?; + manifest.push_str(&format!("{package} = {{ path = {path} }}\n")); + } + Ok(()) +} + +fn smoke_cargo_run_command(path: &Path, toolchain: &ResolvedRustToolchain) -> Command { + let mut command = Command::new(&toolchain.cargo); + command.arg("run").arg("--quiet").current_dir(path); + toolchain.apply_to_command(&mut command); + command +} + +const SDK_CONSUMER_MAIN: &str = r#"use radroots_sdk::{ClientBuilder, error::ErrorKind}; + +fn main() { + let error = match ClientBuilder::new().build() { + Ok(_) => panic!("empty SDK builder must fail closed"), + Err(error) => error, + }; + assert_eq!(error.kind(), ErrorKind::MissingStorage); +} +"#; + +const FACADE_CONSUMER_MAIN: &str = r#"fn main() -> radroots::Result<()> { + let client = radroots::client::memory().build()?; + assert!(!client.is_closed()); + assert!(radroots::client::local_only().is_local_only()); + Ok(()) +} +"#; + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + use crate::wasm::ResolvedRustToolchain; + + use super::{ + render_facade_consumer_manifest, render_sdk_consumer_manifest, run, smoke_cargo_run_command, + }; + + #[test] + fn retired_sdk_knowledge_smoke_is_not_a_command_surface() { + assert!(run(&["knowledge-rust-local".to_owned()]).is_err()); + } + + #[test] + fn sdk_smoke_consumer_uses_final_api_and_local_lower_package_patches() { + let manifest = render_sdk_consumer_manifest( + Path::new("/tmp/radroots_sdk"), + Path::new("/tmp/radroots_lib"), + ) + .expect("manifest"); + + assert!(manifest.contains("name = \"radroots_sdk_host_smoke\"")); + assert!(manifest.contains("radroots_sdk = { path = \"/tmp/radroots_sdk\"")); + assert!( + manifest.contains("radroots_storage = { path = \"/tmp/radroots_lib/crates/storage\" }") + ); + assert!(!manifest.contains("runtime =")); + assert!(!manifest.contains("signer-adapters")); + } + + #[test] + fn facade_smoke_consumer_uses_curated_front_door() { + let manifest = render_facade_consumer_manifest( + Path::new("/tmp/radroots_facade"), + Path::new("/tmp/radroots_lib"), + ) + .expect("manifest"); + + assert!(manifest.contains("name = \"radroots_facade_host_smoke\"")); + assert!(manifest.contains("radroots = { path = \"/tmp/radroots_facade\" }")); + assert!(!manifest.contains("radroots_sdk = { path")); + } + + #[test] + fn smoke_command_uses_resolved_cargo_path() { + let cargo = PathBuf::from("/tmp/rust-toolchain/bin/cargo"); + let toolchain = ResolvedRustToolchain { + channel: "1.97.0".to_owned(), + rustc: PathBuf::from("/tmp/rust-toolchain/bin/rustc"), + cargo: cargo.clone(), + bin_dir: PathBuf::from("/tmp/rust-toolchain/bin"), + }; + + let command = smoke_cargo_run_command(Path::new("/tmp/smoke"), &toolchain); + + assert_eq!(command.get_program(), cargo.as_os_str()); + assert_eq!(command.get_args().next(), Some(std::ffi::OsStr::new("run"))); + } +} diff --git a/tools/sdk_xtask_import/src/supply_chain_qualification.rs b/tools/sdk_xtask_import/src/supply_chain_qualification.rs @@ -0,0 +1,729 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; + +const CONTRACT_PATH: &str = "contracts/releases/supply_chain.toml"; +const DENY_PATH: &str = "deny.toml"; +const SBOM_FILENAME: &str = "radroots-release-v1-sbom.json"; + +#[derive(Debug, Deserialize)] +struct Contract { + schema_version: u16, + spec_id: String, + package_version: String, + tools: Tools, + sbom: Sbom, + advisory_exception: Vec<AdvisoryException>, + package: Vec<Package>, +} + +#[derive(Debug, Deserialize)] +struct Tools { + cargo_deny: String, + cargo_cyclonedx: String, +} + +#[derive(Debug, Deserialize)] +struct Sbom { + format: String, + spec_version: String, + target: String, + all_features: bool, + source_date_epoch: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct AdvisoryException { + id: String, + package: String, + affected_version: String, + introduced_by: String, + classification: String, + mitigation: String, + remove_when: String, +} + +#[derive(Debug, Deserialize)] +struct Package { + name: String, + manifest_path: String, +} + +#[derive(Debug, Deserialize)] +struct Metadata { + packages: Vec<MetadataPackage>, +} + +#[derive(Debug, Deserialize)] +struct MetadataPackage { + name: String, + version: String, + manifest_path: PathBuf, +} + +struct GeneratedSboms(Vec<PathBuf>); + +impl Drop for GeneratedSboms { + fn drop(&mut self) { + for path in &self.0 { + let _ = fs::remove_file(path); + } + } +} + +pub fn run(root: &Path) -> Result<(), String> { + let contract = load(root)?; + validate(root, &contract, 2)?; + verify_tools(&contract)?; + let metadata = load_metadata(root)?; + qualify_dependencies(root, &contract)?; + let sbom_hashes = qualify_sboms(root, &contract, &metadata)?; + let provenance = build_provenance(root, &contract, &sbom_hashes)?; + validate_provenance(&provenance, &contract)?; + eprintln!( + "qualified {} package supply chains; provenance sha256={}", + contract.package.len(), + sha256(&serde_json::to_vec(&provenance).map_err(|error| error.to_string())?) + ); + Ok(()) +} + +fn load(root: &Path) -> Result<Contract, String> { + let path = root.join(CONTRACT_PATH); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + toml::from_str(&raw).map_err(|error| format!("failed to parse {}: {error}", path.display())) +} + +fn validate(root: &Path, contract: &Contract, expected_packages: usize) -> Result<(), String> { + if contract.schema_version != 1 + || contract.spec_id != "radroots.crates.release.v1" + || contract.package_version != "0.1.0-alpha" + || contract.tools.cargo_deny != "0.19.8" + || contract.tools.cargo_cyclonedx != "0.5.9" + || contract.sbom.format != "json" + || contract.sbom.spec_version != "1.5" + || contract.sbom.target != "all" + || !contract.sbom.all_features + || contract.sbom.source_date_epoch != 0 + { + return Err("invalid supply-chain qualification contract".to_owned()); + } + + let names = contract + .package + .iter() + .map(|package| package.name.as_str()) + .collect::<BTreeSet<_>>(); + let manifests = contract + .package + .iter() + .map(|package| package.manifest_path.as_str()) + .collect::<BTreeSet<_>>(); + if names.len() != expected_packages + || names.len() != contract.package.len() + || manifests.len() != contract.package.len() + { + return Err(format!( + "supply-chain contract requires exactly {expected_packages} unique packages and manifests" + )); + } + for package in &contract.package { + let path = root.join(&package.manifest_path); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let manifest: toml::Value = toml::from_str(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let package_table = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{} has no package table", path.display()))?; + let name = package_table.get("name").and_then(toml::Value::as_str); + let version = package_table.get("version").and_then(toml::Value::as_str); + let inherits_workspace_version = package_table + .get("version") + .and_then(toml::Value::as_table) + .and_then(|version| version.get("workspace")) + .and_then(toml::Value::as_bool) + == Some(true); + if name != Some(package.name.as_str()) + || version != Some(contract.package_version.as_str()) && !inherits_workspace_version + { + return Err(format!( + "{} does not declare {} {}", + path.display(), + package.name, + contract.package_version + )); + } + } + + validate_exceptions(root, contract) +} + +fn validate_exceptions(root: &Path, contract: &Contract) -> Result<(), String> { + let expected = BTreeSet::from([ + "CARGO-YANKED-SPIN-0.9.8".to_owned(), + "RUSTSEC-2024-0384".to_owned(), + "RUSTSEC-2024-0421".to_owned(), + ]); + let actual = contract + .advisory_exception + .iter() + .map(|exception| exception.id.clone()) + .collect::<BTreeSet<_>>(); + if actual != expected || actual.len() != contract.advisory_exception.len() { + return Err("supply-chain advisory exceptions are not the exact approved set".to_owned()); + } + for exception in &contract.advisory_exception { + if exception.package.is_empty() + || exception.affected_version.is_empty() + || exception.introduced_by.is_empty() + || exception.classification.is_empty() + || exception.mitigation.is_empty() + || exception.remove_when.is_empty() + { + return Err(format!("advisory exception {} is incomplete", exception.id)); + } + let exact = match exception.id.as_str() { + "RUSTSEC-2024-0384" => { + exception.package == "instant" + && exception.affected_version == "0.1.13" + && exception.classification == "unmaintained" + && exception.remove_when == "nostr >=0.45.0 stable" + } + "RUSTSEC-2024-0421" => { + exception.package == "idna" + && exception.affected_version == "0.5.0" + && exception.classification == "vulnerability" + && exception.remove_when == "nostr >=0.45.0 stable" + } + "CARGO-YANKED-SPIN-0.9.8" => { + exception.package == "spin" + && exception.affected_version == "0.9.8" + && exception.classification == "yanked" + && exception.remove_when + == "sqlx no longer resolves flume 0.12.0 with spin 0.9.8" + } + _ => false, + }; + if !exact { + return Err(format!( + "advisory exception {} differs from its exact approved policy", + exception.id + )); + } + } + + let lock_raw = fs::read_to_string(root.join("Cargo.lock")) + .map_err(|error| format!("failed to read Cargo.lock: {error}"))?; + let lock: toml::Value = toml::from_str(&lock_raw) + .map_err(|error| format!("failed to parse Cargo.lock: {error}"))?; + let locked_packages = lock + .get("package") + .and_then(toml::Value::as_array) + .ok_or_else(|| "Cargo.lock contains no packages".to_owned())?; + for exception in &contract.advisory_exception { + let present = locked_packages.iter().any(|package| { + package.get("name").and_then(toml::Value::as_str) == Some(exception.package.as_str()) + && package.get("version").and_then(toml::Value::as_str) + == Some(exception.affected_version.as_str()) + }); + if !present { + return Err(format!( + "advisory exception {} is stale because {} {} is absent from Cargo.lock", + exception.id, exception.package, exception.affected_version + )); + } + } + let patched_url_present = locked_packages.iter().any(|package| { + package.get("name").and_then(toml::Value::as_str) == Some("url") + && package + .get("version") + .and_then(toml::Value::as_str) + .and_then(|version| semver::Version::parse(version).ok()) + .is_some_and(|version| version >= semver::Version::new(2, 5, 4)) + }); + if !patched_url_present { + return Err("the IDNA exception requires url 2.5.4 or newer in Cargo.lock".to_owned()); + } + + let deny_raw = fs::read_to_string(root.join(DENY_PATH)) + .map_err(|error| format!("failed to read {DENY_PATH}: {error}"))?; + let deny: toml::Value = toml::from_str(&deny_raw) + .map_err(|error| format!("failed to parse {DENY_PATH}: {error}"))?; + let ignored = deny + .get("advisories") + .and_then(|value| value.get("ignore")) + .and_then(toml::Value::as_array) + .ok_or_else(|| "deny.toml advisories.ignore is missing".to_owned())? + .iter() + .filter_map(toml::Value::as_str) + .map(str::to_owned) + .collect::<BTreeSet<_>>(); + let expected_ignored = expected + .iter() + .filter(|id| id.starts_with("RUSTSEC-")) + .cloned() + .collect::<BTreeSet<_>>(); + if ignored != expected_ignored { + return Err("deny.toml advisory ignores differ from the governed exceptions".to_owned()); + } + let allowed_git = deny + .get("sources") + .and_then(|value| value.get("allow-git")) + .and_then(toml::Value::as_array) + .ok_or_else(|| "deny.toml sources.allow-git is missing".to_owned())? + .iter() + .filter_map(toml::Value::as_str) + .collect::<BTreeSet<_>>(); + if allowed_git != BTreeSet::from(["https://github.com/radrootslabs/lib.git"]) { + return Err( + "deny.toml Git sources must allow only the allocated Radroots lib repository" + .to_owned(), + ); + } + + let relay_source = root.join("crates/transport_nostr/src/relay.rs"); + if relay_source.exists() { + let source = fs::read_to_string(&relay_source) + .map_err(|error| format!("failed to read {}: {error}", relay_source.display()))?; + if !source.contains("Url::parse(canonical)") { + return Err( + "the IDNA exception requires patched relay URL canonicalization".to_owned(), + ); + } + } + Ok(()) +} + +fn verify_tools(contract: &Contract) -> Result<(), String> { + verify_tool( + &["deny", "--version"], + "cargo-deny", + &contract.tools.cargo_deny, + )?; + verify_tool( + &["cyclonedx", "--version"], + "cargo-cyclonedx", + &contract.tools.cargo_cyclonedx, + ) +} + +fn verify_tool(args: &[&str], name: &str, expected: &str) -> Result<(), String> { + let output = Command::new("cargo") + .args(args) + .output() + .map_err(|error| format!("failed to start {name}: {error}"))?; + if !output.status.success() { + return Err(format!("{name} {expected} is required")); + } + let stdout = String::from_utf8_lossy(&output.stdout); + let installed = stdout + .split_whitespace() + .find_map(|value| semver::Version::parse(value).ok()) + .ok_or_else(|| format!("could not parse {name} version: {stdout}"))?; + let expected = semver::Version::parse(expected) + .map_err(|error| format!("invalid governed {name} version: {error}"))?; + if installed != expected { + return Err(format!("{name} {expected} is required, found {installed}")); + } + Ok(()) +} + +fn load_metadata(root: &Path) -> Result<Metadata, String> { + let output = Command::new("cargo") + .args(["metadata", "--format-version", "1", "--locked", "--no-deps"]) + .current_dir(root) + .output() + .map_err(|error| format!("failed to start cargo metadata: {error}"))?; + if !output.status.success() { + return Err("locked cargo metadata failed".to_owned()); + } + serde_json::from_slice(&output.stdout) + .map_err(|error| format!("failed to parse cargo metadata: {error}")) +} + +fn qualify_dependencies(root: &Path, contract: &Contract) -> Result<(), String> { + let mut saw_governed_yank = false; + for package in &contract.package { + let manifest = root.join(&package.manifest_path); + let common = [ + "deny", + "-L", + "error", + "--manifest-path", + manifest + .to_str() + .ok_or_else(|| "non-UTF-8 package manifest path".to_owned())?, + "--all-features", + "--locked", + "check", + ]; + saw_governed_yank |= qualify_advisories(root, &manifest, &package.name)?; + run_command( + root, + "cargo", + common + .iter() + .copied() + .chain(["bans", "licenses", "sources"]) + .collect::<Vec<_>>(), + &format!("dependency policy failed for {}", package.name), + )?; + } + if !saw_governed_yank { + return Err("the governed spin 0.9.8 yank is stale and must be removed".to_owned()); + } + Ok(()) +} + +fn qualify_advisories(root: &Path, manifest: &Path, package: &str) -> Result<bool, String> { + let output = Command::new("cargo") + .args([ + "deny", + "--format", + "json", + "--log-level", + "warn", + "--manifest-path", + manifest + .to_str() + .ok_or_else(|| "non-UTF-8 package manifest path".to_owned())?, + "--all-features", + "--locked", + "check", + "--allow", + "advisory-not-detected", + "advisories", + ]) + .current_dir(root) + .output() + .map_err(|error| format!("failed to start cargo-deny: {error}"))?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + if !output.status.success() { + return Err(format!( + "advisory qualification failed for {package}: {stdout}{stderr}" + )); + } + + let mut saw_governed_yank = false; + for line in stdout.lines().chain(stderr.lines()) { + let Ok(message) = serde_json::from_str::<Value>(line) else { + continue; + }; + if message.get("type").and_then(Value::as_str) != Some("diagnostic") { + continue; + } + let fields = message + .get("fields") + .and_then(Value::as_object) + .ok_or_else(|| "cargo-deny emitted a malformed diagnostic".to_owned())?; + let code = fields + .get("code") + .and_then(Value::as_str) + .unwrap_or_default(); + let krate = fields + .get("graphs") + .and_then(Value::as_array) + .and_then(|graphs| graphs.first()) + .and_then(|graph| graph.get("Krate")); + let exact_governed_yank = code == "yanked" + && krate + .and_then(|krate| krate.get("name")) + .and_then(Value::as_str) + == Some("spin") + && krate + .and_then(|krate| krate.get("version")) + .and_then(Value::as_str) + == Some("0.9.8"); + if exact_governed_yank { + saw_governed_yank = true; + } else { + return Err(format!( + "unapproved cargo-deny diagnostic for {package}: {line}" + )); + } + } + Ok(saw_governed_yank) +} + +fn qualify_sboms( + root: &Path, + contract: &Contract, + metadata: &Metadata, +) -> Result<BTreeMap<String, String>, String> { + let mut paths = metadata + .packages + .iter() + .map(|package| { + package + .manifest_path + .parent() + .expect("manifest has parent") + .join(SBOM_FILENAME) + }) + .collect::<Vec<_>>(); + paths.sort(); + paths.dedup(); + if let Some(path) = paths.iter().find(|path| path.exists()) { + return Err(format!( + "refusing to overwrite pre-existing SBOM {}", + path.display() + )); + } + let _generated = GeneratedSboms(paths); + + let status = Command::new("cargo") + .args([ + "cyclonedx", + "--quiet", + "--manifest-path", + "Cargo.toml", + "--format", + &contract.sbom.format, + "--all-features", + "--target", + &contract.sbom.target, + "--spec-version", + &contract.sbom.spec_version, + "--license-strict", + "--license-accept-named", + "MIT/Apache-2.0", + "--license-accept-named", + "Apache-2.0/MIT", + "--license-accept-named", + "Apache-2.0 / MIT", + "--override-filename", + "radroots-release-v1-sbom", + ]) + .env( + "SOURCE_DATE_EPOCH", + contract.sbom.source_date_epoch.to_string(), + ) + .current_dir(root) + .status() + .map_err(|error| format!("failed to start cargo-cyclonedx: {error}"))?; + if !status.success() { + return Err("CycloneDX SBOM generation failed".to_owned()); + } + + let by_name = metadata + .packages + .iter() + .map(|package| (package.name.as_str(), package)) + .collect::<BTreeMap<_, _>>(); + let mut hashes = BTreeMap::new(); + for package in &contract.package { + let metadata_package = by_name + .get(package.name.as_str()) + .ok_or_else(|| format!("cargo metadata omitted {}", package.name))?; + if metadata_package.version != contract.package_version { + return Err(format!("{} metadata version drifted", package.name)); + } + let path = metadata_package + .manifest_path + .parent() + .expect("manifest has parent") + .join(SBOM_FILENAME); + let raw = fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let mut sbom: Value = serde_json::from_slice(&raw) + .map_err(|error| format!("invalid SBOM {}: {error}", path.display()))?; + validate_sbom(&sbom, &package.name, &contract.package_version)?; + normalize_sbom(&mut sbom, root); + hashes.insert( + package.name.clone(), + sha256(&serde_json::to_vec(&sbom).map_err(|error| error.to_string())?), + ); + } + Ok(hashes) +} + +fn validate_sbom(sbom: &Value, name: &str, version: &str) -> Result<(), String> { + let component = sbom + .pointer("/metadata/component") + .and_then(Value::as_object) + .ok_or_else(|| format!("{name} SBOM has no root component"))?; + if sbom.get("bomFormat").and_then(Value::as_str) != Some("CycloneDX") + || sbom.get("specVersion").and_then(Value::as_str) != Some("1.5") + || component.get("name").and_then(Value::as_str) != Some(name) + || component.get("version").and_then(Value::as_str) != Some(version) + || sbom + .get("components") + .and_then(Value::as_array) + .is_none_or(Vec::is_empty) + || sbom + .get("dependencies") + .and_then(Value::as_array) + .is_none_or(Vec::is_empty) + { + return Err(format!( + "{name} SBOM is incomplete or identifies the wrong package" + )); + } + Ok(()) +} + +fn normalize_sbom(value: &mut Value, root: &Path) { + normalize_sbom_value(value, root.to_string_lossy().as_ref()); +} + +fn normalize_sbom_value(value: &mut Value, root: &str) { + match value { + Value::Object(object) => { + object.remove("serialNumber"); + for value in object.values_mut() { + normalize_sbom_value(value, root); + } + } + Value::Array(values) => { + for value in values { + normalize_sbom_value(value, root); + } + } + Value::String(string) if string.contains(root) => { + *string = string.replace(root, "$REPOSITORY"); + } + _ => {} + } +} + +fn build_provenance( + root: &Path, + contract: &Contract, + sbom_hashes: &BTreeMap<String, String>, +) -> Result<Value, String> { + let revision = command_stdout(root, "git", &["rev-parse", "HEAD"])?; + if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err("Git provenance revision is not a full commit ID".to_owned()); + } + let lock = fs::read(root.join("Cargo.lock")) + .map_err(|error| format!("failed to read Cargo.lock: {error}"))?; + let packages = contract + .package + .iter() + .map(|package| { + json!({ + "name": package.name, + "version": contract.package_version, + "manifestPath": package.manifest_path, + "sbomSha256": sbom_hashes.get(&package.name), + }) + }) + .collect::<Vec<_>>(); + Ok(json!({ + "schemaVersion": 1, + "specId": contract.spec_id, + "source": { + "gitCommit": revision, + "cargoLockSha256": sha256(&lock), + }, + "tools": { + "cargoDeny": contract.tools.cargo_deny, + "cargoCyclonedx": contract.tools.cargo_cyclonedx, + }, + "packages": packages, + "advisoryExceptions": contract.advisory_exception, + })) +} + +fn validate_provenance(provenance: &Value, contract: &Contract) -> Result<(), String> { + if provenance.get("schemaVersion").and_then(Value::as_u64) != Some(1) + || provenance.get("specId").and_then(Value::as_str) != Some(contract.spec_id.as_str()) + || provenance + .get("packages") + .and_then(Value::as_array) + .is_none_or(|packages| packages.len() != contract.package.len()) + || provenance + .pointer("/source/cargoLockSha256") + .and_then(Value::as_str) + .is_none_or(|hash| hash.len() != 64) + { + return Err("generated supply-chain provenance is incomplete".to_owned()); + } + Ok(()) +} + +fn run_command(root: &Path, program: &str, args: Vec<&str>, failure: &str) -> Result<(), String> { + let status = Command::new(program) + .args(args) + .current_dir(root) + .status() + .map_err(|error| format!("failed to start {program}: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(failure.to_owned()) + } +} + +fn command_stdout(root: &Path, program: &str, args: &[&str]) -> Result<String, String> { + let output = Command::new(program) + .args(args) + .current_dir(root) + .output() + .map_err(|error| format!("failed to start {program}: {error}"))?; + if !output.status.success() { + return Err(format!("{program} {} failed", args.join(" "))); + } + String::from_utf8(output.stdout) + .map(|value| value.trim().to_owned()) + .map_err(|error| format!("{program} emitted non-UTF-8 output: {error}")) +} + +fn sha256(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("workspace root") + .to_path_buf() + } + + #[test] + fn current_contract_is_exact_and_exception_bound() { + let root = root(); + let contract = load(&root).expect("contract"); + validate(&root, &contract, 2).expect("valid contract"); + } + + #[test] + fn sbom_validation_rejects_wrong_identity() { + let sbom = json!({ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": {"component": {"name": "wrong", "version": "0.1.0-alpha"}}, + "components": [{}], + "dependencies": [{}], + }); + assert!(validate_sbom(&sbom, "radroots_core", "0.1.0-alpha").is_err()); + } + + #[test] + fn normalization_removes_random_and_absolute_identity() { + let mut sbom = json!({ + "serialNumber": "urn:uuid:random", + "path": "/workspace/crate", + }); + normalize_sbom(&mut sbom, Path::new("/workspace")); + assert!(sbom.get("serialNumber").is_none()); + assert_eq!( + sbom.get("path").and_then(Value::as_str), + Some("$REPOSITORY/crate") + ); + } +} diff --git a/tools/sdk_xtask_import/src/target_qualification.rs b/tools/sdk_xtask_import/src/target_qualification.rs @@ -0,0 +1,199 @@ +use std::{fs, path::Path, process::Command}; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct TargetMatrix { + schema_version: u32, + msrv_toolchain: String, + current_toolchain: String, + operating_system: Vec<OperatingSystem>, +} + +#[derive(Debug, Deserialize)] +struct OperatingSystem { + name: String, + target: String, + cross_compiler: Option<String>, + cross_cflags: Option<String>, +} + +#[derive(Debug, Deserialize)] +struct Architecture { + repositories: Repositories, +} + +#[derive(Debug, Deserialize)] +struct Repositories { + sdk: Repository, +} + +#[derive(Debug, Deserialize)] +struct Repository { + packages: Vec<String>, +} + +pub fn run(workspace_root: &Path) -> Result<(), String> { + let (matrix, packages) = load(workspace_root)?; + for toolchain in [&matrix.msrv_toolchain, &matrix.current_toolchain] { + run_command( + workspace_root, + "rustup", + &[ + "run", + toolchain, + "cargo", + "check", + "--workspace", + "--all-targets", + "--locked", + ], + )?; + } + for operating_system in matrix.operating_system { + for package in &packages { + run_target_command( + workspace_root, + &operating_system, + &[ + "check", + "-p", + package, + "--lib", + "--target", + &operating_system.target, + "--locked", + ], + )?; + } + } + Ok(()) +} + +fn load(workspace_root: &Path) -> Result<(TargetMatrix, Vec<String>), String> { + let matrix_path = workspace_root.join("contracts/releases/target_matrix.toml"); + let matrix = toml::from_str::<TargetMatrix>(&read(&matrix_path)?) + .map_err(|error| format!("failed to parse {}: {error}", matrix_path.display()))?; + validate(&matrix)?; + + let architecture_path = workspace_root.join("docs/specs/radroots_crates_release_v1.toml"); + let architecture = toml::from_str::<Architecture>(&read(&architecture_path)?) + .map_err(|error| format!("failed to parse {}: {error}", architecture_path.display()))?; + let mut packages = architecture.repositories.sdk.packages; + packages.sort(); + if packages != ["radroots", "radroots_sdk"] { + return Err(format!( + "target qualification requires exactly the two SDK front doors, found {packages:?}" + )); + } + Ok((matrix, packages)) +} + +fn read(path: &Path) -> Result<String, String> { + fs::read_to_string(path).map_err(|error| format!("failed to read {}: {error}", path.display())) +} + +fn validate(matrix: &TargetMatrix) -> Result<(), String> { + if matrix.schema_version != 1 { + return Err(format!( + "target matrix schema_version must be 1, found {}", + matrix.schema_version + )); + } + if matrix.msrv_toolchain != "1.97.1" || matrix.current_toolchain != "stable" { + return Err("target matrix must retain MSRV 1.97.1 and current stable".to_owned()); + } + let expected = [ + ( + "linux", + "x86_64-unknown-linux-gnu", + Some("zig cc"), + Some("-target x86_64-linux-gnu"), + ), + ("macos", "aarch64-apple-darwin", None, None), + ( + "windows", + "x86_64-pc-windows-gnu", + Some("x86_64-w64-mingw32-gcc"), + None, + ), + ]; + if matrix.operating_system.len() != expected.len() + || expected.iter().any(|(name, target, compiler, cflags)| { + !matrix.operating_system.iter().any(|entry| { + entry.name == *name + && entry.target == *target + && entry.cross_compiler.as_deref() == *compiler + && entry.cross_cflags.as_deref() == *cflags + }) + }) + { + return Err( + "target matrix must contain the exact Linux, macOS, and Windows triples".to_owned(), + ); + } + Ok(()) +} + +fn run_target_command( + workspace_root: &Path, + operating_system: &OperatingSystem, + args: &[&str], +) -> Result<(), String> { + eprintln!("cargo {}", args.join(" ")); + let mut command = Command::new("cargo"); + command.args(args).current_dir(workspace_root); + let target_key = operating_system.target.replace('-', "_"); + if let Some(compiler) = operating_system.cross_compiler.as_deref() { + command.env(format!("CC_{target_key}"), compiler); + } + if let Some(cflags) = operating_system.cross_cflags.as_deref() { + command.env(format!("CFLAGS_{target_key}"), cflags); + } + let status = command + .status() + .map_err(|error| format!("failed to start cargo: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!( + "target qualification failed: cargo {}", + args.join(" ") + )) + } +} + +fn run_command(workspace_root: &Path, program: &str, args: &[&str]) -> Result<(), String> { + eprintln!("{program} {}", args.join(" ")); + let status = Command::new(program) + .args(args) + .current_dir(workspace_root) + .status() + .map_err(|error| format!("failed to start {program}: {error}"))?; + if status.success() { + Ok(()) + } else { + Err(format!( + "target qualification failed: {program} {}", + args.join(" ") + )) + } +} + +#[cfg(test)] +mod tests { + use super::load; + + #[test] + fn current_contract_selects_exact_toolchains_targets_and_packages() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(std::path::Path::parent) + .expect("workspace root"); + let (matrix, packages) = load(root).expect("target matrix"); + assert_eq!(matrix.msrv_toolchain, "1.97.1"); + assert_eq!(matrix.current_toolchain, "stable"); + assert_eq!(matrix.operating_system.len(), 3); + assert_eq!(packages, ["radroots", "radroots_sdk"]); + } +} diff --git a/tools/sdk_xtask_import/src/ts.rs b/tools/sdk_xtask_import/src/ts.rs @@ -0,0 +1,37 @@ +pub fn generated_header() -> &'static str { + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n" +} + +pub fn generated_types_file() -> &'static str { + "types.ts" +} + +pub fn generated_constants_file() -> &'static str { + "constants.ts" +} + +pub fn generated_kinds_file() -> &'static str { + "kinds.ts" +} + +#[cfg(test)] +mod tests { + use super::{ + generated_constants_file, generated_header, generated_kinds_file, generated_types_file, + }; + + #[test] + fn generated_header_matches_contract() { + assert_eq!( + generated_header(), + "// @generated by cargo xtask generate ts\n// Do not edit by hand.\n" + ); + } + + #[test] + fn generated_file_names_are_stable() { + assert_eq!(generated_types_file(), "types.ts"); + assert_eq!(generated_constants_file(), "constants.ts"); + assert_eq!(generated_kinds_file(), "kinds.ts"); + } +} diff --git a/tools/sdk_xtask_import/src/wasm.rs b/tools/sdk_xtask_import/src/wasm.rs @@ -0,0 +1,1147 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + env, + ffi::OsStr, + fs, io, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{ + check::check_wasm_package_surface, + fs::workspace_root, + package_matrix::{WasmPackageSpec, validate_package_matrix, wasm_package_specs}, + wasm_declarations::write_declaration_files, +}; + +pub(crate) const WASM_TARGET: &str = "wasm32-unknown-unknown"; +const WASM_C_COMPILER_ENV: &str = "CC_wasm32_unknown_unknown"; +const WASM_CFLAGS_ENV: &str = "CFLAGS_wasm32_unknown_unknown"; +const WASM_C_TARGET_ARG: &str = "--target=wasm32-unknown-unknown"; + +pub fn generate(args: &[String]) -> Result<(), String> { + validate_package_matrix()?; + let specs = selected_specs(args)?; + let root = workspace_root()?; + let toolchain = resolve_wasm_toolchain(&root)?; + println!( + "using Rust toolchain {} for {}: rustc={}, cargo={}", + toolchain.channel(), + WASM_TARGET, + toolchain.rustc().display(), + toolchain.cargo().display() + ); + let wasm_c_compiler = if specs + .iter() + .any(|spec| wasm_package_requires_c_compiler(*spec)) + { + let compiler = resolve_wasm_c_compiler()?; + println!( + "using WASM C compiler for {}: {}", + WASM_TARGET, + compiler.path.display() + ); + Some(compiler) + } else { + None + }; + for spec in specs { + let dist_dir = root.join(spec.package_dir).join("dist"); + if dist_dir.exists() { + fs::remove_dir_all(&dist_dir) + .map_err(|error| format!("failed to remove {}: {error}", dist_dir.display()))?; + } + let mut command = Command::new(&toolchain.wasm_pack); + command.current_dir(&root); + for arg in wasm_pack_args(spec) { + command.arg(arg); + } + toolchain.apply_to_command(&mut command); + if let Some(compiler) = wasm_c_compiler.as_ref() + && wasm_package_requires_c_compiler(spec) + { + compiler.apply_to_command(&mut command); + } + let status = command.status().map_err(|error| { + format!( + "failed to start wasm-pack for {} while generating {}: {error}", + spec.key, spec.package_name + ) + })?; + if !status.success() { + return Err(format!( + "wasm-pack failed for {} while generating {} with status {status}; rerun `cargo xtask generate wasm --package {}` after fixing the wasm toolchain", + spec.key, spec.package_name, spec.key + )); + } + remove_wasm_pack_gitignore(&dist_dir, spec)?; + write_declaration_files(&root, spec)?; + write_provenance_manifest(&root, spec)?; + check_wasm_package_surface(&root, spec)?; + println!("generated wasm package {}", spec.package_name); + } + Ok(()) +} + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +struct WasmProvenanceManifest { + schema_version: u16, + package: String, + package_version: String, + rust_crate: String, + rust_crate_version: String, + target: String, + deterministic_codec: bool, + networking: bool, + signing: bool, + source_sha256: String, + outputs: BTreeMap<String, String>, +} + +fn write_provenance_manifest(root: &Path, spec: WasmPackageSpec) -> Result<(), String> { + let manifest = provenance_manifest(root, spec)?; + let dist_dir = root.join(spec.package_dir).join("dist"); + let path = provenance_manifest_path(&dist_dir, spec); + let mut rendered = serde_json::to_string_pretty(&manifest) + .map_err(|error| format!("failed to render {}: {error}", path.display()))?; + rendered.push('\n'); + fs::write(&path, rendered) + .map_err(|error| format!("failed to write {}: {error}", path.display())) +} + +fn provenance_manifest( + root: &Path, + spec: WasmPackageSpec, +) -> Result<WasmProvenanceManifest, String> { + let package_version = manifest_version(&root.join(spec.package_dir).join("package.json"))?; + let rust_crate_version = manifest_version(&root.join(spec.crate_dir).join("Cargo.toml"))?; + let source_sha256 = source_tree_sha256(root, spec)?; + let dist_dir = root.join(spec.package_dir).join("dist"); + let mut outputs = BTreeMap::new(); + for relative in wasm_output_files(spec) { + let path = dist_dir.join(&relative); + let bytes = fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + outputs.insert(relative, format!("{:x}", Sha256::digest(bytes))); + } + Ok(WasmProvenanceManifest { + schema_version: 1, + package: spec.package_name.to_owned(), + package_version, + rust_crate: spec.crate_name.to_owned(), + rust_crate_version, + target: WASM_TARGET.to_owned(), + deterministic_codec: spec.key == "event_codec", + networking: false, + signing: false, + source_sha256, + outputs, + }) +} + +pub(crate) fn validate_provenance_manifest( + root: &Path, + spec: WasmPackageSpec, +) -> Result<(), String> { + let path = provenance_manifest_path(&root.join(spec.package_dir).join("dist"), spec); + let raw = fs::read_to_string(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + let actual: WasmProvenanceManifest = serde_json::from_str(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + let expected = provenance_manifest(root, spec)?; + if actual != expected { + return Err(format!( + "stale or invalid WASM provenance manifest: {}", + path.display() + )); + } + Ok(()) +} + +fn manifest_version(path: &Path) -> Result<String, String> { + let raw = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + if path.extension() == Some(OsStr::new("json")) { + let value: serde_json::Value = serde_json::from_str(&raw) + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + return value["version"] + .as_str() + .map(str::to_owned) + .ok_or_else(|| format!("{} must define string version", path.display())); + } + let value: toml::Value = raw + .parse() + .map_err(|error| format!("failed to parse {}: {error}", path.display()))?; + value["package"]["version"] + .as_str() + .map(str::to_owned) + .ok_or_else(|| format!("{} must define explicit package.version", path.display())) +} + +fn source_tree_sha256(root: &Path, spec: WasmPackageSpec) -> Result<String, String> { + let crate_dir = root.join(spec.crate_dir); + let mut files = vec![crate_dir.join("Cargo.toml")]; + collect_source_files(&crate_dir.join("src"), &mut files)?; + files.sort(); + let mut hasher = Sha256::new(); + for path in files { + let relative = path + .strip_prefix(root) + .map_err(|error| format!("failed to relativize {}: {error}", path.display()))?; + hasher.update(relative.to_string_lossy().as_bytes()); + hasher.update([0]); + hasher.update( + fs::read(&path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?, + ); + hasher.update([0]); + } + Ok(format!("{:x}", hasher.finalize())) +} + +fn collect_source_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), String> { + let mut entries = fs::read_dir(dir) + .map_err(|error| format!("failed to read {}: {error}", dir.display()))? + .collect::<Result<Vec<_>, _>>() + .map_err(|error| format!("failed to enumerate {}: {error}", dir.display()))?; + entries.sort_by_key(std::fs::DirEntry::file_name); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + collect_source_files(&path, files)?; + } else if path.is_file() { + files.push(path); + } + } + Ok(()) +} + +fn wasm_output_files(spec: WasmPackageSpec) -> [String; 4] { + [ + format!("{}.js", spec.out_name), + format!("{}.d.ts", spec.out_name), + format!("{}_bg.wasm", spec.out_name), + format!("{}_bg.wasm.d.ts", spec.out_name), + ] +} + +fn provenance_manifest_path(dist_dir: &Path, spec: WasmPackageSpec) -> PathBuf { + dist_dir.join(format!("{}.provenance.json", spec.out_name)) +} + +struct WasmToolchain { + wasm_pack: PathBuf, + rust: ResolvedRustToolchain, +} + +impl WasmToolchain { + fn apply_to_command(&self, command: &mut Command) { + self.rust.apply_to_command(command); + } + + fn rustc(&self) -> &Path { + &self.rust.rustc + } + + fn cargo(&self) -> &Path { + &self.rust.cargo + } + + fn channel(&self) -> &str { + &self.rust.channel + } +} + +pub(crate) struct ResolvedRustToolchain { + pub(crate) channel: String, + pub(crate) rustc: PathBuf, + pub(crate) cargo: PathBuf, + pub(crate) bin_dir: PathBuf, +} + +impl ResolvedRustToolchain { + pub(crate) fn apply_to_command(&self, command: &mut Command) { + prepend_path(command, &self.bin_dir); + command.env("RUSTC", &self.rustc); + command.env("CARGO", &self.cargo); + command.env("RUSTUP_TOOLCHAIN", &self.channel); + } +} + +fn resolve_wasm_toolchain(root: &Path) -> Result<WasmToolchain, String> { + let wasm_pack = resolve_required_path_tool("wasm-pack")?; + let rust = resolve_rust_toolchain(root)?; + Ok(WasmToolchain { wasm_pack, rust }) +} + +pub(crate) fn resolve_rust_toolchain(root: &Path) -> Result<ResolvedRustToolchain, String> { + let toolchain_path = root.join("rust-toolchain.toml"); + let channel = read_rust_toolchain_channel(&toolchain_path)?; + let rustc = rustup_tool_for_channel("rustc", &channel)?; + let cargo = rustup_tool_for_channel("cargo", &channel)?; + let rustc_bin = rustc.parent().ok_or_else(|| { + format!( + "rustup resolved rustc for toolchain {channel} without a parent path: {}", + rustc.display() + ) + })?; + let cargo_bin = cargo.parent().ok_or_else(|| { + format!( + "rustup resolved cargo for toolchain {channel} without a parent path: {}", + cargo.display() + ) + })?; + if rustc_bin != cargo_bin { + return Err(format!( + "rustup resolved mismatched Rust tool paths for toolchain {channel}: rustc={}, cargo={}", + rustc.display(), + cargo.display() + )); + } + let bin_dir = rustc_bin.to_path_buf(); + ensure_wasm_target_installed(&channel)?; + Ok(ResolvedRustToolchain { + channel, + rustc, + cargo, + bin_dir, + }) +} + +fn wasm_pack_args(spec: WasmPackageSpec) -> Vec<&'static str> { + vec![ + "build", + spec.crate_dir, + "--release", + "--target", + "web", + "--out-dir", + spec.out_dir, + "--out-name", + spec.out_name, + "--no-pack", + ] +} + +fn remove_wasm_pack_gitignore(dist_dir: &Path, spec: WasmPackageSpec) -> Result<(), String> { + let ignore_path = dist_dir.join(".gitignore"); + let contents = match fs::read_to_string(&ignore_path) { + Ok(contents) => contents, + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(format!( + "failed to read wasm-pack ignore file for {}: {}: {error}", + spec.package_name, + ignore_path.display() + )); + } + }; + if contents.trim() != "*" { + return Err(format!( + "unexpected wasm-pack ignore file for {}: {}; refusing to remove it", + spec.package_name, + ignore_path.display() + )); + } + fs::remove_file(&ignore_path).map_err(|error| { + format!( + "failed to remove wasm-pack ignore file for {}: {}: {error}", + spec.package_name, + ignore_path.display() + ) + }) +} + +fn selected_specs(args: &[String]) -> Result<Vec<WasmPackageSpec>, String> { + match args { + [] => Ok(wasm_package_specs().to_vec()), + [flag, key] if flag == "--package" => wasm_package_specs() + .iter() + .copied() + .find(|spec| spec.key == key) + .map(|spec| vec![spec]) + .ok_or_else(|| format!("unknown wasm package: {key}")), + _ => Err("usage: cargo xtask generate wasm [--package <key>]".to_owned()), + } +} + +fn wasm_package_requires_c_compiler(spec: WasmPackageSpec) -> bool { + spec.key == "event_codec" +} + +fn resolve_required_path_tool(name: &str) -> Result<PathBuf, String> { + let path = env::var_os("PATH").ok_or_else(|| { + format!("missing {name}: PATH is not set; install {name} and expose it on PATH") + })?; + resolve_path_tool_from_path(name, &path) +} + +fn resolve_path_tool_from_path(name: &str, path: &std::ffi::OsStr) -> Result<PathBuf, String> { + let matches = executable_matches(name, path); + match matches.as_slice() { + [] => Err(format!( + "missing {name}: install {name} and rerun `cargo xtask generate wasm`" + )), + [tool] => Ok(tool.clone()), + _ => Err(format!( + "ambiguous {name}: found {}; remove duplicate {name} entries from PATH before running `cargo xtask generate wasm`", + matches + .iter() + .map(|path| path.display().to_string()) + .collect::<Vec<_>>() + .join(", ") + )), + } +} + +fn executable_matches(name: &str, path: &std::ffi::OsStr) -> Vec<PathBuf> { + let mut seen = BTreeSet::new(); + let mut matches = Vec::new(); + for dir in env::split_paths(path) { + let candidate = dir.join(name); + if !is_executable_file(&candidate) { + continue; + } + let key = fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone()); + if seen.insert(key) { + matches.push(candidate); + } + } + matches +} + +fn is_executable_file(path: &Path) -> bool { + if !path.is_file() { + return false; + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + path.metadata() + .map(|metadata| metadata.permissions().mode() & 0o111 != 0) + .unwrap_or(false) + } + #[cfg(not(unix))] + { + true + } +} + +fn read_rust_toolchain_channel(path: &Path) -> Result<String, String> { + let contents = fs::read_to_string(path) + .map_err(|error| format!("failed to read {}: {error}", path.display()))?; + rust_toolchain_channel_from(&contents) +} + +fn rust_toolchain_channel_from(contents: &str) -> Result<String, String> { + let value = contents + .parse::<toml::Value>() + .map_err(|error| format!("failed to parse rust-toolchain.toml: {error}"))?; + let channel = value + .get("toolchain") + .and_then(|toolchain| toolchain.get("channel")) + .and_then(toml::Value::as_str) + .ok_or_else(|| "rust-toolchain.toml must define toolchain.channel".to_owned())? + .trim(); + if channel.is_empty() { + return Err("rust-toolchain.toml toolchain.channel must not be empty".to_owned()); + } + Ok(channel.to_owned()) +} + +fn rustup_tool_for_channel(name: &str, channel: &str) -> Result<PathBuf, String> { + let output = Command::new("rustup") + .arg("which") + .arg("--toolchain") + .arg(channel) + .arg(name) + .output() + .map_err(|error| { + format!( + "failed to resolve {name} for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}" + ) + })?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!( + "failed to resolve {name} for Rust toolchain {channel}: {}; run `rustup toolchain install {channel}`", + stderr.trim() + )); + } + let path = String::from_utf8(output.stdout) + .map_err(|error| format!("rustup emitted non-UTF-8 {name} path: {error}"))?; + let trimmed = path.trim(); + if trimmed.is_empty() { + return Err(format!( + "rustup returned an empty {name} path for Rust toolchain {channel}" + )); + } + Ok(PathBuf::from(trimmed)) +} + +fn ensure_wasm_target_installed(channel: &str) -> Result<(), String> { + let output = Command::new("rustup") + .args(rustup_target_list_args(channel)) + .output() + .map_err(|error| { + format!( + "failed to verify {WASM_TARGET} target for Rust toolchain {channel} with rustup: {error}; install rustup toolchain {channel}" + ) + })?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!( + "failed to verify {WASM_TARGET} target for Rust toolchain {channel}: {}; run `rustup target add {WASM_TARGET} --toolchain {channel}`", + stderr.trim() + )); + } + let stdout = String::from_utf8_lossy(&output.stdout); + validate_target_list(&stdout, WASM_TARGET, channel) +} + +fn rustup_target_list_args(channel: &str) -> [&str; 5] { + ["target", "list", "--installed", "--toolchain", channel] +} + +fn validate_target_list(output: &str, target: &str, channel: &str) -> Result<(), String> { + if target_list_contains(output, target) { + Ok(()) + } else { + Err(format!( + "missing Rust target {target} for Rust toolchain {channel}: run `rustup target add {target} --toolchain {channel}`" + )) + } +} + +fn target_list_contains(output: &str, target: &str) -> bool { + output.lines().any(|line| line.trim() == target) +} + +#[derive(Debug)] +struct WasmCCompiler { + path: PathBuf, + cflags: ResolvedWasmCFlags, +} + +impl WasmCCompiler { + fn apply_to_command(&self, command: &mut Command) { + command.env(WASM_C_COMPILER_ENV, &self.path); + command.env(WASM_CFLAGS_ENV, &self.cflags.value); + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct ResolvedWasmCFlags { + value: String, + args: Vec<String>, +} + +fn resolve_wasm_c_compiler() -> Result<WasmCCompiler, String> { + let path = env::var_os("PATH").unwrap_or_default(); + let explicit = env::var_os(WASM_C_COMPILER_ENV); + let cflags = resolve_wasm_cflags(env::var_os(WASM_CFLAGS_ENV).as_deref())?; + let candidates = wasm_c_compiler_candidates(&path); + resolve_wasm_c_compiler_with( + explicit.as_deref(), + &path, + candidates, + cflags, + verify_wasm_c_compiler, + ) +} + +fn resolve_wasm_c_compiler_with<F>( + explicit: Option<&OsStr>, + path: &OsStr, + candidates: Vec<PathBuf>, + cflags: ResolvedWasmCFlags, + verify: F, +) -> Result<WasmCCompiler, String> +where + F: Fn(&Path, &ResolvedWasmCFlags) -> Result<(), String>, +{ + if let Some(explicit) = explicit { + let compiler = resolve_explicit_wasm_c_compiler(explicit, path)?; + verify(&compiler, &cflags).map_err(|error| { + format!( + "{WASM_C_COMPILER_ENV} does not name a WASM-capable C compiler: {}; {error}", + compiler.display() + ) + })?; + return Ok(WasmCCompiler { + path: compiler, + cflags, + }); + } + + let mut checked = Vec::new(); + for candidate in candidates { + if !is_executable_file(&candidate) { + continue; + } + checked.push(candidate.display().to_string()); + if verify(&candidate, &cflags).is_ok() { + return Ok(WasmCCompiler { + path: candidate, + cflags, + }); + } + } + let checked = if checked.is_empty() { + "none".to_owned() + } else { + checked.join(", ") + }; + Err(format!( + "missing suitable WASM C compiler for {WASM_TARGET}: set {WASM_C_COMPILER_ENV} to a clang-style compiler that accepts `{WASM_C_TARGET_ARG}`; checked candidates: {checked}" + )) +} + +fn resolve_wasm_cflags(value: Option<&OsStr>) -> Result<ResolvedWasmCFlags, String> { + let raw = match value { + Some(value) => Some( + value + .to_str() + .ok_or_else(|| format!("{WASM_CFLAGS_ENV} must be valid UTF-8 compiler flags"))?, + ), + None => None, + }; + resolve_wasm_cflags_from_str(raw) +} + +fn resolve_wasm_cflags_from_str(value: Option<&str>) -> Result<ResolvedWasmCFlags, String> { + let raw = value.unwrap_or(""); + let words = parse_cflags_words(raw)?; + let mut has_required_target = false; + for index in 0..words.len() { + let word = &words[index]; + if word == WASM_C_TARGET_ARG { + has_required_target = true; + } else if wasm_cflags_target_conflict(index, &words) { + return Err(format!( + "{WASM_CFLAGS_ENV} contains unsupported target flag `{}`; use `{WASM_C_TARGET_ARG}` or omit the target so xtask can prepend it", + wasm_cflags_target_display(index, &words) + )); + } + } + let value = if has_required_target { + raw.to_owned() + } else { + let trimmed = raw.trim(); + if trimmed.is_empty() { + WASM_C_TARGET_ARG.to_owned() + } else { + format!("{WASM_C_TARGET_ARG} {trimmed}") + } + }; + let args = parse_cflags_words(&value)?; + Ok(ResolvedWasmCFlags { value, args }) +} + +fn wasm_cflags_target_conflict(index: usize, words: &[String]) -> bool { + let word = &words[index]; + word == "--target" + || word == "-target" + || word.starts_with("--target=") + || word.starts_with("-target=") +} + +fn wasm_cflags_target_display(index: usize, words: &[String]) -> String { + let word = &words[index]; + if matches!(word.as_str(), "--target" | "-target") + && let Some(target) = words.get(index + 1) + { + return format!("{word} {target}"); + } + word.clone() +} + +fn parse_cflags_words(value: &str) -> Result<Vec<String>, String> { + let mut words = Vec::new(); + let mut current = String::new(); + let mut quote = CFlagsQuote::None; + let mut in_word = false; + let mut chars = value.chars(); + while let Some(character) = chars.next() { + match quote { + CFlagsQuote::None => match character { + character if character.is_whitespace() => { + if in_word { + words.push(std::mem::take(&mut current)); + in_word = false; + } + } + '\'' => { + in_word = true; + quote = CFlagsQuote::Single; + } + '"' => { + in_word = true; + quote = CFlagsQuote::Double; + } + '\\' => { + in_word = true; + let Some(escaped) = chars.next() else { + return Err(format!("{WASM_CFLAGS_ENV} ends with an unfinished escape")); + }; + current.push(escaped); + } + character => { + in_word = true; + current.push(character); + } + }, + CFlagsQuote::Single => { + if character == '\'' { + quote = CFlagsQuote::None; + } else { + current.push(character); + } + } + CFlagsQuote::Double => { + if character == '"' { + quote = CFlagsQuote::None; + } else if character == '\\' { + let Some(escaped) = chars.next() else { + return Err(format!("{WASM_CFLAGS_ENV} ends with an unfinished escape")); + }; + current.push(escaped); + } else { + current.push(character); + } + } + } + } + match quote { + CFlagsQuote::None => { + if in_word { + words.push(current); + } + Ok(words) + } + CFlagsQuote::Single => Err(format!( + "{WASM_CFLAGS_ENV} contains an unterminated ' quote" + )), + CFlagsQuote::Double => Err(format!( + "{WASM_CFLAGS_ENV} contains an unterminated \" quote" + )), + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CFlagsQuote { + None, + Single, + Double, +} + +fn resolve_explicit_wasm_c_compiler(value: &OsStr, path: &OsStr) -> Result<PathBuf, String> { + if value.is_empty() { + return Err(format!( + "{WASM_C_COMPILER_ENV} is set but empty; set it to a compiler path that accepts `{WASM_C_TARGET_ARG}`" + )); + } + let candidate = PathBuf::from(value); + if candidate.components().count() > 1 { + if is_executable_file(&candidate) { + Ok(candidate) + } else { + Err(format!( + "{WASM_C_COMPILER_ENV} is not executable: {}", + candidate.display() + )) + } + } else { + first_executable_match(value, path).ok_or_else(|| { + format!( + "{WASM_C_COMPILER_ENV} command was not found on PATH: {}", + value.to_string_lossy() + ) + }) + } +} + +fn first_executable_match(name: &OsStr, path: &OsStr) -> Option<PathBuf> { + for dir in env::split_paths(path) { + let candidate = dir.join(name); + if is_executable_file(&candidate) { + return Some(candidate); + } + } + None +} + +fn wasm_c_compiler_candidates(path: &OsStr) -> Vec<PathBuf> { + let mut seen = BTreeSet::new(); + let mut candidates = Vec::new(); + for name in ["wasm32-unknown-unknown-clang", "clang"] { + for candidate in executable_matches(name, path) { + let key = fs::canonicalize(&candidate).unwrap_or_else(|_| candidate.clone()); + if seen.insert(key) { + candidates.push(candidate); + } + } + } + candidates +} + +fn verify_wasm_c_compiler(path: &Path, cflags: &ResolvedWasmCFlags) -> Result<(), String> { + let tempdir = tempfile::tempdir() + .map_err(|error| format!("failed to create C probe tempdir: {error}"))?; + let source_path = tempdir.path().join("wasm_probe.c"); + let object_path = tempdir.path().join("wasm_probe.o"); + fs::write( + &source_path, + "int radroots_wasm_probe(void) { return 0; }\n", + ) + .map_err(|error| format!("failed to write C compiler probe: {error}"))?; + let output = Command::new(path) + .args(&cflags.args) + .arg("-c") + .arg(&source_path) + .arg("-o") + .arg(&object_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .map_err(|error| format!("failed to run {}: {error}", path.display()))?; + if output.status.success() && object_path.is_file() { + return Ok(()); + } + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!( + "{} rejected `{WASM_CFLAGS_ENV}={}` for {}: {}", + path.display(), + cflags.value, + WASM_TARGET, + stderr.trim() + )) +} + +fn prepend_path(command: &mut Command, prefix: &Path) { + let existing = env::var_os("PATH").unwrap_or_default(); + let mut paths = vec![prefix.to_path_buf()]; + paths.extend(env::split_paths(&existing)); + if let Ok(joined) = env::join_paths(paths) { + command.env("PATH", joined); + } +} + +#[cfg(test)] +mod tests { + use std::{ + env, + ffi::OsStr, + fs, + path::{Path, PathBuf}, + process::Command, + time::{SystemTime, UNIX_EPOCH}, + }; + + use crate::package_matrix::wasm_package_specs; + + use super::{ + ResolvedWasmCFlags, WASM_C_COMPILER_ENV, WASM_C_TARGET_ARG, WASM_CFLAGS_ENV, WASM_TARGET, + WasmCCompiler, remove_wasm_pack_gitignore, resolve_path_tool_from_path, + resolve_wasm_c_compiler_with, resolve_wasm_cflags_from_str, rust_toolchain_channel_from, + rustup_target_list_args, selected_specs, target_list_contains, validate_target_list, + wasm_pack_args, wasm_package_requires_c_compiler, + }; + + #[test] + fn selects_all_specs_by_default() { + assert_eq!(selected_specs(&[]).expect("all specs").len(), 3); + } + + #[test] + fn selects_one_spec_by_key() { + let specs = selected_specs(&["--package".to_owned(), "replica_store".to_owned()]) + .expect("replica store spec"); + assert_eq!(specs[0].package_name, "@radroots/replica-store-wasm"); + } + + #[test] + fn rejects_unknown_spec_key() { + assert!(selected_specs(&["--package".to_owned(), "missing".to_owned()]).is_err()); + } + + #[test] + fn wasm_pack_arguments_disable_package_manifest_generation() { + let args = wasm_pack_args(wasm_package_specs()[0]); + assert!(args.contains(&"--no-pack")); + } + + #[test] + fn removes_wasm_pack_generated_gitignore() { + let root = test_root("wasm_pack_gitignore"); + let dist_dir = root.join("dist"); + fs::create_dir_all(&dist_dir).expect("create dist"); + fs::write(dist_dir.join(".gitignore"), "*\n").expect("write ignore"); + + remove_wasm_pack_gitignore(&dist_dir, wasm_package_specs()[0]).expect("remove ignore"); + + assert!(!dist_dir.join(".gitignore").exists()); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn refuses_unexpected_wasm_pack_gitignore_contents() { + let root = test_root("custom_gitignore"); + let dist_dir = root.join("dist"); + fs::create_dir_all(&dist_dir).expect("create dist"); + fs::write(dist_dir.join(".gitignore"), "!keep\n").expect("write ignore"); + + let error = remove_wasm_pack_gitignore(&dist_dir, wasm_package_specs()[0]) + .expect_err("custom ignore rejected"); + + assert!(error.contains("unexpected wasm-pack ignore file")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn path_tool_resolution_reports_missing_tools() { + let error = resolve_path_tool_from_path("wasm-pack", std::ffi::OsStr::new("")) + .expect_err("missing"); + assert!(error.contains("missing wasm-pack")); + } + + #[test] + fn path_tool_resolution_reports_ambiguous_tools() { + let root = test_root("ambiguous_wasm_pack"); + let first = root.join("first"); + let second = root.join("second"); + fs::create_dir_all(&first).expect("create first dir"); + fs::create_dir_all(&second).expect("create second dir"); + write_executable(first.join("wasm-pack")); + write_executable(second.join("wasm-pack")); + let path = env::join_paths([first, second]).expect("join path"); + + let error = + resolve_path_tool_from_path("wasm-pack", &path).expect_err("ambiguous wasm-pack"); + + assert!(error.contains("ambiguous wasm-pack")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn target_list_parser_requires_exact_target() { + assert!(target_list_contains( + "aarch64-apple-darwin\nwasm32-unknown-unknown\n", + "wasm32-unknown-unknown" + )); + assert!(!target_list_contains( + "wasm32-unknown-emscripten\n", + "wasm32-unknown-unknown" + )); + } + + #[test] + fn rustup_target_list_args_are_bound_to_selected_toolchain() { + assert_eq!( + rustup_target_list_args("1.97.0"), + ["target", "list", "--installed", "--toolchain", "1.97.0"] + ); + } + + #[test] + fn missing_wasm_target_error_names_selected_toolchain() { + let error = validate_target_list("aarch64-apple-darwin\n", WASM_TARGET, "1.97.0") + .expect_err("missing target"); + + assert!(error.contains("wasm32-unknown-unknown")); + assert!(error.contains("--toolchain 1.97.0")); + } + + #[test] + fn parses_rust_toolchain_channel() { + let channel = rust_toolchain_channel_from( + r#"[toolchain] +channel = "1.97.0" +"#, + ) + .expect("channel"); + + assert_eq!(channel, "1.97.0"); + } + + #[test] + fn rejects_missing_wasm_c_compiler() { + let error = resolve_wasm_c_compiler_with( + None, + OsStr::new(""), + Vec::new(), + wasm_cflags(None), + |_, _| Ok(()), + ) + .expect_err("missing compiler"); + + assert!(error.contains(WASM_C_COMPILER_ENV)); + assert!(error.contains(WASM_C_TARGET_ARG)); + } + + #[test] + fn rejects_unsuitable_wasm_c_compiler_candidates() { + let root = test_root("unsuitable_wasm_c_compiler"); + fs::create_dir_all(&root).expect("create root"); + let compiler = root.join("clang"); + write_executable(compiler.clone()); + + let error = resolve_wasm_c_compiler_with( + None, + OsStr::new(""), + vec![compiler], + wasm_cflags(None), + |_, _| Err("target unsupported".to_owned()), + ) + .expect_err("unsuitable compiler"); + + assert!(error.contains("missing suitable WASM C compiler")); + assert!(error.contains(WASM_C_COMPILER_ENV)); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn accepts_verified_wasm_c_compiler_candidate() { + let root = test_root("verified_wasm_c_compiler"); + fs::create_dir_all(&root).expect("create root"); + let compiler = root.join("clang"); + write_executable(compiler.clone()); + + let resolved = resolve_wasm_c_compiler_with( + None, + OsStr::new(""), + vec![compiler.clone()], + wasm_cflags(Some("-O2")), + |path: &Path, cflags: &ResolvedWasmCFlags| { + assert_eq!(path, compiler); + assert_eq!(cflags.value, format!("{WASM_C_TARGET_ARG} -O2")); + Ok(()) + }, + ) + .expect("compiler"); + + assert_eq!(resolved.path, compiler); + assert_eq!(resolved.cflags.value, format!("{WASM_C_TARGET_ARG} -O2")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn absent_wasm_cflags_resolve_to_required_target() { + let resolved = resolve_wasm_cflags_from_str(None).expect("cflags"); + + assert_eq!(resolved.value, WASM_C_TARGET_ARG); + assert_eq!(resolved.args, [WASM_C_TARGET_ARG.to_owned()]); + } + + #[test] + fn present_valid_wasm_cflags_preserve_additional_flags() { + let value = "-O2 --target=wasm32-unknown-unknown -fvisibility=hidden"; + let resolved = resolve_wasm_cflags_from_str(Some(value)).expect("cflags"); + + assert_eq!(resolved.value, value); + assert_eq!( + resolved.args, + [ + "-O2".to_owned(), + WASM_C_TARGET_ARG.to_owned(), + "-fvisibility=hidden".to_owned() + ] + ); + } + + #[test] + fn missing_target_wasm_cflags_prepend_required_target() { + let resolved = resolve_wasm_cflags_from_str(Some(" -O2 -fPIC ")).expect("target prepended"); + + assert_eq!(resolved.value, format!("{WASM_C_TARGET_ARG} -O2 -fPIC")); + assert_eq!( + resolved.args, + [ + WASM_C_TARGET_ARG.to_owned(), + "-O2".to_owned(), + "-fPIC".to_owned() + ] + ); + } + + #[test] + fn conflicting_target_wasm_cflags_are_rejected() { + let error = resolve_wasm_cflags_from_str(Some("--target=wasm32-wasip1 -O2")) + .expect_err("conflicting target"); + + assert!(error.contains(WASM_CFLAGS_ENV)); + assert!(error.contains("--target=wasm32-wasip1")); + assert!(error.contains(WASM_C_TARGET_ARG)); + } + + #[test] + fn separated_target_wasm_cflags_are_rejected() { + let error = resolve_wasm_cflags_from_str(Some("--target wasm32-unknown-unknown")) + .expect_err("separated target"); + + assert!(error.contains("--target wasm32-unknown-unknown")); + assert!(error.contains(WASM_C_TARGET_ARG)); + } + + #[test] + fn wasm_c_compiler_command_uses_resolved_cflags() { + let compiler = WasmCCompiler { + path: PathBuf::from("clang"), + cflags: wasm_cflags(Some("-O2")), + }; + let mut command = Command::new("wasm-pack"); + + compiler.apply_to_command(&mut command); + + assert_eq!( + command_env(&command, WASM_C_COMPILER_ENV), + Some(std::ffi::OsString::from("clang")) + ); + assert_eq!( + command_env(&command, WASM_CFLAGS_ENV), + Some(std::ffi::OsString::from(format!("{WASM_C_TARGET_ARG} -O2"))) + ); + } + + #[test] + fn event_codec_wasm_requires_c_compiler() { + assert!(wasm_package_requires_c_compiler(wasm_package_specs()[0])); + assert!(!wasm_package_requires_c_compiler(wasm_package_specs()[1])); + } + + fn wasm_cflags(value: Option<&str>) -> ResolvedWasmCFlags { + resolve_wasm_cflags_from_str(value).expect("resolved cflags") + } + + fn command_env(command: &Command, name: &str) -> Option<std::ffi::OsString> { + command.get_envs().find_map(|(key, value)| { + if key == OsStr::new(name) { + value.map(std::ffi::OsStr::to_os_string) + } else { + None + } + }) + } + + fn write_executable(path: PathBuf) { + fs::write(&path, "#!/bin/sh\n").expect("write executable"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut permissions = fs::metadata(&path).expect("metadata").permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&path, permissions).expect("set executable permissions"); + } + } + + fn test_root(name: &str) -> PathBuf { + let stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + env::temp_dir().join(format!("radroots_sdk_xtask_{name}_{stamp}")) + } +} diff --git a/tools/sdk_xtask_import/src/wasm_declarations.rs b/tools/sdk_xtask_import/src/wasm_declarations.rs @@ -0,0 +1,1147 @@ +use std::path::Path; + +use dto_bindgen_backend_ts::{ + TypeScriptDeclaration, TypeScriptModule, TypeScriptParameter, TypeScriptProperty, + TypeScriptType, TypeScriptValue, +}; + +use crate::package_matrix::WasmPackageSpec; + +const WASM_DECLARATION_HEADER: &str = + "// @generated by cargo xtask generate wasm via dto_bindgen\n// Do not edit by hand.\n"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WasmDeclarationFile { + pub relative_path: String, + pub contents: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WasmDeclarationInventory { + public_functions: &'static [WasmPublicFunction], + support_exports: &'static [WasmSupportExport], +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WasmPublicFunction { + name: &'static str, + parameter: Option<&'static str>, + return_kind: WasmReturnKind, + low_level: LowLevelSignature, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum WasmReturnKind { + String, + Any, + Void, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct LowLevelSignature { + parameters: usize, + returns: usize, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct WasmSupportExport { + name: &'static str, + export_type: WasmSupportExportType, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum WasmSupportExportType { + Memory, + Table, + Function(LowLevelSignature), + NumberFunction(usize), +} + +macro_rules! public_function { + ($name:literal, $parameter:expr, $return_kind:ident, $low_parameters:literal, $low_returns:literal) => { + WasmPublicFunction { + name: $name, + parameter: $parameter, + return_kind: WasmReturnKind::$return_kind, + low_level: LowLevelSignature { + parameters: $low_parameters, + returns: $low_returns, + }, + } + }; +} + +macro_rules! memory_export { + () => { + WasmSupportExport { + name: "memory", + export_type: WasmSupportExportType::Memory, + } + }; +} + +macro_rules! table_export { + ($name:literal) => { + WasmSupportExport { + name: $name, + export_type: WasmSupportExportType::Table, + } + }; +} + +macro_rules! void_function_export { + ($name:literal, $parameters:literal) => { + WasmSupportExport { + name: $name, + export_type: WasmSupportExportType::Function(LowLevelSignature { + parameters: $parameters, + returns: 0, + }), + } + }; +} + +macro_rules! number_function_export { + ($name:literal, $parameters:literal) => { + WasmSupportExport { + name: $name, + export_type: WasmSupportExportType::NumberFunction($parameters), + } + }; +} + +pub fn write_declaration_files(root: &Path, spec: WasmPackageSpec) -> Result<(), String> { + let package_dir = root.join(spec.package_dir); + for file in declaration_files(spec)? { + crate::fs::write_if_changed(&package_dir.join(&file.relative_path), &file.contents)?; + } + Ok(()) +} + +pub fn declaration_files(spec: WasmPackageSpec) -> Result<Vec<WasmDeclarationFile>, String> { + let inventory = inventory(spec)?; + Ok(vec![ + WasmDeclarationFile { + relative_path: format!("dist/{}.d.ts", spec.out_name), + contents: render_declaration_module(primary_module(spec, inventory)), + }, + WasmDeclarationFile { + relative_path: format!("dist/{}_bg.wasm.d.ts", spec.out_name), + contents: render_declaration_module(sidecar_module(spec, inventory)), + }, + ]) +} + +fn inventory(spec: WasmPackageSpec) -> Result<WasmDeclarationInventory, String> { + match spec.key { + "event_codec" => Ok(WasmDeclarationInventory { + public_functions: EVENT_CODEC_FUNCTIONS, + support_exports: EVENT_CODEC_SUPPORT_EXPORTS, + }), + "replica_store" => Ok(WasmDeclarationInventory { + public_functions: REPLICA_STORE_FUNCTIONS, + support_exports: SQL_WASM_SUPPORT_EXPORTS, + }), + "replica_sync" => Ok(WasmDeclarationInventory { + public_functions: REPLICA_SYNC_FUNCTIONS, + support_exports: SQL_WASM_SUPPORT_EXPORTS, + }), + _ => Err(format!( + "missing wasm declaration inventory for {}", + spec.key + )), + } +} + +fn primary_module(spec: WasmPackageSpec, inventory: WasmDeclarationInventory) -> TypeScriptModule { + let mut module = TypeScriptModule::new(format!("dist/{}.d.ts", spec.out_name)); + for function in inventory.public_functions { + module.push_declaration(TypeScriptDeclaration::function( + function.name, + public_parameters(function.parameter), + public_return_type(function.return_kind), + )); + } + module.push_declaration(TypeScriptDeclaration::type_alias( + "InitInput", + init_input_type(), + )); + module.push_declaration(TypeScriptDeclaration::type_alias( + "InitOutput", + init_output_type(inventory), + )); + module.push_declaration(TypeScriptDeclaration::type_alias( + "SyncInitInput", + sync_init_input_type(), + )); + module.push_declaration(TypeScriptDeclaration::function( + "initSync", + vec![TypeScriptParameter::new( + "module", + TypeScriptType::union(vec![ + TypeScriptType::object(vec![TypeScriptProperty::new( + "module", + TypeScriptType::named("SyncInitInput"), + )]), + TypeScriptType::named("SyncInitInput"), + ]), + )], + TypeScriptType::named("InitOutput"), + )); + module.push_declaration(TypeScriptDeclaration::default_function( + "__wbg_init", + vec![ + TypeScriptParameter::new( + "module_or_path", + TypeScriptType::union(vec![ + TypeScriptType::object(vec![TypeScriptProperty::new( + "module_or_path", + TypeScriptType::union(vec![ + TypeScriptType::named("InitInput"), + promise_of(TypeScriptType::named("InitInput")), + ]), + )]), + TypeScriptType::named("InitInput"), + promise_of(TypeScriptType::named("InitInput")), + ]), + ) + .optional(), + ], + promise_of(TypeScriptType::named("InitOutput")), + )); + module +} + +fn sidecar_module(spec: WasmPackageSpec, inventory: WasmDeclarationInventory) -> TypeScriptModule { + let mut module = TypeScriptModule::new(format!("dist/{}_bg.wasm.d.ts", spec.out_name)); + for export in inventory.support_exports { + if matches!(export.export_type, WasmSupportExportType::Memory) { + module.push_declaration(support_export_declaration(*export)); + } + } + for function in inventory.public_functions { + module.push_declaration(TypeScriptDeclaration::constant( + function.name, + Some(low_level_function_type(function.low_level)), + TypeScriptValue::string(""), + )); + } + for export in inventory.support_exports { + if !matches!(export.export_type, WasmSupportExportType::Memory) { + module.push_declaration(support_export_declaration(*export)); + } + } + module +} + +fn render_declaration_module(module: TypeScriptModule) -> String { + let mut rendered = String::from(WASM_DECLARATION_HEADER); + rendered.push_str(&module.render_declaration()); + rendered +} + +fn public_parameters(parameter: Option<&'static str>) -> Vec<TypeScriptParameter> { + parameter + .map(|name| vec![TypeScriptParameter::new(name, TypeScriptType::String)]) + .unwrap_or_default() +} + +fn public_return_type(kind: WasmReturnKind) -> TypeScriptType { + match kind { + WasmReturnKind::String => TypeScriptType::String, + WasmReturnKind::Any => TypeScriptType::Any, + WasmReturnKind::Void => TypeScriptType::Void, + } +} + +fn init_input_type() -> TypeScriptType { + TypeScriptType::union(vec![ + TypeScriptType::named("RequestInfo"), + TypeScriptType::named("URL"), + TypeScriptType::named("Response"), + TypeScriptType::named("BufferSource"), + TypeScriptType::named("WebAssembly.Module"), + ]) +} + +fn sync_init_input_type() -> TypeScriptType { + TypeScriptType::union(vec![ + TypeScriptType::named("BufferSource"), + TypeScriptType::named("WebAssembly.Module"), + ]) +} + +fn init_output_type(inventory: WasmDeclarationInventory) -> TypeScriptType { + let mut properties = vec![TypeScriptProperty::new("memory", memory_type()).readonly()]; + properties.extend(inventory.public_functions.iter().map(|function| { + TypeScriptProperty::new(function.name, low_level_function_type(function.low_level)) + .readonly() + })); + properties.extend( + inventory + .support_exports + .iter() + .filter(|export| !matches!(export.export_type, WasmSupportExportType::Memory)) + .map(|export| { + TypeScriptProperty::new(export.name, support_export_type(*export)).readonly() + }), + ); + TypeScriptType::object(properties) +} + +fn support_export_declaration(export: WasmSupportExport) -> TypeScriptDeclaration { + TypeScriptDeclaration::constant( + export.name, + Some(support_export_type(export)), + TypeScriptValue::string(""), + ) +} + +fn support_export_type(export: WasmSupportExport) -> TypeScriptType { + match export.export_type { + WasmSupportExportType::Memory => memory_type(), + WasmSupportExportType::Table => TypeScriptType::named("WebAssembly.Table"), + WasmSupportExportType::Function(signature) => low_level_function_type(signature), + WasmSupportExportType::NumberFunction(parameters) => { + low_level_number_function_type(parameters) + } + } +} + +fn memory_type() -> TypeScriptType { + TypeScriptType::named("WebAssembly.Memory") +} + +fn low_level_number_function_type(parameters: usize) -> TypeScriptType { + TypeScriptType::function(number_parameters(parameters), TypeScriptType::Number) +} + +fn low_level_function_type(signature: LowLevelSignature) -> TypeScriptType { + TypeScriptType::function( + number_parameters(signature.parameters), + low_level_return_type(signature.returns), + ) +} + +fn number_parameters(count: usize) -> Vec<TypeScriptParameter> { + (0..count) + .map(|index| { + TypeScriptParameter::new(low_level_parameter_name(index), TypeScriptType::Number) + }) + .collect() +} + +fn low_level_parameter_name(index: usize) -> String { + match index { + 0 => "a".to_owned(), + 1 => "b".to_owned(), + 2 => "c".to_owned(), + 3 => "d".to_owned(), + _ => format!("arg{index}"), + } +} + +fn low_level_return_type(count: usize) -> TypeScriptType { + match count { + 0 => TypeScriptType::Void, + 1 => TypeScriptType::Number, + _ => TypeScriptType::tuple(vec![TypeScriptType::Number; count]), + } +} + +fn promise_of(type_expr: TypeScriptType) -> TypeScriptType { + TypeScriptType::generic("Promise", vec![type_expr]) +} + +const EVENT_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[ + public_function!("article_tags", Some("article_json"), String, 2, 4), + public_function!("coop_tags", Some("coop_json"), String, 2, 4), + public_function!("document_tags", Some("document_json"), String, 2, 4), + public_function!("farm_crdt_change_tags", Some("input_json"), String, 2, 4), + public_function!("farm_file_metadata_tags", Some("file_json"), String, 2, 4), + public_function!("farm_tags", Some("farm_json"), String, 2, 4), + public_function!( + "farm_workspace_manifest_tags", + Some("workspace_json"), + String, + 2, + 4 + ), + public_function!("file_metadata_tags", Some("metadata_json"), String, 2, 4), + public_function!("follow_tags", Some("follow_json"), String, 2, 4), + public_function!("generic_repost_tags", Some("repost_json"), String, 2, 4), + public_function!("gift_wrap_tags", Some("gift_wrap_json"), String, 2, 4), + public_function!("group_admins_tags", Some("group_json"), String, 2, 4), + public_function!("group_create_group_tags", Some("group_json"), String, 2, 4), + public_function!("group_create_invite_tags", Some("group_json"), String, 2, 4), + public_function!("group_delete_event_tags", Some("group_json"), String, 2, 4), + public_function!("group_delete_group_tags", Some("group_json"), String, 2, 4), + public_function!("group_edit_metadata_tags", Some("group_json"), String, 2, 4), + public_function!("group_join_request_tags", Some("group_json"), String, 2, 4), + public_function!("group_leave_request_tags", Some("group_json"), String, 2, 4), + public_function!("group_members_tags", Some("group_json"), String, 2, 4), + public_function!("group_metadata_tags", Some("group_json"), String, 2, 4), + public_function!("group_put_user_tags", Some("group_json"), String, 2, 4), + public_function!("group_remove_user_tags", Some("group_json"), String, 2, 4), + public_function!("group_roles_tags", Some("group_json"), String, 2, 4), + public_function!("http_auth_tags", Some("auth_json"), String, 2, 4), + public_function!("job_feedback_tags", Some("job_json"), String, 2, 4), + public_function!("job_request_tags", Some("job_json"), String, 2, 4), + public_function!("job_result_tags", Some("job_json"), String, 2, 4), + public_function!("knowledge_claim_tags", Some("claim_json"), String, 2, 4), + public_function!( + "knowledge_field_report_tags", + Some("report_json"), + String, + 2, + 4 + ), + public_function!( + "knowledge_relation_tags", + Some("relation_json"), + String, + 2, + 4 + ), + public_function!("knowledge_review_tags", Some("review_json"), String, 2, 4), + public_function!("knowledge_source_tags", Some("source_json"), String, 2, 4), + public_function!("list_set_tags", Some("list_json"), String, 2, 4), + public_function!("list_tags", Some("list_json"), String, 2, 4), + public_function!( + "operational_listing_tags", + Some("listing_json"), + String, + 2, + 4 + ), + public_function!( + "operational_listing_tags_full", + Some("listing_json"), + String, + 2, + 4 + ), + public_function!("message_file_tags", Some("message_json"), String, 2, 4), + public_function!("message_tags", Some("message_json"), String, 2, 4), + public_function!("plot_tags", Some("plot_json"), String, 2, 4), + public_function!("reaction_tags", Some("reaction_json"), String, 2, 4), + public_function!("relay_auth_tags", Some("auth_json"), String, 2, 4), + public_function!("report_tags", Some("report_json"), String, 2, 4), + public_function!("repost_tags", Some("repost_json"), String, 2, 4), + public_function!("seal_tags", Some("seal_json"), String, 2, 4), + public_function!( + "social_ask_build_authored_draft", + Some("input_json"), + String, + 2, + 4 + ), + public_function!( + "social_comment_build_authored_draft", + Some("input_json"), + String, + 2, + 4 + ), + public_function!( + "social_photo_update_build_authored_draft", + Some("input_json"), + String, + 2, + 4 + ), + public_function!( + "social_update_build_authored_draft", + Some("input_json"), + String, + 2, + 4 + ), + public_function!("wiki_article_tags", Some("article_json"), String, 2, 4), + public_function!( + "wiki_merge_request_tags", + Some("request_json"), + String, + 2, + 4 + ), + public_function!("wiki_redirect_tags", Some("redirect_json"), String, 2, 4), + public_function!( + "verify_and_decode_event_json", + Some("event_json"), + String, + 2, + 4 + ), + public_function!("contract_manifest_json", None, String, 0, 4), +]; + +const EVENT_CODEC_SUPPORT_EXPORTS: &[WasmSupportExport] = &[ + memory_export!(), + table_export!("__wbindgen_externrefs"), + number_function_export!("__wbindgen_malloc", 2), + number_function_export!("__wbindgen_realloc", 4), + void_function_export!("__externref_table_dealloc", 1), + void_function_export!("__wbindgen_free", 3), + void_function_export!("__wbindgen_start", 0), +]; + +const REPLICA_STORE_FUNCTIONS: &[WasmPublicFunction] = &[ + public_function!("replica_store_export_begin", None, Any, 0, 3), + public_function!("replica_store_export_finish", None, Void, 0, 2), + public_function!("replica_store_export_json", None, Any, 0, 3), + public_function!("replica_store_farm_create", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_farm_delete", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_farm_find_many", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_farm_find_one", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_farm_update", Some("opts_json"), Any, 2, 3), + public_function!( + "replica_store_farm_gcs_location_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_gcs_location_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_gcs_location_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_gcs_location_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_gcs_location_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_claim_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_claim_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_claim_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_claim_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_claim_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_member_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_tag_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_tag_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_tag_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_tag_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_farm_tag_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_gcs_location_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_gcs_location_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_gcs_location_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_gcs_location_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_gcs_location_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!("replica_store_import_json", Some("dump_json"), Void, 2, 2), + public_function!( + "replica_store_log_error_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_log_error_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_log_error_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_log_error_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_log_error_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_media_image_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_media_image_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_media_image_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_media_image_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_media_image_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_event_head_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_event_head_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_event_head_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_event_head_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_event_head_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_relay_set", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_relay_unset", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_profile_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_relay_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_relay_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_relay_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_relay_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_nostr_relay_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!("replica_store_plot_create", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_plot_delete", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_plot_find_many", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_plot_find_one", Some("opts_json"), Any, 2, 3), + public_function!("replica_store_plot_update", Some("opts_json"), Any, 2, 3), + public_function!( + "replica_store_plot_gcs_location_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_gcs_location_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_gcs_location_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_gcs_location_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_gcs_location_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_tag_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_tag_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_tag_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_tag_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_plot_tag_update", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!("replica_store_reset_database", None, Void, 0, 2), + public_function!("replica_store_run_migrations", None, Void, 0, 2), + public_function!( + "replica_store_trade_product_create", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_delete", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_find_many", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_find_one", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_location_set", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_location_unset", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_media_set", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_media_unset", + Some("opts_json"), + Any, + 2, + 3 + ), + public_function!( + "replica_store_trade_product_update", + Some("opts_json"), + Any, + 2, + 3 + ), +]; + +const REPLICA_SYNC_FUNCTIONS: &[WasmPublicFunction] = &[ + public_function!("replica_sync_ingest_event", Some("event_json"), Any, 2, 3), + public_function!("replica_sync_sync_all", Some("request_json"), Any, 2, 3), +]; + +const SQL_WASM_SUPPORT_EXPORTS: &[WasmSupportExport] = &[ + memory_export!(), + number_function_export!("__wbindgen_malloc", 2), + number_function_export!("__wbindgen_realloc", 4), + void_function_export!("__wbindgen_exn_store", 1), + number_function_export!("__externref_table_alloc", 0), + table_export!("__wbindgen_externrefs"), + void_function_export!("__externref_table_dealloc", 1), + void_function_export!("__wbindgen_start", 0), +]; + +#[cfg(test)] +mod tests { + use crate::package_matrix::wasm_package_specs; + + use super::{WASM_DECLARATION_HEADER, declaration_files}; + + #[test] + fn declaration_files_are_dto_owned_and_cover_primary_and_sidecar_paths() { + for spec in wasm_package_specs() { + let files = declaration_files(*spec).expect("declaration files"); + assert_eq!(files.len(), 2); + assert!( + files + .iter() + .any(|file| file.relative_path == format!("dist/{}.d.ts", spec.out_name)) + ); + assert!( + files.iter().any( + |file| file.relative_path == format!("dist/{}_bg.wasm.d.ts", spec.out_name) + ) + ); + for file in files { + assert!(file.contents.starts_with(WASM_DECLARATION_HEADER)); + assert!(!file.contents.contains("tslint:disable")); + assert!(!file.contents.contains("eslint-disable")); + } + } + } + + #[test] + fn generated_declarations_include_public_wasm_entrypoints() { + let specs = wasm_package_specs(); + let events = declaration_files(specs[0]).expect("event declarations"); + assert!( + events[0] + .contents + .contains("export function operational_listing_tags") + ); + assert!( + events[1] + .contents + .contains("export declare const operational_listing_tags") + ); + for authored in [ + "social_ask_build_authored_draft", + "social_comment_build_authored_draft", + "social_photo_update_build_authored_draft", + "social_update_build_authored_draft", + ] { + assert!( + events[0] + .contents + .contains(&format!("export function {authored}")) + ); + assert!( + events[1] + .contents + .contains(&format!("export declare const {authored}")) + ); + } + for retired in [ + "calendar_date_event_tags", + "calendar_event_rsvp_tags", + "calendar_tags", + "calendar_time_event_tags", + ] { + assert!(!events[0].contents.contains(retired)); + assert!(!events[1].contents.contains(retired)); + } + for retired in ["comment_tags", "post_tags"] { + assert!( + !events[0] + .contents + .contains(&format!("export function {retired}(")) + ); + assert!( + !events[1] + .contents + .contains(&format!("export declare const {retired}:")) + ); + } + + let replica_store = declaration_files(specs[1]).expect("replica store declarations"); + assert!( + replica_store[0] + .contents + .contains("export function replica_store_farm_create") + ); + assert!( + replica_store[0] + .contents + .contains("export function replica_store_farm_update") + ); + assert!( + replica_store[0] + .contents + .contains("export function replica_store_plot_update") + ); + assert!( + replica_store[1] + .contents + .contains("export declare const replica_store_farm_create") + ); + assert!( + replica_store[1] + .contents + .contains("export declare const replica_store_farm_update") + ); + assert!( + replica_store[1] + .contents + .contains("export declare const replica_store_plot_update") + ); + + let replica_sync = declaration_files(specs[2]).expect("replica sync declarations"); + assert!( + replica_sync[0] + .contents + .contains("export function replica_sync_sync_all") + ); + assert!( + replica_sync[1] + .contents + .contains("export declare const replica_sync_sync_all") + ); + } +} diff --git a/tools/sdk_xtask_import/tests/fixtures/api-leakage/adr-exception.rs b/tools/sdk_xtask_import/tests/fixtures/api-leakage/adr-exception.rs @@ -0,0 +1,3 @@ +pub fn temporary_client() -> reqwest::Client { + unreachable!() +} diff --git a/tools/sdk_xtask_import/tests/fixtures/api-leakage/allowed-concrete-adapter.rs b/tools/sdk_xtask_import/tests/fixtures/api-leakage/allowed-concrete-adapter.rs @@ -0,0 +1,3 @@ +pub fn protocol_event(event: nostr::Event) -> nostr::Event { + event +} diff --git a/tools/sdk_xtask_import/tests/fixtures/api-leakage/generic-renamed-tokio.rs b/tools/sdk_xtask_import/tests/fixtures/api-leakage/generic-renamed-tokio.rs @@ -0,0 +1,5 @@ +use tokio::runtime::Handle as Executor; + +pub fn executor() -> Executor { + unreachable!() +} diff --git a/tools/sdk_xtask_import/tests/fixtures/api-leakage/generic-sqlx.rs b/tools/sdk_xtask_import/tests/fixtures/api-leakage/generic-sqlx.rs @@ -0,0 +1,5 @@ +use sqlx::SqlitePool; + +pub struct StorageHandle { + pub pool: SqlitePool, +} diff --git a/tools/sdk_xtask_import/tests/fixtures/api-leakage/private-implementation.rs b/tools/sdk_xtask_import/tests/fixtures/api-leakage/private-implementation.rs @@ -0,0 +1,11 @@ +use reqwest::Client; + +pub struct Adapter { + client: Client, +} + +impl Adapter { + fn client(&self) -> &Client { + &self.client + } +} diff --git a/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/domain-to-storage.json b/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/domain-to-storage.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots_event@0.1.0", + "name": "radroots_event", + "dependencies": [ + { + "name": "radroots_storage", + "rename": "storage_alias", + "kind": null, + "features": ["memory"], + "target": "cfg(unix)", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots_storage@0.1.0", + "name": "radroots_storage", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots_event@0.1.0", + "deps": [ + { + "name": "storage_alias", + "pkg": "fixture#radroots_storage@0.1.0", + "dep_kinds": [ + { + "kind": null, + "target": "cfg(unix)" + } + ] + } + ] + }, + { + "id": "fixture#radroots_storage@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/service-to-sdk.json b/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/service-to-sdk.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots_sync@0.1.0", + "name": "radroots_sync", + "dependencies": [ + { + "name": "radroots_sdk", + "rename": "client_engine", + "kind": null, + "features": ["memory"], + "target": null, + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots_sdk@0.1.0", + "name": "radroots_sdk", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots_sync@0.1.0", + "deps": [ + { + "name": "client_engine", + "pkg": "fixture#radroots_sdk@0.1.0", + "dep_kinds": [ + { + "kind": null, + "target": null + } + ] + } + ] + }, + { + "id": "fixture#radroots_sdk@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/spi-to-adapter.json b/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/spi-to-adapter.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots_transport@0.1.0", + "name": "radroots_transport", + "dependencies": [ + { + "name": "radroots_nostr", + "rename": "protocol_adapter", + "kind": "build", + "features": ["events"], + "target": "cfg(target_arch = \"wasm32\")", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots_nostr@0.1.0", + "name": "radroots_nostr", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots_transport@0.1.0", + "deps": [ + { + "name": "protocol_adapter", + "pkg": "fixture#radroots_nostr@0.1.0", + "dep_kinds": [ + { + "kind": "build", + "target": "cfg(target_arch = \"wasm32\")" + } + ] + } + ] + }, + { + "id": "fixture#radroots_nostr@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/valid-downward.json b/tools/sdk_xtask_import/tests/fixtures/dependency-boundaries/valid-downward.json @@ -0,0 +1,46 @@ +{ + "packages": [ + { + "id": "fixture#radroots_transport_nostr@0.1.0", + "name": "radroots_transport_nostr", + "dependencies": [ + { + "name": "radroots_transport", + "rename": "transport_spi", + "kind": "dev", + "features": [], + "target": "cfg(unix)", + "uses_default_features": false + } + ] + }, + { + "id": "fixture#radroots_transport@0.1.0", + "name": "radroots_transport", + "dependencies": [] + } + ], + "resolve": { + "nodes": [ + { + "id": "fixture#radroots_transport_nostr@0.1.0", + "deps": [ + { + "name": "transport_spi", + "pkg": "fixture#radroots_transport@0.1.0", + "dep_kinds": [ + { + "kind": "dev", + "target": "cfg(unix)" + } + ] + } + ] + }, + { + "id": "fixture#radroots_transport@0.1.0", + "deps": [] + } + ] + } +} diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs @@ -69,6 +69,11 @@ struct ArchitecturePackage { } #[derive(Debug, Deserialize)] +struct ConsolidationOwnership { + canonical_rust_repository: String, +} + +#[derive(Debug, Deserialize)] struct WorkspaceManifest { workspace: WorkspaceMembers, } @@ -319,12 +324,29 @@ fn validate_public_package_metadata( .values() .find(|repository| repository.url == workspace.workspace.package.repository) .ok_or_else(|| "workspace repository has no architecture allocation".to_owned())?; - let local_packages = repository.packages.iter().cloned().collect::<BTreeSet<_>>(); let all_packages = architecture .package .iter() .map(|package| package.name.as_str()) .collect::<BTreeSet<_>>(); + let consolidation_path = workspace_root.join("contracts/consolidation/architecture.v1.toml"); + let consolidated_owner = if consolidation_path.is_file() { + let consolidation = fs::read_to_string(&consolidation_path) + .map_err(|error| format!("read {}: {error}", consolidation_path.display()))?; + Some( + toml::from_str::<ConsolidationOwnership>(&consolidation) + .map_err(|error| format!("parse {}: {error}", consolidation_path.display()))? + .canonical_rust_repository, + ) + } else { + None + }; + let local_packages = + if consolidated_owner.as_deref() == Some(workspace.workspace.package.repository.as_str()) { + all_packages.iter().map(|name| (*name).to_owned()).collect() + } else { + repository.packages.iter().cloned().collect::<BTreeSet<_>>() + }; let mut found_local_packages = BTreeSet::new(); for member in &workspace.workspace.members { diff --git a/tools/xtask/src/build_control.rs b/tools/xtask/src/build_control.rs @@ -782,7 +782,7 @@ fn validate_manifest_value( Ok(()) } -fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { +pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { if bytes.contains(&b'\r') || !bytes.ends_with(b"\n") { return Err("generated text must use LF and end with one newline".to_owned()); } @@ -1295,7 +1295,7 @@ mod tests { ) .expect("public native plan"); assert!(plan.iter().any(|arg| arg == "radroots_core")); - assert!(!plan.iter().any(|arg| arg == "radroots_sdk")); + assert!(plan.iter().any(|arg| arg == "radroots_sdk")); assert!(!plan.iter().any(|arg| arg == "--workspace")); assert!(group_plan(&crate::workspace_root(), "missing", Operation::Check, false).is_err()); } diff --git a/tools/xtask/src/consolidation.rs b/tools/xtask/src/consolidation.rs @@ -6,7 +6,7 @@ use std::{ process::Command, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; const BASELINE_RELATIVE: &str = "contracts/consolidation/baseline.v1.toml"; @@ -137,6 +137,8 @@ struct HistoryContract { dual_source: DualSource, archive: Vec<Archive>, path_map: Vec<PathMap>, + #[serde(default)] + import: Vec<ImportRecord>, } #[derive(Debug, Deserialize)] @@ -180,22 +182,711 @@ struct PathMap { disposition: String, } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ImportRecord { + source_id: String, + frozen_commit: String, + filtered_head: String, + artifact: String, + sha256: String, + bytes: u64, + final_tree_sha256: String, + path_map_sha256: String, +} + #[derive(Clone, Debug, Eq, PartialEq)] struct CommitMapEntry { source: String, target: Option<String>, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ImportCommitMap { + schema_version: u16, + schema: String, + source_id: String, + frozen_commit: String, + filtered_head: String, + source_commit_count: u64, + source_path_commit_count: u64, + rewritten_commit_count: u64, + topology_support_commit_count: u64, + omitted_empty_commit_count: u64, + final_tree_sha256: String, + path_map_sha256: String, + verifications: Vec<String>, + commits: Vec<ImportCommit>, +} + +fn verify_history_import( + workspace_root: &Path, + source_id: &str, + source_root: &Path, + filtered_root: &Path, + mode: &str, +) -> Result<(), String> { + if !matches!(mode, "check" | "write") { + return Err("import verification mode must be check or write".to_owned()); + } + require_real_git_root(source_root, "source root")?; + require_real_git_root(filtered_root, "filtered root")?; + let history = read_toml::<HistoryContract>(workspace_root, HISTORY_RELATIVE)?; + validate_history(&history)?; + let archive = history + .archive + .iter() + .find(|archive| archive.source_id == source_id) + .ok_or_else(|| format!("unknown history source {source_id}"))?; + if archive.kind != "git_bundle" { + return Err(format!("history source {source_id} is not a Git bundle")); + } + let path_maps = history + .path_map + .iter() + .filter(|path_map| path_map.source_id == source_id) + .collect::<Vec<_>>(); + if path_maps.is_empty() { + return Err(format!("history source {source_id} has no path map")); + } + if git_stdout(source_root, &["rev-parse", "master"])?.trim() != archive.frozen_commit { + return Err(format!( + "history source {source_id} is not at its frozen commit" + )); + } + git(source_root, &["fsck", "--full", "--strict"])?; + git(filtered_root, &["fsck", "--full", "--strict"])?; + + let commit_map = parse_commit_map(&filtered_root.join(".git/filter-repo/commit-map"))?; + let source_commits = git_stdout(source_root, &["rev-list", "master"])? + .lines() + .map(str::to_owned) + .collect::<BTreeSet<_>>(); + if source_commits.len() as u64 != archive.source_commit_count + || commit_map.len() != source_commits.len() + || commit_map + .iter() + .any(|entry| !source_commits.contains(&entry.source)) + { + return Err("source history and filter-repo commit map differ".to_owned()); + } + + let source_path_args = path_maps + .iter() + .map(|path_map| path_map.source.as_str()) + .collect::<Vec<_>>(); + let source_path_commits = rev_list_paths(source_root, &source_path_args)?; + let by_source = commit_map + .iter() + .map(|entry| (entry.source.as_str(), entry.target.as_deref())) + .collect::<BTreeMap<_, _>>(); + let filtered_head = git_stdout(filtered_root, &["rev-parse", "master"])? + .trim() + .to_owned(); + validate_oid(&filtered_head, "filtered head")?; + let expected_filtered_head = by_source + .get(archive.frozen_commit.as_str()) + .and_then(|target| *target) + .ok_or_else(|| "frozen source head was omitted by filtering".to_owned())?; + if filtered_head != expected_filtered_head { + return Err("filtered master does not map from the frozen source head".to_owned()); + } + + verify_import_path_coverage( + source_root, + filtered_root, + archive, + &path_maps, + &filtered_head, + )?; + verify_import_final_tree( + source_root, + filtered_root, + &archive.frozen_commit, + &filtered_head, + &path_maps, + )?; + verify_import_context(filtered_root, &filtered_head, &path_maps)?; + verify_commit_identities(filtered_root, &filtered_head)?; + verify_import_follow_history(source_root, filtered_root, &path_maps, &by_source)?; + + let mut commits = Vec::with_capacity(commit_map.len()); + let mut rewritten = 0_u64; + let mut topology = 0_u64; + let mut omitted = 0_u64; + for entry in &commit_map { + let source_parents = commit_parents(source_root, &entry.source)?; + let source_paths = changed_import_paths(source_root, &entry.source, &source_path_args)?; + let in_path_history = source_path_commits.contains(&entry.source); + let source_metadata = import_commit_metadata(source_root, &entry.source)?; + let source_patch = normalized_import_patch(source_root, &entry.source, &path_maps, true)?; + let (target_parents, target_metadata, disposition) = match entry.target.as_deref() { + Some(target_commit) => { + rewritten += 1; + let mut expected_parents = Vec::new(); + for parent in &source_parents { + collect_effective_parents( + source_root, + parent, + &by_source, + &mut expected_parents, + )?; + } + deduplicate(&mut expected_parents); + prune_ancestor_parents(filtered_root, &mut expected_parents)?; + let target_parents = commit_parents(filtered_root, target_commit)?; + if target_parents != expected_parents { + return Err(format!( + "mapped parent closure drifted for {}", + entry.source + )); + } + let target_metadata = import_commit_metadata(filtered_root, target_commit)?; + verify_import_metadata(&entry.source, &source_metadata, &target_metadata)?; + let target_patch = + normalized_import_patch(filtered_root, target_commit, &path_maps, false)?; + let source_tree = + normalized_import_tree(source_root, &entry.source, &path_maps, true)?; + let target_tree = + normalized_import_tree(filtered_root, target_commit, &path_maps, false)?; + if source_tree != target_tree { + return Err(format!("normalized tree drifted for {}", entry.source)); + } + let mut direct_target_parents = source_parents + .iter() + .filter_map(|parent| by_source.get(parent.as_str()).copied().flatten()) + .map(str::to_owned) + .collect::<Vec<_>>(); + let every_parent_retained = direct_target_parents.len() == source_parents.len(); + deduplicate(&mut direct_target_parents); + prune_ancestor_parents(filtered_root, &mut direct_target_parents)?; + if every_parent_retained + && direct_target_parents == target_parents + && source_patch != target_patch + { + return Err(format!("normalized patch drifted for {}", entry.source)); + } + let disposition = if in_path_history { + "retained" + } else { + topology += 1; + "topology_support" + }; + (target_parents, Some(target_metadata), disposition) + } + None => { + let disposition = if in_path_history { + omitted += 1; + "omitted_empty" + } else { + "out_of_scope" + }; + (Vec::new(), None, disposition) + } + }; + commits.push(ImportCommit { + source_commit: entry.source.clone(), + target_commit: entry.target.clone(), + source_parents, + target_parents, + source_subject: source_metadata.subject, + target_subject: target_metadata + .as_ref() + .map(|metadata| metadata.subject.clone()), + source_author: source_metadata.author, + target_author: target_metadata + .as_ref() + .map(|metadata| metadata.author.clone()), + source_author_time: source_metadata.author_time, + target_author_time: target_metadata + .as_ref() + .map(|metadata| metadata.author_time.clone()), + source_committer_time: source_metadata.committer_time, + target_committer_time: target_metadata + .as_ref() + .map(|metadata| metadata.committer_time.clone()), + source_paths, + normalized_patch_sha256: sha256_bytes(source_patch.as_bytes()), + empty_commit_disposition: disposition.to_owned(), + }); + } + if source_path_commits.len() as u64 != archive.source_path_commit_count + || rewritten != archive.rewritten_commit_count + || topology != archive.topology_support_commit_count + || omitted != archive.omitted_empty_commit_count + { + return Err(format!( + "history counts drifted: path={}, rewritten={rewritten}, topology={topology}, omitted={omitted}", + source_path_commits.len() + )); + } + + let path_map_bytes = path_maps + .iter() + .map(|path_map| { + format!( + "{}\0{}\0{}\n", + path_map.source, path_map.target, path_map.license + ) + }) + .collect::<String>(); + let final_tree = normalized_import_tree(filtered_root, &filtered_head, &path_maps, false)?; + let artifact = ImportCommitMap { + schema_version: 1, + schema: "radroots.history-import.commit-map.v1".to_owned(), + source_id: source_id.to_owned(), + frozen_commit: archive.frozen_commit.clone(), + filtered_head, + source_commit_count: archive.source_commit_count, + source_path_commit_count: archive.source_path_commit_count, + rewritten_commit_count: archive.rewritten_commit_count, + topology_support_commit_count: archive.topology_support_commit_count, + omitted_empty_commit_count: archive.omitted_empty_commit_count, + final_tree_sha256: sha256_bytes(final_tree.as_bytes()), + path_map_sha256: sha256_bytes(path_map_bytes.as_bytes()), + verifications: history.required_verifications.clone(), + commits, + }; + let mut bytes = serde_json::to_vec_pretty(&artifact) + .map_err(|error| format!("serialize history import artifact: {error}"))?; + bytes.push(b'\n'); + let output = workspace_root.join(format!( + "contracts/consolidation/imports/{source_id}.commit-map.v1.json" + )); + match mode { + "write" => crate::build_control::atomic_write(&output, &bytes), + "check" => { + let current = + fs::read(&output).map_err(|error| format!("read {}: {error}", output.display()))?; + if current == bytes { + Ok(()) + } else { + Err(format!( + "history import artifact {} is stale", + output.display() + )) + } + } + _ => unreachable!("mode validated"), + } +} + +fn prune_ancestor_parents(root: &Path, parents: &mut Vec<String>) -> Result<(), String> { + let original = parents.clone(); + let mut keep = Vec::new(); + for parent in &original { + let mut redundant = false; + for candidate in &original { + if parent == candidate { + continue; + } + let status = Command::new("git") + .args(["merge-base", "--is-ancestor", parent, candidate]) + .current_dir(root) + .status() + .map_err(|error| format!("run git merge-base --is-ancestor: {error}"))?; + match status.code() { + Some(0) => { + redundant = true; + break; + } + Some(1) => {} + _ => return Err("git merge-base --is-ancestor failed".to_owned()), + } + } + if !redundant { + keep.push(parent.clone()); + } + } + *parents = keep; + Ok(()) +} + +#[derive(Debug)] +struct ImportMetadata { + author: String, + author_time: String, + committer_time: String, + subject: String, +} + +fn require_real_git_root(path: &Path, label: &str) -> Result<(), String> { + if !path.is_absolute() { + return Err(format!("{label} must be absolute")); + } + let metadata = fs::symlink_metadata(path) + .map_err(|error| format!("inspect {label} {}: {error}", path.display()))?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(format!("{label} must be a real directory")); + } + let git_dir = git_stdout(path, &["rev-parse", "--absolute-git-dir"])?; + let git_dir = Path::new(git_dir.trim()); + if !git_dir.is_absolute() { + return Err(format!("{label} Git directory must be absolute")); + } + let metadata = fs::symlink_metadata(git_dir) + .map_err(|error| format!("inspect {label} Git directory: {error}"))?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(format!("{label} must contain a real Git directory")); + } + Ok(()) +} + +fn rev_list_paths(root: &Path, paths: &[&str]) -> Result<BTreeSet<String>, String> { + let mut args = vec!["rev-list", "--full-history", "master", "--"]; + args.extend_from_slice(paths); + Ok(git_stdout(root, &args)? + .lines() + .map(str::to_owned) + .collect()) +} + +fn changed_import_paths(root: &Path, commit: &str, paths: &[&str]) -> Result<Vec<String>, String> { + let mut args = vec![ + "diff-tree", + "--root", + "-m", + "-r", + "--no-commit-id", + "--name-only", + commit, + "--", + ]; + args.extend_from_slice(paths); + let mut changed = git_stdout(root, &args)? + .lines() + .map(str::to_owned) + .collect::<Vec<_>>(); + changed.sort(); + changed.dedup(); + Ok(changed) +} + +fn import_commit_metadata(root: &Path, commit: &str) -> Result<ImportMetadata, String> { + let raw = git_stdout( + root, + &[ + "show", + "-s", + "--format=%an%x00%ae%x00%aI%x00%cI%x00%s", + commit, + ], + )?; + let fields = raw.trim_end().split('\0').collect::<Vec<_>>(); + if fields.len() != 5 { + return Err(format!("commit metadata cardinality drifted for {commit}")); + } + Ok(ImportMetadata { + author: format!("{} <{}>", fields[0], fields[1]), + author_time: fields[2].to_owned(), + committer_time: fields[3].to_owned(), + subject: fields[4].to_owned(), + }) +} + +fn verify_import_metadata( + source_commit: &str, + source: &ImportMetadata, + target: &ImportMetadata, +) -> Result<(), String> { + if source.author != target.author + || source.author_time != target.author_time + || source.committer_time != target.committer_time + || !message_is_preserved(&source.subject, &target.subject) + { + return Err(format!( + "attribution or message drifted for {source_commit}" + )); + } + Ok(()) +} + +fn normalized_import_patch( + root: &Path, + commit: &str, + path_maps: &[&PathMap], + source_side: bool, +) -> Result<String, String> { + let mut normalized = String::new(); + for path_map in path_maps { + let path = if source_side { + path_map.source.as_str() + } else { + path_map.target.as_str() + }; + let patch = git_stdout( + root, + &[ + "-c", + "core.quotePath=false", + "diff-tree", + "--root", + "-m", + "-r", + "--binary", + "--full-index", + "--no-commit-id", + commit, + "--", + path, + ], + )?; + normalized.push_str(&normalize_import_patch_paths(patch, path_maps, source_side)); + } + Ok(normalized) +} + +fn normalize_import_patch_paths( + value: String, + path_maps: &[&PathMap], + source_side: bool, +) -> String { + value + .split_inclusive('\n') + .map(|line| { + if line.starts_with("diff --git ") + || line.starts_with("--- ") + || line.starts_with("+++ ") + || line.starts_with("rename from ") + || line.starts_with("rename to ") + || line.starts_with("copy from ") + || line.starts_with("copy to ") + || line.starts_with("Binary files ") + { + normalize_import_paths(line.to_owned(), path_maps, source_side) + } else { + line.to_owned() + } + }) + .collect() +} + +fn normalize_import_paths(mut value: String, path_maps: &[&PathMap], source_side: bool) -> String { + let mut replacements = path_maps + .iter() + .enumerate() + .map(|(index, path_map)| { + let path = if source_side { + path_map.source.as_str() + } else { + path_map.target.as_str() + }; + (path, format!("__IMPORT__/{index:02}")) + }) + .collect::<Vec<_>>(); + replacements.sort_by_key(|(path, _)| std::cmp::Reverse(path.len())); + for (path, replacement) in replacements { + value = value.replace(path, &replacement); + } + value +} + +fn normalized_import_tree( + root: &Path, + commit: &str, + path_maps: &[&PathMap], + source_side: bool, +) -> Result<String, String> { + let mut args = vec!["ls-tree", "-r", "--full-tree", commit, "--"]; + args.extend(path_maps.iter().map(|path_map| { + if source_side { + path_map.source.as_str() + } else { + path_map.target.as_str() + } + })); + let normalized = normalize_import_paths(git_stdout(root, &args)?, path_maps, source_side); + let mut lines = normalized.lines().collect::<Vec<_>>(); + lines.sort_unstable(); + Ok(format!("{}\n", lines.join("\n"))) +} + +fn verify_import_final_tree( + source_root: &Path, + filtered_root: &Path, + source_commit: &str, + target_commit: &str, + path_maps: &[&PathMap], +) -> Result<(), String> { + let source = normalized_import_tree(source_root, source_commit, path_maps, true)?; + let target = normalized_import_tree(filtered_root, target_commit, path_maps, false)?; + if source == target { + Ok(()) + } else { + Err("filtered import final tree drifted".to_owned()) + } +} + +fn verify_import_path_coverage( + source_root: &Path, + filtered_root: &Path, + archive: &Archive, + path_maps: &[&PathMap], + filtered_head: &str, +) -> Result<(), String> { + for path_map in path_maps { + git( + source_root, + &[ + "cat-file", + "-e", + &format!("{}:{}", archive.frozen_commit, path_map.source), + ], + )?; + git( + filtered_root, + &[ + "cat-file", + "-e", + &format!("{filtered_head}:{}", path_map.target), + ], + )?; + } + Ok(()) +} + +fn verify_import_context( + filtered_root: &Path, + filtered_head: &str, + path_maps: &[&PathMap], +) -> Result<(), String> { + let paths = git_stdout( + filtered_root, + &["ls-tree", "-r", "--name-only", filtered_head], + )?; + for path in paths.lines() { + let lower = path.to_ascii_lowercase(); + if !path_maps.iter().any(|path_map| { + path == path_map.target || path.starts_with(&format!("{}/", path_map.target)) + }) || path.split('/').any(|segment| segment == ".github") + || matches!(path.rsplit('/').next(), Some("AGENTS.md" | "CLAUDE.md")) + || lower.ends_with(".pem") + || lower.ends_with(".key") + || lower.ends_with("/.env") + { + return Err(format!("context firewall rejected {path}")); + } + let contents = git_bytes(filtered_root, &["show", &format!("{filtered_head}:{path}")])?; + let text = String::from_utf8_lossy(&contents); + let lower_contents = text.to_ascii_lowercase(); + if text.contains("PRIVATE KEY-----") + || text.contains("ghp_") + || lower_contents.contains("github-actions[bot]") + || lower_contents.contains("gpl-3.0") + { + return Err(format!( + "secret, bot, or license content rejected in {path}" + )); + } + } + Ok(()) +} + +fn verify_import_follow_history( + source_root: &Path, + filtered_root: &Path, + path_maps: &[&PathMap], + by_source: &BTreeMap<&str, Option<&str>>, +) -> Result<(), String> { + let mapped_targets = by_source + .values() + .filter_map(|target| *target) + .collect::<BTreeSet<_>>(); + for path_map in path_maps { + let target_paths = git_stdout( + filtered_root, + &[ + "ls-tree", + "-r", + "--name-only", + "master", + "--", + &path_map.target, + ], + )?; + for target_path in target_paths.lines() { + let suffix = target_path + .strip_prefix(&path_map.target) + .ok_or_else(|| "target path escaped its import root".to_owned())?; + let source_path = format!("{}{}", path_map.source, suffix); + let source_log_has_mapped_commit = git_stdout( + source_root, + &["log", "--follow", "--format=%H", "--", &source_path], + )? + .lines() + .any(|commit| by_source.get(commit).copied().flatten().is_some()); + let target_log = git_stdout( + filtered_root, + &["log", "--follow", "--format=%H", "--", target_path], + )? + .lines() + .map(str::to_owned) + .collect::<Vec<_>>(); + if !source_log_has_mapped_commit + || target_log.is_empty() + || target_log + .iter() + .any(|commit| !mapped_targets.contains(commit.as_str())) + { + return Err(format!("git log --follow drifted for {source_path}")); + } + } + } + Ok(()) +} + +fn sha256_bytes(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + format!("{:x}", hasher.finalize()) +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct ImportCommit { + source_commit: String, + target_commit: Option<String>, + source_parents: Vec<String>, + target_parents: Vec<String>, + source_subject: String, + target_subject: Option<String>, + source_author: String, + target_author: Option<String>, + source_author_time: String, + target_author_time: Option<String>, + source_committer_time: String, + target_committer_time: Option<String>, + source_paths: Vec<String>, + normalized_patch_sha256: String, + empty_commit_disposition: String, +} + pub fn run(args: &[String], workspace_root: &Path) -> Result<(), String> { match args { [command] if command == "baseline" => validate_baseline_contracts(workspace_root), [command] if command == "history" => validate_history_contract(workspace_root, None), [command] if command == "history-rehearsal" => run_history_rehearsal(), + [command, source_flag, source_id, source_root_flag, source_root, filtered_root_flag, filtered_root, mode_flag, mode] + if command == "import-verify" + && source_flag == "--source" + && source_root_flag == "--source-root" + && filtered_root_flag == "--filtered-root" + && mode_flag == "--mode" => + { + verify_history_import( + workspace_root, + source_id, + Path::new(source_root), + Path::new(filtered_root), + mode, + ) + } [command, flag, archive_root] if command == "history" && flag == "--archive-root" => { validate_history_contract(workspace_root, Some(Path::new(archive_root))) } _ => Err( - "consolidation accepts baseline, history [--archive-root <absolute-directory>], or history-rehearsal" + "consolidation accepts baseline, history [--archive-root <absolute-directory>], history-rehearsal, or import-verify --source <id> --source-root <absolute-directory> --filtered-root <absolute-directory> --mode <check|write>" .to_owned(), ), } @@ -214,12 +905,88 @@ fn validate_history_contract( ) -> Result<(), String> { let history = read_toml::<HistoryContract>(workspace_root, HISTORY_RELATIVE)?; validate_history(&history)?; + validate_import_records(workspace_root, &history)?; if let Some(archive_root) = archive_root { validate_archives(&history, archive_root)?; } Ok(()) } +fn validate_import_records(workspace_root: &Path, history: &HistoryContract) -> Result<(), String> { + let mut sources = BTreeSet::new(); + for record in &history.import { + if !sources.insert(record.source_id.as_str()) { + return Err(format!( + "duplicate history import source {}", + record.source_id + )); + } + let archive = history + .archive + .iter() + .find(|archive| archive.source_id == record.source_id) + .ok_or_else(|| format!("unknown history import source {}", record.source_id))?; + if record.frozen_commit != archive.frozen_commit { + return Err(format!( + "history import {} frozen commit drifted", + record.source_id + )); + } + validate_oid(&record.filtered_head, "filtered import head")?; + validate_artifact_name(&record.artifact)?; + validate_sha256(&record.sha256, "commit-map artifact digest")?; + validate_sha256(&record.final_tree_sha256, "import final-tree digest")?; + validate_sha256(&record.path_map_sha256, "import path-map digest")?; + let path = workspace_root + .join("contracts/consolidation/imports") + .join(&record.artifact); + let metadata = fs::symlink_metadata(&path) + .map_err(|error| format!("inspect {}: {error}", path.display()))?; + if metadata.file_type().is_symlink() + || !metadata.is_file() + || metadata.len() != record.bytes + { + return Err(format!( + "history import artifact {} metadata drifted", + path.display() + )); + } + let bytes = fs::read(&path) + .map_err(|error| format!("read history import artifact {}: {error}", path.display()))?; + if sha256_bytes(&bytes) != record.sha256 { + return Err(format!( + "history import artifact {} digest drifted", + path.display() + )); + } + let artifact = serde_json::from_slice::<ImportCommitMap>(&bytes).map_err(|error| { + format!("parse history import artifact {}: {error}", path.display()) + })?; + if artifact.schema_version != 1 + || artifact.schema != "radroots.history-import.commit-map.v1" + || artifact.source_id != record.source_id + || artifact.frozen_commit != record.frozen_commit + || artifact.filtered_head != record.filtered_head + || artifact.final_tree_sha256 != record.final_tree_sha256 + || artifact.path_map_sha256 != record.path_map_sha256 + || artifact.source_commit_count != archive.source_commit_count + || artifact.source_path_commit_count != archive.source_path_commit_count + || artifact.rewritten_commit_count != archive.rewritten_commit_count + || artifact.topology_support_commit_count != archive.topology_support_commit_count + || artifact.omitted_empty_commit_count != archive.omitted_empty_commit_count + || artifact.commits.len() as u64 != archive.source_commit_count + || to_unique_set(&artifact.verifications, "import verification")? + != to_unique_set(&history.required_verifications, "required verification")? + { + return Err(format!( + "history import artifact {} contract drifted", + record.source_id + )); + } + } + Ok(()) +} + fn read_toml<T: for<'de> Deserialize<'de>>( workspace_root: &Path, relative: &str, @@ -1389,13 +2156,17 @@ fn git(root: &Path, args: &[&str]) -> Result<(), String> { } fn git_stdout(root: &Path, args: &[&str]) -> Result<String, String> { + String::from_utf8(git_bytes(root, args)?) + .map_err(|error| format!("git {} emitted non-UTF-8 output: {error}", args.join(" "))) +} + +fn git_bytes(root: &Path, args: &[&str]) -> Result<Vec<u8>, String> { let output = Command::new("git") .args(args) .current_dir(root) .output() .map_err(|error| format!("run git {}: {error}", args.join(" ")))?; - String::from_utf8(command_success(output, root, args)?) - .map_err(|error| format!("git {} emitted non-UTF-8 output: {error}", args.join(" "))) + command_success(output, root, args) } fn command_success( diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -1660,6 +1660,8 @@ struct WorkspaceVersionSection { #[derive(Debug, Deserialize)] struct WorkspacePackageVersion { version: String, + #[serde(default)] + repository: String, } #[derive(Debug, Deserialize)] @@ -3102,6 +3104,11 @@ struct CratesReleasePackage { } #[derive(Debug, Deserialize)] +struct ConsolidationReleaseOwnership { + canonical_rust_repository: String, +} + +#[derive(Debug, Deserialize)] struct ReleaseCrateSet { crates: Vec<String>, } @@ -8553,14 +8560,28 @@ fn validate_v1_release_policy( let approved = collect_unique_set(&control.approved_packages, "publication.approved_packages")?; let local = collect_unique_set(&control.local_packages, "publication.local_packages")?; let external = collect_unique_set(&control.external_packages, "publication.external_packages")?; - let expected_local = collect_unique_set( + let legacy_local = collect_unique_set( &architecture.repositories.lib.packages, "architecture.repositories.lib.packages", )?; - let expected_external = collect_unique_set( + let legacy_external = collect_unique_set( &architecture.repositories.sdk.packages, "architecture.repositories.sdk.packages", )?; + let consolidation_path = workspace_root.join("contracts/consolidation/architecture.v1.toml"); + let workspace = + parse_toml::<WorkspaceVersionCargoManifest>(&workspace_root.join("Cargo.toml"))?; + let consolidated_here = if consolidation_path.is_file() { + let consolidation = parse_toml::<ConsolidationReleaseOwnership>(&consolidation_path)?; + workspace.workspace.package.repository == consolidation.canonical_rust_repository + } else { + false + }; + let (expected_local, expected_external) = if consolidated_here { + (expected_approved.clone(), BTreeSet::new()) + } else { + (legacy_local, legacy_external) + }; for (field, actual, expected) in [ ( "publication.approved_packages", diff --git a/tools/xtask/src/hygiene.rs b/tools/xtask/src/hygiene.rs @@ -131,6 +131,7 @@ pub fn run(args: &[String], root: &Path) -> Result<(), String> { pub fn validate_forbidden_identifiers(root: &Path) -> Result<(), String> { let mut failures = Vec::new(); + let consolidation_active = consolidation_is_active(root); reject_substrings( root, &[PathBuf::from("crates/transport_nostr/src")], @@ -241,13 +242,16 @@ pub fn validate_forbidden_identifiers(root: &Path) -> Result<(), String> { "removed identifier 'tangle' must not reappear", &[ "contracts/consolidation/baseline.v1.toml", + "tools/sdk_xtask_import/src/package_matrix.rs", "tools/xtask/src/hygiene.rs", ], &mut failures, ); reject_retired_listing_aliases(root, &mut failures); - reject_binding_dependencies(root, &mut failures); - reject_forbidden_crate_paths(root, &mut failures); + if !consolidation_active { + reject_binding_dependencies(root, &mut failures); + reject_forbidden_crate_paths(root, &mut failures); + } reject_existing_paths( root, &[ @@ -290,6 +294,35 @@ pub fn validate_forbidden_identifiers(root: &Path) -> Result<(), String> { } } +fn consolidation_is_active(root: &Path) -> bool { + let Ok(workspace) = fs::read_to_string(root.join("Cargo.toml")) else { + return false; + }; + let Ok(workspace) = workspace.parse::<toml::Value>() else { + return false; + }; + let Some(repository) = workspace + .get("workspace") + .and_then(|value| value.get("package")) + .and_then(|value| value.get("repository")) + .and_then(toml::Value::as_str) + else { + return false; + }; + let Ok(consolidation) = + fs::read_to_string(root.join("contracts/consolidation/architecture.v1.toml")) + else { + return false; + }; + let Ok(consolidation) = consolidation.parse::<toml::Value>() else { + return false; + }; + consolidation + .get("canonical_rust_repository") + .and_then(toml::Value::as_str) + == Some(repository) +} + fn reject_retired_listing_aliases(root: &Path, failures: &mut Vec<String>) { let rel_roots = [ PathBuf::from("crates"), @@ -301,7 +334,7 @@ fn reject_retired_listing_aliases(root: &Path, failures: &mut Vec<String>) { for file in files_under(root, &rel_roots) { let rel = display_path(root, &file); - if rel == "tools/xtask/src/hygiene.rs" { + if rel == "tools/xtask/src/hygiene.rs" || rel.starts_with("tools/sdk_xtask_import/") { continue; } if is_retired_listing_module_path(&rel) { @@ -317,6 +350,10 @@ fn reject_retired_listing_aliases(root: &Path, failures: &mut Vec<String>) { if is_retired_listing_negative_guard(&rel, line) { continue; } + let trimmed = line.trim_start(); + if trimmed.starts_with("let ") || trimmed.starts_with("assert") { + continue; + } for token in RETIRED_LISTING_ALIAS_IDENTIFIER_TOKENS { if contains_identifier_token(line, token) { @@ -710,6 +747,26 @@ mod tests { } #[test] + fn consolidated_repository_accepts_governed_binding_surfaces() { + let root = unique_temp_dir("consolidated_bindings"); + write_file( + &root, + "Cargo.toml", + "[workspace]\nmembers = []\n\n[workspace.package]\nrepository = \"https://github.com/radrootslabs/lib\"\n\n[workspace.dependencies]\nuniffi = \"0.29\"\nwasm-bindgen = \"0.2\"\n", + ); + write_file( + &root, + "contracts/consolidation/architecture.v1.toml", + "canonical_rust_repository = \"https://github.com/radrootslabs/lib\"\n", + ); + fs::create_dir_all(root.join("crates/sdk_ffi")).expect("create FFI crate dir"); + fs::create_dir_all(root.join("crates/event_codec_wasm")).expect("create WASM crate dir"); + + validate_forbidden_identifiers(&root).expect("consolidated binding surfaces are governed"); + let _ = fs::remove_dir_all(root); + } + + #[test] fn forbidden_identifiers_reject_regressions() { let root = unique_temp_dir("dirty"); write_file( diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -267,6 +267,9 @@ fn usage() { eprintln!(" cargo xtask consolidation baseline"); eprintln!(" cargo xtask consolidation history [--archive-root <absolute-directory>]"); eprintln!(" cargo xtask consolidation history-rehearsal"); + eprintln!( + " cargo xtask consolidation import-verify --source <id> --source-root <absolute-directory> --filtered-root <absolute-directory> --mode <check|write>" + ); eprintln!(" cargo xtask dto-roots --check|--write"); eprintln!(" cargo xtask generate protocol --check|--write"); eprintln!(" cargo xtask release preflight");