commit f7a7d8bf62211ac090c922ba0a917bf3548c5b2f
parent 8cea39ce833e5136a2a166a51b2670ec4bf8a20f
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 11:54:12 +0000
sdk: implement the approved SDK feature vocabulary
- expose only the approved capability-oriented feature names
- activate optional dependencies explicitly through owning features
- document the complete standalone SDK qualification matrix
- lock the vocabulary with package-boundary regression coverage
Diffstat:
4 files changed, 162 insertions(+), 139 deletions(-)
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -31,6 +31,7 @@ nip46 = [
"dep:radroots_nostr_connect",
]
local-signing = [
+ "dep:radroots_nostr",
"dep:radroots_secrets",
"radroots_nostr/signing",
]
diff --git a/crates/sdk/README.md b/crates/sdk/README.md
@@ -1,143 +1,99 @@
# radroots_sdk
-Curated Radroots Rust SDK for local-first Radroots product workflows.
-
-The SDK v1 product runtime is centered on `RadrootsClient::builder()`,
-`sdk.farms()`, `sdk.listings()`, `sdk.trades()`, `sdk.market()`, and `sdk.sync()`.
-
-The current refactor exposes explicit memory or SQLite storage composition and
-never creates files until the host invokes an I/O constructor. Canonical SQLite
-storage owns only `runtime.sqlite` and `private.sqlite`; application presentation
-state belongs to the host.
-
-## Studio state migration
-
-The predecessor SDK-owned `studio.sqlite` database is not opened, copied,
-backed up, restored, or deleted by this release. Before upgrading, a Studio host
-that needs values from that file must use the predecessor version to export the
-application state, validate the export, and import it into a host-owned schema.
-The host must retain its original file until it has independently verified the
-new state. This SDK intentionally provides no dual read, dual write, implicit
-migration, or fallback path.
-
-When `signer-adapters` is enabled, `RadrootsClient::builder()` accepts a configured
-`RadrootsSdkSignerProvider`. The production signing modes are `local_key` and `myc_nip46`. Product
-enqueue methods use that configured provider, so callers do not pass signing internals through every
-farm, listing, or trade write call. `signer_status()`, `configured_signer()`, and
-`sign_with_configured_signer(...)` expose the typed SDK signer boundary.
-
-`sdk.listings().prepare_publish(...)` is side-effect-free and returns a typed
-`ListingPublishPlan`. With a configured signer, `sdk.listings().enqueue_prepared_publish(...)` signs
-that prepared plan, ingests it into the local event store, queues signed outbox work, and returns a
-typed `ListingEnqueueReceipt`. `sdk.listings().enqueue_publish(...)` is the convenience path that
-prepares once and delegates to `enqueue_prepared_publish(...)`. Farm and trade write methods follow
-the same configured signer pattern. The enqueue path uses typed transport target and idempotency
-inputs; write requests require explicit UUIDv7 idempotency keys. `SatisfactionPolicy::NoWait`
-records first-class no-wait delivery plans, so enqueue-only product workflows can complete without
-accepted or delivered transport targets and without fabricated target delivery receipts.
-
-Event `created_at` and local observation time are separate contracts. The event timestamp remains
-the authored event-envelope timestamp, while event-store and outbox mutation timestamps use the SDK
-clock at enqueue time. Listing enqueue receipts report mutation state with the product names
-`StoredAndQueued` and `AlreadyQueued`.
-
-`sdk.sync().push_outbox(...)` is the product sync entrypoint. It publishes queued signed outbox work
-when `transport-nostr-runtime` is enabled. Push time uses the delivery targets already stored on each
-queued outbox event, so already queued work does not require a configured builder profile. Reticulum
-work reports explicit deferred-until-implemented receipt state with zero network
-attempts; it does not deliver and does not fall back to another transport. Nostr transport publishing
-and radrootsd execution consume signed outbox events; neither path owns signing.
-`push_outbox_with_transport(...)` remains available for tests and controlled transport-level
-substrate checks. `radrootsd-execution` adds daemon-resolved publishing through `publish.event`.
-
-`sdk.trades()` exposes the local trade runtime surface. With `runtime`, callers can read
-`sdk.trades().capabilities()`, seal/open/delete protected private artifacts, issue semantic trade
-commands through `sdk.trades().commands()`, and read semantic projections through
-`sdk.trades().queries()`. Query methods read local projections and evidence; they are not network
-fetches.
-
-Configured-signer trade writes compile when both `runtime` and `signer-adapters` are enabled.
-Trade write workflows use `SubmitProposalRequest`, `ProposeRevisionRequest`,
-`DecideCandidateRequest`, `CancelTradeRequest`, and `ResumeOperationRequest` through the command
-service. Runtime-only builds keep explicit-signer methods available and do not expose a parallel
-buyer/seller workflow namespace.
-
-The `local-runtime` feature is the curated feature bundle for local product runtime consumers. It
-enables `std`, `serde`, `serde_json`, `runtime`, `signer-adapters`, `transport-nostr-runtime`, and
-`transport-nostr-client`. `signer-adapters` contains the SDK `local_key` and `myc_nip46` signing surface.
-`transport-nostr-client` supplies the Nostr relay WebSocket publish adapter used when signed outbox work
-targets Nostr relay URLs. `local-runtime-radrootsd-execution` uses daemon-resolved publishing through
-`publish.event` and the same configured signer provider API.
-
-`radroots_sdk_myc_nip46_product_permissions()` and
-`radroots_sdk_myc_nip46_product_permission_strings()` expose the SDK product NIP-46
-`sign_event:<kind>` permission set for Myc connection setup. The set is derived from the farm,
-listing, and canonical trade mutation event kinds the SDK can write.
-
-Relay URL policy is explicit. Public relay URLs must use `wss://`. Local development `ws://` relay
-URLs are accepted only under `NostrRelayUrlPolicy::Localhost` and only for `localhost`, `127.0.0.1`,
-or `[::1]`. Non-local insecure `ws://` targets, including private LAN addresses, are rejected.
-
-Low-level event-contract, wire-codec, reducer, and agreement-attestation contract ownership lives in
-the shared Radroots libraries. The SDK exposes product APIs and selected adapter boundaries; it does
-not expose a public workflow-bypass namespace for product callers.
-
-## Examples
-
-Compile the SDK v1 product examples with:
-
-```bash
-cargo check -p radroots_sdk --example sdk_v1_listing_prepare --features runtime
-cargo check -p radroots_sdk --example sdk_v1_knowledge_prepare --features knowledge
-cargo check -p radroots_sdk --example sdk_v1_local_enqueue_and_mock_sync --features runtime,signer-adapters
-cargo check -p radroots_sdk --example sdk_v1_myc_nip46_signer_setup --features runtime,signer-adapters
+`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.
+
+## 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
```
-`sdk_v1_listing_prepare` shows `RadrootsClient::builder()`,
-`ListingPreparePublishRequest`, and `ListingPublishPlan`.
-`sdk_v1_knowledge_prepare` shows `radroots_sdk::knowledge`, typed event builders, frozen draft
-preparation, signed-event verification, decoded event matching, and manifest hash access without
-local runtime storage.
-`sdk_v1_local_enqueue_and_mock_sync` shows localhost Nostr target selection, configured local-key
-signing, prepared listing enqueue, `push_outbox_with_transport(...)` with a Nostr transport facade, and
-queued outbox sync. `sdk_v1_myc_nip46_signer_setup` shows Myc NIP-46 signer provider setup and
-product permission derivation at the SDK boundary. The examples stay on product APIs and do not use
-wire-event internals.
-
-SDK v1 does not claim strict NIP-99 Markdown-content interoperability. It emits and consumes
-Radroots v1 listing and trade event contracts.
-
-Optional advanced substrate is explicitly feature-scoped:
-
-- `identity-models`: identity data types without local storage coupling
-- `signing`: dependency substrate for curated Nostr adapters; it exposes no
- generic builder or caller-constructed wire-part signing module
-- `transport-nostr-client`: Nostr relay WebSocket client and publish adapters
-- `signer-adapters`: SDK local-key and Myc NIP-46 signer providers plus configured-signer product
- write APIs
-
-The crate is licensed as `MIT OR Apache-2.0`. Its manifest is configured for a future crates.io
-release, but a public release still requires the full SDK check lane, generated artifact
-reproducibility checks, metadata review, and a publish dry run.
-
-Runtime errors use a non-exhaustive `RadrootsSdkError` enum. Public callers should branch on the
-stable method surface: `code`, `class`, `retryable`, `detail_json`, and `recovery_actions`.
-
-## Runtime DTO Stability
-
-Runtime request DTOs are constructor-led and marked non-exhaustive where they carry public fields:
-`ListingPreparePublishRequest`, `ListingEnqueuePublishRequest`, `SubmitProposalRequest`,
-`ProposeRevisionRequest`, `DecideCandidateRequest`, `CancelTradeRequest`, `ResumeOperationRequest`,
-`GetTradeRequest`, `ListTradesRequest`, `RefreshTradeEvidenceRequest`, `InspectEvidenceRequest`,
-`StorageStatusRequest`, `BackupRequest`, `IntegrityRequest`, `SyncStatusRequest`, and
-`PushOutboxRequest`. Restore uses the same request posture through `RestoreRequest`. Use `new`,
-`parse`, `default`, and `with_*` methods rather than struct literals.
-
-Runtime enums that may gain variants are non-exhaustive. This includes storage, clock,
-target-policy, transport-profile, mutation-state, trade-status, sync-status, relay-auth, and
-push-outbox state or outcome enums.
-
-Runtime receipts and status records expose stable serialized public fields for CLI and local-runtime
-reporting. Future additive reporting must add fields without changing existing serialized field
-names, meanings, or types. Restore archive and receipt records follow that same serialized-field
-stance.
+## 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.
+
+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.
+
+## 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.
+
+## 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 remains `publish = false` until the complete package-realistic
+release qualification and separately authorized publication step. The crate is
+licensed under `MIT OR Apache-2.0`.
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -43,6 +43,64 @@ fn manifest_has_final_identity_and_dependency_boundary() {
}
#[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()
diff --git a/docs/engineering/ci.md b/docs/engineering/ci.md
@@ -11,6 +11,14 @@ The command validates the synchronized release specification, workspace and
package metadata, production dependency paths, the Cargo-resolved package-tier
graph, public API implementation leakage, SDK feature boundaries, publication
freeze, facade scaffold, language contracts, and generated-source freshness.
+
+SDK feature qualification additionally runs no-default, default, each of the
+eleven public features in isolation, the `native` and `full` bundles, and
+all-features. Every lane uses `--all-targets`; strict Clippy mirrors the
+no-default and all-feature endpoints. Package-boundary tests reject any feature
+outside `memory`, `sqlite`, `sync`, `nostr`, `nip46`, `local-signing`,
+`radrootsd`, `geonames`, `knowledge`, `native`, and `full`, and verify that
+optional dependencies are activated through explicit `dep:` entries.
The workflow invokes the same dispatcher with `cargo run --locked` so lockfile
drift fails rather than being resolved implicitly.