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:
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, §ion)?;
+ 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, §ion)?;
+ }
+ }
+ 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, §ion)?;
+ 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("ed);
+ 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 != ¤t_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 != ¤t_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");