lib

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

commit dcfb0da64257c9d61854e367b50f14da661e64cc
parent 432b2ad078bd56ac73733b8610aeaa9b332855ef
Author: triesap <tyson@radroots.org>
Date:   Sat, 11 Apr 2026 15:57:05 +0000

repo: move governance roots to spec and policy

Diffstat:
MAGENTS.md | 4++--
MAGENT_INSTRUCTIONS.md | 8++++----
Dcontract/RCLD.md | 877-------------------------------------------------------------------------------
Dcontract/README.md | 83-------------------------------------------------------------------------------
Mcrates/xtask/src/contract.rs | 249+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/xtask/src/coverage.rs | 38+++++++++++++++++++-------------------
Mcrates/xtask/src/export_ts.rs | 50++++++++++++++++++++++++--------------------------
Mcrates/xtask/src/main.rs | 15++++++++-------
Mnix/common.nix | 3++-
Rcontract/coverage/POLICY.md -> policy/coverage/POLICY.md | 0
Rcontract/coverage/policy.toml -> policy/coverage/policy.toml | 0
Rcontract/coverage/profiles.toml -> policy/coverage/profiles.toml | 0
Aspec/RCLD.md | 877+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aspec/README.md | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rcontract/exports/kotlin.toml -> spec/exports/kotlin.toml | 0
Rcontract/exports/py.toml -> spec/exports/py.toml | 0
Rcontract/exports/swift.toml -> spec/exports/swift.toml | 0
Rcontract/exports/ts.toml -> spec/exports/ts.toml | 0
Rcontract/manifest.toml -> spec/manifest.toml | 0
Rcontract/operations.toml -> spec/operations.toml | 0
Rcontract/replica.toml -> spec/replica.toml | 0
Rcontract/sdk-exports/ts.toml -> spec/sdk-exports/ts.toml | 0
Rcontract/version.toml -> spec/version.toml | 0
23 files changed, 1137 insertions(+), 1150 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -25,7 +25,7 @@ This file exists for compatibility with tools that look for AGENTS.md. Before editing code: -- Read this file, `AGENT_INSTRUCTIONS.md`, `README`, `docs/nix.md`, and `contract/README.md`. +- Read this file, `AGENT_INSTRUCTIONS.md`, `README`, `docs/nix.md`, and `spec/README.md`. - Enter the canonical environment with `nix develop` or `direnv allow` before targeted cargo work. - Discover commands from checked-in repo surfaces; do not invent ad hoc workflows. - Read the current implementation and nearby tests before designing a change. @@ -57,7 +57,7 @@ Before editing code: ## 6. Contract and release discipline -- `contract/`, `conformance/`, and `crates/xtask` are authoritative for public SDK contract, export, and release governance. +- `spec/`, `conformance/`, and `crates/xtask` are authoritative for public SDK contract, export, and release governance. - Behavior changes that affect public surfaces must update the relevant contract metadata, conformance vectors, export rules, or validation flows in the same change. - Keep pure flake checks and repo-aware command apps aligned with the documented Nix command map. diff --git a/AGENT_INSTRUCTIONS.md b/AGENT_INSTRUCTIONS.md @@ -40,7 +40,7 @@ Before editing code: - Read `AGENTS.md`. - Read this file. -- Read `README`, `docs/nix.md`, and `contract/README.md` when the change touches workflow, exports, or public surfaces. +- Read `README`, `docs/nix.md`, and `spec/README.md` when the change touches workflow, exports, or public surfaces. - Read the relevant crate manifest, implementation files, and nearby tests before proposing a new structure. - Check `git status --short`. @@ -63,7 +63,7 @@ Use this mental model: - `crates/` - library crates and workspace tooling crates - keep domain logic inside the correct crate rather than spreading it across the workspace -- `contract/` +- `spec/` - public SDK contract metadata, export policy, release policy, and coverage governance - `conformance/` - cross-language and cross-surface vector expectations @@ -74,7 +74,7 @@ Use this mental model: - `scripts/` - repo-owned automation used by canonical lanes -Do not duplicate contract knowledge between crates when `contract/`, `conformance/`, or `xtask` already owns it. +Do not duplicate contract knowledge between crates when `spec/`, `conformance/`, or `xtask` already owns it. ## 5. Rust engineering standards @@ -134,7 +134,7 @@ Do not duplicate contract knowledge between crates when `contract/`, `conformanc ## 6. Contract, conformance, and release workflow -`contract/`, `conformance/`, and `crates/xtask` are first-class parts of the product surface, not secondary metadata. +`spec/`, `conformance/`, and `crates/xtask` are first-class parts of the product surface, not secondary metadata. When a change affects exported models, transforms, identifiers, or public runtime expectations: diff --git a/contract/RCLD.md b/contract/RCLD.md @@ -1,877 +0,0 @@ -# Radroots Cross-Language SDK Contract Design - -Status: approved direction, design artifact - -Scope: public Radroots SDK contract for external language SDKs derived from the Rust workspace in this repository - -Canonical source: Rust remains the canonical implementation and conformance source for public contract behavior - -## Purpose - -This document defines the approved operation-first design for the Radroots cross-language SDK contract. - -It replaces the crate-first mental model currently expressed in `contract/manifest.toml` with a public contract shaped around external integration tasks: - -- produce Radroots-compliant Nostr events -- parse Radroots-compliant Nostr events -- validate Radroots-compliant contract behavior -- preserve deterministic cross-language behavior for supported operations - -This document does not require the Rust workspace to stop using crate boundaries internally. Crates remain implementation and provenance boundaries inside Rust. Operations become the public SDK boundary. - -## Problem - -The current repository expresses SDK surface primarily in terms of Rust crates: - -- `surface.model_crates` -- `surface.algorithm_crates` -- `surface.wasm_crates` -- language export manifests keyed by Rust crate name -- `xtask` validation and export logic that assume crate-to-package mapping - -That framing is not aligned with the needs of third-party integrators. Integrators do not want a mirror of the Rust workspace. They want a small, stable, idiomatic SDK that helps them publish and read Radroots-compliant Nostr events. - -The codebase already contains the correct technical boundary: - -- event model types in `radroots_events`, `radroots_trade`, `radroots_identity`, and supporting model crates -- deterministic builders and parsers in `radroots_events_codec` -- shared unsigned event primitives in `radroots_events_codec::wire` -- optional wasm packaging for deterministic helper logic - -The contract must move upward from crate inventory to public operations. - -## Decisions Ratified - -The following decisions are approved and are treated as default design constraints: - -- external SDKs optimize first for third-party app integrations, not for full Radroots internal app parity -- publishing is the first-class use case -- reading and validation are supported for the same Tier 1 domains, but remain secondary to publishing -- Tier 1 domains are `profile`, `farm`, `listing`, and `trade` -- the public contract unit is an operation, not a crate -- networking and signing remain native to each target language -- TypeScript is the first reference SDK for the new contract -- Python follows after TypeScript proves the operation model -- Rust crate names are not part of the public SDK mental model -- migration should be additive first and support old and new manifest shapes during transition - -## Goals - -- define a stable public SDK contract in terms of operations -- preserve Rust as the canonical behavioral implementation -- support idiomatic language SDKs without requiring full Rust API parity -- keep transport, relay IO, and signing runtime-native -- make deterministic encode, parse, normalize, and validate behavior conformance-testable -- provide a migration path from the current crate-keyed contract and export system - -## Non-Goals - -- exporting every Rust function to every language -- standardizing one shared Nostr client implementation across languages -- exposing Radroots app-internal marketplace, replica, moderation, or backoffice surfaces as public SDK APIs -- introducing a flag-day rewrite of the entire contract and export toolchain - -## External Audience - -The public SDK contract is designed for: - -- third-party apps publishing Radroots-compliant profiles, farms, listings, and trade events -- apps that need to parse or validate those supported event families -- language SDK maintainers implementing contract-compliant APIs in TypeScript, Python, Swift, and Kotlin - -The public SDK contract is not designed for: - -- exposing Radroots internal admin flows -- exposing internal replica storage contracts -- exposing internal moderation and backoffice read models - -## Public Contract Principles - -1. Operations are public. Crates are internal. -2. Inputs, outputs, and errors are explicit. -3. Deterministic behavior is contract material. -4. Cross-language conformance is mandatory for approved operations. -5. Runtime choices such as relay transport and signer integration remain language-native. -6. Public surface must be narrower than the full Rust workspace. -7. Internal app-specific projections are excluded unless explicitly promoted. - -## Public Surface Taxonomy - -The public SDK contract has four surface classes: - -### 1. Operations - -Task-oriented public entry points for supported domains. - -Examples: - -- `profile.build_draft` -- `farm.build_draft` -- `listing.build_tags` -- `listing.build_draft` -- `listing.parse_event` -- `trade.build_envelope_draft` -- `trade.parse_envelope` -- `trade.parse_listing_address` -- `trade.validate_listing_event` - -### 2. Shared Types - -Public cross-operation types required for operation inputs and outputs. - -Examples: - -- `WireEventParts` -- `UnsignedEventDraft` -- `RadrootsNostrEvent` -- `RadrootsNostrEventRef` -- `RadrootsTradeListingAddress` - -### 3. Shared Errors - -Public error categories and domain-specific parse and validation errors that languages must preserve semantically. - -Examples: - -- event encode errors -- listing parse errors -- trade envelope parse errors -- listing validation errors - -### 4. Implementation Provenance - -Rust crate and wasm provenance used by maintainers and tooling, but not treated as the public contract unit. - -Examples: - -- operation implemented in `radroots_events_codec` -- type defined in `radroots_events` -- deterministic helper exposed via `radroots_events_codec_wasm` - -## Tier 1 Domains And Operations - -The initial approved public domains are `profile`, `farm`, `listing`, and `trade`. - -The following operations form the recommended Tier 1 surface. - -### Profile - -#### `profile.build_draft` - -Purpose: produce an unsigned Nostr event draft for a Radroots profile event - -Rust implementation sources: - -- `crates/events_codec/src/profile/encode.rs` - -Input: - -- `RadrootsProfile` -- optional `RadrootsProfileType` - -Output: - -- `WireEventParts` -- optional `UnsignedEventDraft` helper via shared draft adapter - -Determinism: - -- required - -Runtime ownership: - -- signing: native -- transport: native - -### Farm - -#### `farm.build_draft` - -Purpose: produce an unsigned Nostr event draft for a farm event - -Rust implementation sources: - -- `crates/events_codec/src/farm/encode.rs` - -Input: - -- `RadrootsFarm` - -Output: - -- `WireEventParts` - -Determinism: - -- required - -Runtime ownership: - -- signing: native -- transport: native - -### Listing - -#### `listing.build_tags` - -Purpose: produce canonical listing tags without creating a full unsigned event - -Rust implementation sources: - -- `crates/events_codec/src/listing/encode.rs` -- `crates/events_codec/src/listing/tags.rs` - -Input: - -- `RadrootsListing` - -Output: - -- `Vec<Vec<String>>` - -Determinism: - -- required - -#### `listing.build_draft` - -Purpose: produce an unsigned listing event contract from a listing model - -Rust implementation sources: - -- `crates/events_codec/src/listing/encode.rs` -- `crates/events_codec/src/wire.rs` - -Input: - -- `RadrootsListing` -- optional listing kind override when explicitly allowed - -Output: - -- `WireEventParts` -- optionally adapted to `UnsignedEventDraft` - -Determinism: - -- required - -#### `listing.parse_event` - -Purpose: parse a listing event into the canonical listing model - -Rust implementation sources: - -- `crates/trade/src/listing/codec.rs` -- `crates/events_codec/src/listing/decode.rs` - -Input: - -- `RadrootsNostrEvent` - -Output: - -- `RadrootsListing` - -Determinism: - -- required - -### Trade - -#### `trade.build_envelope_draft` - -Purpose: produce an unsigned trade envelope event from typed trade payload input - -Rust implementation sources: - -- `crates/events_codec/src/trade/encode.rs` - -Input: - -- recipient pubkey -- trade message type -- listing address -- optional order id -- optional listing event pointer -- optional root event id -- optional previous event id -- typed trade payload - -Output: - -- `WireEventParts` - -Determinism: - -- required - -#### `trade.parse_envelope` - -Purpose: parse a trade event into a typed trade envelope - -Rust implementation sources: - -- `crates/events_codec/src/trade/decode.rs` - -Input: - -- `RadrootsNostrEvent` - -Output: - -- typed `RadrootsTradeEnvelope<T>` - -Determinism: - -- required - -#### `trade.parse_listing_address` - -Purpose: parse and validate the canonical listing address used by trade flows - -Rust implementation sources: - -- `crates/events_codec/src/trade/decode.rs` - -Input: - -- listing address string - -Output: - -- `RadrootsTradeListingAddress` - -Determinism: - -- required - -#### `trade.validate_listing_event` - -Purpose: validate that an event meets Radroots listing contract expectations for trade workflows - -Rust implementation sources: - -- `crates/trade/src/listing/validation.rs` - -Input: - -- `RadrootsNostrEvent` -- optional fetched dependencies if the validation path requires them - -Output: - -- validation result structure or domain validation error - -Determinism: - -- required for local validation logic -- explicitly scoped where external dependency fetch is involved - -## Shared Types - -The public contract should explicitly enumerate a minimal shared type set. - -Recommended Tier 1 shared types: - -- `WireEventParts` -- `UnsignedEventDraft` -- `RadrootsNostrEvent` -- `RadrootsNostrEventRef` -- `RadrootsNostrEventPtr` -- `RadrootsTradeListingAddress` -- public model types required by Tier 1 operations: -- `RadrootsProfile` -- `RadrootsFarm` -- `RadrootsListing` -- trade payload and envelope types required by approved trade operations - -`UnsignedEventDraft` should be a public contract alias or wrapper over the current `EventDraft` concept in `crates/events_codec/src/wire.rs`. The public naming should emphasize unsigned event construction rather than internal adapter mechanics. - -## Shared Errors - -The contract should distinguish between: - -- semantic error categories that are part of the public API -- internal implementation error types that can be mapped privately - -Recommended public error classes: - -- `encode_error` -- `parse_error` -- `validation_error` -- `address_error` - -Recommended public semantic guarantees: - -- required-field failures remain distinguishable -- invalid-kind failures remain distinguishable -- invalid-json failures remain distinguishable where applicable -- domain-specific parse mismatches remain distinguishable for listing and trade operations - -Language SDKs may translate exact type names, but they must preserve error meaning and conformance behavior. - -## Explicit Exclusions From The Public SDK - -The following surfaces remain internal unless separately promoted: - -- `radroots_replica_*` surfaces -- backoffice overlays -- marketplace read models and projections -- internal moderation models -- full `radroots_nostr` client runtime -- internal runtime management contracts - -This exclusion is important because the Rust workspace contains valuable internal app surfaces that are not appropriate to freeze as external SDK contract. - -## Runtime Ownership Model - -The cross-language contract owns: - -- deterministic model encode behavior -- parse behavior -- validation behavior -- canonical tags and content construction -- canonical address and pointer parsing - -The language runtime owns: - -- relay transport -- signer integration -- key management -- subscription lifecycle -- connection policies -- local storage and caching choices - -This means SDKs should primarily produce and consume unsigned or already-signed event shapes rather than wrapping one shared transport stack. - -## Package Strategy - -### Public Package Strategy - -The public package strategy should be operation-first and ergonomic. - -Recommended TypeScript package strategy: - -- one main package, for example `@radroots/sdk` -- one optional deterministic helper package or embedded asset for wasm-backed helpers - -Recommended Python package strategy: - -- one main package, for example `radroots_sdk` -- optional implementation-private native or wasm helper assets - -Recommended Swift and Kotlin strategy: - -- one main package or module namespace per language -- helper implementation details remain private unless explicitly useful - -### What Not To Ship - -Do not use crate-mirror packages as the primary public shape: - -- `@radroots/core` -- `@radroots/types` -- `@radroots/events` -- `@radroots/trade` -- `@radroots/identity` - -Those may remain transitional or internal build artifacts, but they should not be the product definition for external integrators. - -## Contract Schema v2 - -The new contract should be additive first. The repository should support both: - -- the existing crate-keyed contract metadata -- a new operation-keyed contract schema - -The operation-keyed schema should become the public source of truth. Crate metadata should become provenance or migration-only data. - -### Recommended Top-Level Manifest Shape - -```toml -[contract] -name = "radroots-sdk-contract" -version = "0.2.0-alpha.1" -source = "rust" -stability = "draft" - -[public] -domains = ["profile", "farm", "listing", "trade"] - -[shared_types] -public = [ - "WireEventParts", - "UnsignedEventDraft", - "RadrootsNostrEvent", - "RadrootsNostrEventRef", - "RadrootsNostrEventPtr", - "RadrootsTradeListingAddress", - "RadrootsProfile", - "RadrootsFarm", - "RadrootsListing", -] - -[errors] -classes = ["encode_error", "parse_error", "validation_error", "address_error"] - -[operations.profile_build_draft] -domain = "profile" -id = "profile.build_draft" -stability = "beta" -inputs = ["RadrootsProfile", "RadrootsProfileType?"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.profile_build_draft.implementation] -rust_modules = ["crates/events_codec/src/profile/encode.rs"] -rust_types = ["radroots_events::profile::RadrootsProfile"] - -[operations.profile_build_draft.conformance] -vector = "conformance/vectors/profile/build_draft.v1.json" - -[operations.listing_build_draft] -domain = "listing" -id = "listing.build_draft" -stability = "beta" -inputs = ["RadrootsListing"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.listing_build_draft.implementation] -rust_modules = [ - "crates/events_codec/src/listing/encode.rs", - "crates/events_codec/src/listing/tags.rs", - "crates/events_codec/src/wire.rs", -] - -[operations.listing_build_draft.conformance] -vector = "conformance/vectors/listing/build_draft.v1.json" -``` - -### Provenance Section - -During migration, crate provenance should remain available: - -```toml -[implementation_provenance] -model_crates = [ - "radroots_core", - "radroots_types", - "radroots_events", - "radroots_trade", - "radroots_identity", -] -algorithm_crates = ["radroots_events_codec"] -wasm_crates = ["radroots_events_codec_wasm"] -``` - -This keeps current workspace knowledge available without making it the public contract unit. - -## Language Export Manifest v2 - -Language export manifests should stop mapping crate names to packages as the primary concept. - -Instead they should answer: - -- which operations are supported in the language -- where those operations are exposed -- how deterministic logic is implemented -- which shared types and error classes are public - -### Recommended TypeScript Export Shape - -```toml -[language] -id = "ts" -repository = "sdk-typescript" - -[sdk] -package = "@radroots/sdk" -module_format = "esm" -deterministic_codec = "wasm" -signing = "native" -networking = "native" - -[operations] -"profile.build_draft" = "profile.buildDraft" -"farm.build_draft" = "farm.buildDraft" -"listing.build_tags" = "listing.buildTags" -"listing.build_draft" = "listing.buildDraft" -"listing.parse_event" = "listing.parseEvent" -"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" -"trade.parse_envelope" = "trade.parseEnvelope" -"trade.parse_listing_address" = "trade.parseListingAddress" -"trade.validate_listing_event" = "trade.validateListingEvent" - -[shared_types] -"WireEventParts" = "WireEventParts" -"UnsignedEventDraft" = "UnsignedEventDraft" -"RadrootsNostrEvent" = "RadrootsNostrEvent" -"RadrootsTradeListingAddress" = "TradeListingAddress" - -[artifacts] -models_dir = "src/generated" -runtime_dir = "src/runtime" -wasm_dist_dir = "dist" -manifest_file = "export-manifest.json" -``` - -Equivalent manifests for Python, Swift, and Kotlin should use their own naming conventions. - -## Export Manifest Output - -`xtask` should write an export manifest that includes operation coverage metadata, not only file hashes. - -Recommended structure: - -```json -{ - "language": "ts", - "sdk_package": "@radroots/sdk", - "operations": [ - { - "id": "listing.build_draft", - "symbol": "listing.buildDraft", - "deterministic_codec": "wasm" - } - ], - "files": [ - { - "path": "src/generated/listing.ts", - "sha256": "..." - } - ] -} -``` - -## Conformance Model - -Conformance becomes the real multi-language product gate for the public contract. - -### Rules - -- every public operation must have at least one conformance vector suite -- deterministic operations require positive and negative vectors -- parse operations require round-trip or semantic equivalence vectors where applicable -- error behavior that is part of the contract must be vectorized -- language SDKs must pass conformance without local overrides - -### Recommended Vector Layout - -```text -conformance/ - vectors/ - profile/ - build_draft.v1.json - farm/ - build_draft.v1.json - listing/ - build_tags.v1.json - build_draft.v1.json - parse_event.v1.json - trade/ - build_envelope_draft.v1.json - parse_envelope.v1.json - parse_listing_address.v1.json - validate_listing_event.v1.json -``` - -### Minimum Vector Coverage - -For each operation: - -- one minimal valid case -- one rich valid case -- one canonical normalization case if normalization exists -- one required-field failure case -- one invalid-format or invalid-kind failure case where applicable - -## Versioning Policy v2 - -The contract version policy must shift from exported crate surface to operation semantics. - -### Major Version Triggers - -- remove a public operation -- change required operation input shape -- change output shape incompatibly -- change deterministic operation behavior incompatibly -- collapse or remove a public error distinction - -### Minor Version Triggers - -- add a public operation -- add optional input or output fields -- add a new public shared type -- add new conformance vectors that extend supported behavior without breaking old behavior - -### Patch Version Triggers - -- documentation fixes -- packaging fixes without behavior changes -- non-behavioral codegen fixes -- bug fixes that do not change the contract shape and do not invalidate existing conforming clients - -## Rust Implementation Strategy - -The Rust workspace should add a curated facade for approved public operations. - -Recommended shape: - -- add a new crate or dedicated public module namespace, for example `radroots_sdk_contract` -- re-export only approved operations and approved shared types -- keep direct crate internals available for Rust maintainers but do not treat them as the cross-language contract by default - -This facade should: - -- define public operation names -- define any public wrapper naming such as `UnsignedEventDraft` -- centralize contract documentation and source references -- make it easier for generators and future language bindings to target one approved surface - -## `xtask` Migration Strategy - -### Current State - -`xtask` currently: - -- parses a crate-keyed surface -- validates crate-keyed export coverage -- exports TypeScript by iterating crate-to-package mappings -- has tests that assert TypeScript export coverage matches model plus wasm crates - -### Required Changes - -1. add new manifest parsing structs for operation-based contract metadata -2. support dual parsing during migration -3. introduce validation for: -- non-empty public domain list -- unique operation ids -- known shared type references -- conformance vector presence for each public operation -- language export manifests mapping approved operations -4. replace crate-coverage assertions with operation-coverage assertions -5. update export manifest generation to report operation coverage -6. keep current crate provenance checks only as implementation validation - -### Recommended `xtask` Command Evolution - -Keep existing commands temporarily: - -- `sdk export-ts` -- `sdk validate` - -Add new migration-aware behavior behind the same commands: - -- `sdk validate` validates both old and new contract surfaces -- `sdk export-ts` assembles an operation-first TypeScript SDK package from approved operations - -Optional additive commands: - -- `sdk validate-operations` -- `sdk export-ts-sdk` -- `sdk conformance check --language <id>` - -## Language SDK Strategy - -### TypeScript - -TypeScript is the reference external SDK. - -Implementation recommendation: - -- keep Rust-driven generated models where useful -- use wasm for deterministic codec helpers where beneficial -- handwrite the final ergonomic operation surface -- expose one main SDK package - -### Python - -Python follows after TypeScript proves the operation model. - -Implementation recommendation: - -- do not mirror Rust crates directly -- either implement pure-Python adapters on top of contract artifacts or bind a very small deterministic Rust core -- keep packaging centered on one main SDK package - -### Swift And Kotlin - -Swift and Kotlin should wait until the operation contract is stable and conformance coverage is broader. - -Implementation recommendation: - -- keep the same operation contract -- keep runtime integration native -- only introduce shared native bindings for a narrow deterministic core if the maintenance tradeoff is justified - -## Migration Plan - -### Phase 0: Ratify Design - -- adopt this design as the operation-first target -- treat current crate-keyed metadata as migration-only - -### Phase 1: Add New Contract Metadata - -- add operation-based metadata to the contract directory -- keep crate provenance metadata for existing tooling -- do not remove current manifest shape yet - -### Phase 2: Add Curated Rust Facade - -- introduce a public Rust contract facade -- map approved operations to existing implementation functions -- exclude projections, overlays, replica, and backoffice surfaces - -### Phase 3: Expand Conformance - -- add operation-based vectors for Tier 1 operations -- make vector coverage a release-blocking validation rule - -### Phase 4: Migrate TypeScript Export - -- shift `xtask export-ts` to operation-first packaging -- ship one main external TypeScript SDK package -- keep transitional generated artifacts internal if necessary - -### Phase 5: Introduce Python - -- implement Python export or packaging against the same contract and vector set - -### Phase 6: Remove Crate-First Public Assumptions - -- remove tests and validation that require package coverage by Rust crate name -- keep crate provenance only as internal documentation and maintenance metadata - -## Acceptance Criteria - -This design is implemented successfully when: - -- the public contract manifest declares operations, shared types, and error classes -- `xtask validate` enforces operation coverage and conformance presence -- the curated Rust facade exposes only approved public operations -- the TypeScript SDK ships an operation-first public API -- conformance vectors exist for every Tier 1 operation -- public docs describe the SDK in terms of operations, not Rust crates - -## Immediate Next Workstreams - -1. introduce contract schema v2 files and parser structs -2. define the exact Tier 1 operation ids in machine-readable metadata -3. add a Rust public facade crate or module for those operations -4. author conformance vectors for every Tier 1 operation -5. rewrite `xtask` validator assumptions -6. redesign the TypeScript export manifest and package assembly -7. draft the first external TypeScript SDK surface around the approved operations - -## Repository Notes - -This document intentionally does not modify the current crate-keyed contract files in place. The repository currently contains user edits in several existing files, including `contract/README`. The recommended implementation path is to add the new operation-first contract artifacts alongside the current files first, then migrate validation and export tooling incrementally. diff --git a/contract/README.md b/contract/README.md @@ -1,83 +0,0 @@ -# radroots-sdk-contract - -Core contract for the Rad Roots cross-language SDK. - -## Purpose - -This directory defines the Rad Roots SDK contract used to align Rust, TypeScript, Python, Swift, and Kotlin surfaces. -It defines the public interoperability boundary for external integrators, keeps Rust as the canonical source for exported models and transforms, and enforces deterministic, machine-verifiable governance for contract changes and releases. - -## Contract Surface - -Contract metadata is defined in `contract/manifest.toml` and currently includes: - -- model crates: `radroots_core`, `radroots_types`, `radroots_events`, `radroots_trade`, `radroots_identity` -- algorithm crate: `radroots_events_codec` -- wasm crate: `radroots_events_codec_wasm` - -Public SDK exports are intentionally narrower than the full Rust workspace. - -## Export Targets - -Language export mappings and artifact layout rules are defined under `contract/exports/`: - -- `contract/exports/ts.toml` -- `contract/exports/py.toml` -- `contract/exports/swift.toml` -- `contract/exports/kotlin.toml` - -Each export target defines package naming, artifact directories, and runtime expectations. - -## Internal Replica Contract - -Offline-first replica crates are internal contract surfaces and are not public SDK exports. -Replica contract metadata is defined in `contract/replica.toml`. - -Internal replica crate family: - -- `radroots_replica_db_schema` -- `radroots_replica_db` -- `radroots_replica_db_wasm` -- `radroots_replica_sync` -- `radroots_replica_sync_wasm` - -## Governance - -Versioning and compatibility policy is defined in `contract/version.toml`. -Contract evolution is semver-governed and requires conformance updates, export manifest validation, and release notes. - -Repository guards also enforce: - -- deterministic export requirements -- strict no-legacy identifier policy for replica surfaces -- no committed generated TypeScript artifacts in repo export directories (`target/ts-rs/` and `target/sdk-export-ci/`) - -## Coverage Policy - -Coverage governance is defined under `contract/coverage/`: - -- machine-readable policy: `contract/coverage/policy.toml` -- human policy notes: `contract/coverage/POLICY.md` -- per-crate profiles: `contract/coverage/profiles.toml` - -Required Rust crates are gated at `100/100/100/100` (exec lines, functions, branches, regions), with branch records required. - -## Release Policy - -Release crate classification and publish order are defined in the owning monorepo at -`contracts/release/mounted-rust-crates/publish-policy.toml`. -Operator workflow is root-owned and documented in: - -- `contracts/release/mounted-rust-crates/runbook.md` -- `contracts/release/mounted-rust-crates/checklist.md` - -Primary commands: - -- `cargo run -q -p xtask -- sdk validate` -- `cargo run -q -p xtask -- sdk release preflight` -- `./scripts/ci/release_preflight.sh` -- `scripts/release/rr-rs-preflight.sh <plan-id> [crate-list]` from the owning monorepo - -## License - -Licensed under AGPL-3.0. See LICENSE. diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -336,7 +336,7 @@ fn parse_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { } fn contract_root(workspace_root: &Path) -> PathBuf { - workspace_root.join("contract") + workspace_root.join("spec") } fn base_contract_version(version: &str) -> &str { @@ -409,16 +409,24 @@ fn workspace_package_manifests(workspace_root: &Path) -> Result<BTreeMap<String, fn load_coverage_policy( contract_root: &Path, ) -> Result<crate::coverage::CoveragePolicyFile, String> { - read_coverage_policy(&contract_root.join("coverage").join("policy.toml")) + read_coverage_policy(&coverage_root(contract_root).join("policy.toml")) } -fn legacy_release_contract_path(contract_root: &Path) -> PathBuf { - contract_root.join("release").join("publish-set.toml") +fn coverage_root(contract_root: &Path) -> PathBuf { + contract_root + .parent() + .unwrap_or(contract_root) + .join("policy") + .join("coverage") +} + +#[cfg_attr(not(test), allow(dead_code))] +fn root_release_policy_path(workspace_root: &Path) -> PathBuf { + workspace_root.join(ROOT_RELEASE_POLICY_RELATIVE) } fn resolve_release_contract_path_with_override( workspace_root: &Path, - contract_root: &Path, release_policy_override: Option<PathBuf>, ) -> Result<Option<PathBuf>, String> { if let Some(path) = release_policy_override { @@ -438,21 +446,12 @@ fn resolve_release_contract_path_with_override( } } - let legacy = legacy_release_contract_path(contract_root); - if legacy.is_file() { - return Ok(Some(legacy)); - } - Ok(None) } -fn resolve_release_contract_path( - workspace_root: &Path, - contract_root: &Path, -) -> Result<Option<PathBuf>, String> { +fn resolve_release_contract_path(workspace_root: &Path) -> Result<Option<PathBuf>, String> { resolve_release_contract_path_with_override( workspace_root, - contract_root, env::var_os(RELEASE_POLICY_ENV).map(PathBuf::from), ) } @@ -470,21 +469,17 @@ fn load_release_contract( fn load_release_contract_with_override( workspace_root: &Path, - contract_root: &Path, + _contract_root: &Path, release_policy_override: Option<PathBuf>, ) -> Result<ReleaseContractFile, String> { - let path = resolve_release_contract_path_with_override( - workspace_root, - contract_root, - release_policy_override, - )? - .ok_or_else(|| { - format!( - "release publish policy not found; expected {} or legacy {}", - ROOT_RELEASE_POLICY_RELATIVE, - legacy_release_contract_path(contract_root).display() - ) - })?; + let path = + resolve_release_contract_path_with_override(workspace_root, release_policy_override)? + .ok_or_else(|| { + format!( + "release publish policy not found; expected {}", + ROOT_RELEASE_POLICY_RELATIVE + ) + })?; parse_toml::<ReleaseContractFile>(&path) } @@ -1499,13 +1494,9 @@ fn validate_contract_bundle_with_release_policy_override( } validate_core_unit_dimension_variant_order(workspace_root)?; validate_coverage_policy_parity(workspace_root, &bundle.root)?; - if resolve_release_contract_path_with_override( - workspace_root, - &bundle.root, - release_policy_override.clone(), - ) - .expect("validated release contract path resolution should not fail") - .is_some() + if resolve_release_contract_path_with_override(workspace_root, release_policy_override.clone()) + .expect("validated release contract path resolution should not fail") + .is_some() { validate_release_publish_policy_with_override( workspace_root, @@ -1917,7 +1908,7 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { } validate_core_unit_dimension_variant_order(workspace_root)?; validate_coverage_policy_parity(workspace_root, &bundle.root)?; - if resolve_release_contract_path(workspace_root, &bundle.root) + if resolve_release_contract_path(workspace_root) .expect("validated release contract path resolution should not fail") .is_some() { @@ -2014,7 +2005,7 @@ publish = false ); write_file( - &root.join("contract").join("manifest.toml"), + &root.join("spec").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -2032,7 +2023,7 @@ require_conformance_vectors = true "#, ); write_file( - &root.join("contract").join("version.toml"), + &root.join("spec").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -2049,7 +2040,7 @@ requires_release_notes = true "#, ); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -2065,7 +2056,7 @@ manifest_file = "export-manifest.json" "#, ); write_file( - &root.join("contract").join("coverage").join("policy.toml"), + &root.join("policy").join("coverage").join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2078,10 +2069,7 @@ crates = ["radroots_a", "radroots_b"] "#, ); write_file( - &root - .join("contract") - .join("release") - .join("publish-set.toml"), + &root_release_policy_path(&root), r#"[release] version = "1.0.0" @@ -2107,7 +2095,7 @@ crates = ["radroots_a"] fn add_operation_contract_files(root: &Path) { write_file( - &root.join("contract").join("operations.toml"), + &root.join("spec").join("operations.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -2175,7 +2163,7 @@ vector = "conformance/vectors/listing/build_draft.v1.json" "#, ); write_file( - &root.join("contract").join("sdk-exports").join("ts.toml"), + &root.join("spec").join("sdk-exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -2272,7 +2260,7 @@ publish = false ); } write_file( - &root.join("contract").join("coverage").join("policy.toml"), + &root.join("policy").join("coverage").join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2291,11 +2279,7 @@ crates = ["radroots_a", "radroots_b", "radroots_c", "radroots_d", "radroots_e"] .join("coverage-refresh.tsv"), "crate\tstatus\texec\tfunc\tbranch\tregion\treport\nradroots_a\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots_b\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots_c\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots_d\tpass\t100.0\t100.0\t100.0\t100.0\tfile\nradroots_e\tpass\t100.0\t100.0\t100.0\t100.0\tfile\n", ); - let _ = fs::remove_file( - root.join("contract") - .join("release") - .join("publish-set.toml"), - ); + let _ = fs::remove_file(root_release_policy_path(&root)); } #[test] @@ -2411,7 +2395,7 @@ pub enum RadrootsCoreUnitDimension { .expect("workspace crates") .into_iter() .collect::<BTreeSet<_>>(); - let policy = load_coverage_policy(&root.join("contract")).expect("coverage policy"); + let policy = load_coverage_policy(&root.join("spec")).expect("coverage policy"); let required_names = policy .required_crates() .expect("required crates") @@ -2423,7 +2407,7 @@ pub enum RadrootsCoreUnitDimension { #[test] fn coverage_required_crates_match_policy_required_status() { let root = workspace_root(); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); let policy = load_coverage_policy(&contract_root).expect("coverage policy"); let required = CoverageRequiredFile { required: CoverageRequiredSection { @@ -2453,9 +2437,10 @@ pub enum RadrootsCoreUnitDimension { let duplicate_root = create_synthetic_workspace("load_coverage_required_duplicate_required"); - let contract_root = duplicate_root.join("contract"); + let contract_root = duplicate_root.join("spec"); + let coverage_root = coverage_root(&contract_root); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", ); let duplicate_err = @@ -2727,23 +2712,15 @@ readme = { workspace = true } ); let root = create_synthetic_workspace("release_contract_env_override"); - let contract_root = root.join("contract"); - let policy_path = contract_root.join("release").join("publish-set.toml"); - let resolved = resolve_release_contract_path_with_override( - &root, - &contract_root, - Some(policy_path.clone()), - ) - .expect("existing override policy should resolve"); + let policy_path = root_release_policy_path(&root); + let resolved = + resolve_release_contract_path_with_override(&root, Some(policy_path.clone())) + .expect("existing override policy should resolve"); assert_eq!(resolved, Some(policy_path)); let missing_policy = root.join("missing-release-policy.toml"); - let err = resolve_release_contract_path_with_override( - &root, - &contract_root, - Some(missing_policy.clone()), - ) - .expect_err("missing env policy should fail"); + let err = resolve_release_contract_path_with_override(&root, Some(missing_policy.clone())) + .expect_err("missing env policy should fail"); assert!(err.contains(RELEASE_POLICY_ENV)); assert!(err.contains("missing release policy file")); assert!(err.contains(&missing_policy.display().to_string())); @@ -2880,10 +2857,11 @@ members = ["crates/a", "crates/b"] #[test] fn coverage_policy_parity_reports_contract_errors() { let root = create_synthetic_workspace("coverage_policy_errors"); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); + let coverage_root = coverage_root(&contract_root); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2900,7 +2878,7 @@ crates = [] assert!(empty_required.contains("required crates list must not be empty")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 99.0 fail_under_functions = 100.0 @@ -2917,7 +2895,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_gate.contains("100/100/100/100")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 99.0 @@ -2934,7 +2912,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_functions.contains("100/100/100/100")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2951,7 +2929,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_regions.contains("100/100/100/100")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2968,7 +2946,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_branches.contains("100/100/100/100")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -2985,7 +2963,7 @@ crates = ["radroots_a", "radroots_a"] assert!(duplicate_required.contains("duplicate crate")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -3002,7 +2980,7 @@ crates = ["radroots_a", "radroots_b"] assert!(branches_optional.contains("required branches")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -3019,7 +2997,7 @@ crates = ["radroots_a"] assert!(missing_workspace.contains("missing workspace crates")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -3041,10 +3019,11 @@ crates = ["unknown"] #[test] fn release_publish_policy_reports_contract_errors() { let root = create_synthetic_workspace("release_policy_errors"); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); + let release_policy_path = root_release_policy_path(&root); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "" @@ -3063,7 +3042,7 @@ crates = ["radroots_a"] assert!(empty_version.contains("must not be empty")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "2.0.0" @@ -3082,7 +3061,7 @@ crates = ["radroots_a"] assert!(version_mismatch.contains("must match contract version")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3101,7 +3080,7 @@ crates = ["radroots_a"] assert!(overlap.contains("overlap is not allowed")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3120,7 +3099,7 @@ crates = ["radroots_a"] assert!(missing_workspace.contains("missing workspace crates")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3139,7 +3118,7 @@ crates = [] assert!(missing_publish_order.contains("missing publish crates")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3188,7 +3167,7 @@ readme = "README" "#, ); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3207,7 +3186,7 @@ crates = ["radroots_a", "radroots_b"] assert!(dependency_order.contains("must place dependency")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3243,7 +3222,7 @@ publish = false "#, ); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3669,7 +3648,9 @@ publish = false #[test] fn coverage_release_and_bundle_loaders_report_parse_and_read_errors() { let root = create_synthetic_workspace("coverage_release_loader_errors"); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); + let coverage_root = coverage_root(&contract_root); + let release_policy_path = root_release_policy_path(&root); let missing_workspace = temp_root("coverage_missing_workspace_manifest"); let policy_workspace_err = @@ -3678,12 +3659,12 @@ publish = false assert!(policy_workspace_err.contains("Cargo.toml")); let _ = fs::remove_dir_all(&missing_workspace); - let _ = fs::remove_file(contract_root.join("coverage").join("policy.toml")); + let _ = fs::remove_file(coverage_root.join("policy.toml")); let policy_load_err = validate_coverage_policy_parity(&root, &contract_root) .expect_err("coverage policy read error"); assert!(policy_load_err.contains("policy.toml")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -3697,19 +3678,34 @@ crates = ["radroots_a", "radroots_b"] ); let missing_release = temp_root("release_missing_workspace_manifest"); + write_root_release_policy( + &missing_release, + r#"[release] +version = "1.0.0" + +[publish] +crates = ["radroots_a"] + +[internal] +crates = ["radroots_b"] + +[publish_order] +crates = ["radroots_a"] +"#, + ); let release_workspace_err = validate_release_publish_policy(&missing_release, &contract_root, "1.0.0") .expect_err("release workspace read error"); assert!(release_workspace_err.contains("Cargo.toml")); let _ = fs::remove_dir_all(&missing_release); - let _ = fs::remove_file(contract_root.join("release").join("publish-set.toml")); + let _ = fs::remove_file(&release_policy_path); let release_load_err = validate_release_publish_policy(&root, &contract_root, "1.0.0") .expect_err("release contract read error"); - assert!(release_load_err.contains("publish-set.toml")); + assert!(release_load_err.contains(ROOT_RELEASE_POLICY_RELATIVE)); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3728,7 +3724,7 @@ crates = ["radroots_a"] assert!(duplicate_publish.contains("publish.crates has duplicate crate")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3747,7 +3743,7 @@ crates = ["radroots_a"] assert!(duplicate_internal.contains("internal.crates has duplicate crate")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3766,7 +3762,7 @@ crates = ["radroots_a", "radroots_a"] assert!(duplicate_order.contains("publish_order.crates has duplicate crate")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -3794,7 +3790,7 @@ crates = ["radroots_a"] #[test] fn load_release_contract_with_override_reports_override_and_missing_policy_errors() { let root = create_synthetic_workspace("release_contract_loader_errors"); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); let missing_override = root.join("missing-release-policy.toml"); let override_err = load_release_contract_with_override( @@ -3806,7 +3802,7 @@ crates = ["radroots_a"] assert!(override_err.contains(RELEASE_POLICY_ENV)); assert!(override_err.contains("missing release policy file")); - let _ = fs::remove_file(contract_root.join("release").join("publish-set.toml")); + let _ = fs::remove_file(root_release_policy_path(&root)); let missing_policy_err = load_release_contract_with_override(&root, &contract_root, None) .expect_err("missing release policy should fail"); assert!(missing_policy_err.contains("release publish policy not found")); @@ -3902,7 +3898,7 @@ crates = ["radroots_a"] configure_root_release_policy_workspace(&root); write_root_release_policy(&root, policy_body); - let err = validate_release_publish_policy(&root, &root.join("contract"), "1.0.0") + let err = validate_release_publish_policy(&root, &root.join("spec"), "1.0.0") .expect_err("invalid non-public classification should fail"); assert!(err.contains(expected), "{label} err: {err}"); @@ -3920,7 +3916,7 @@ crates = ["radroots_a"] let invalid_bundle = create_synthetic_workspace("preflight_invalid_bundle"); write_file( - &invalid_bundle.join("contract").join("manifest.toml"), + &invalid_bundle.join("spec").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -3943,21 +3939,16 @@ require_conformance_vectors = true let _ = fs::remove_dir_all(&invalid_bundle); let missing_release = create_synthetic_workspace("preflight_missing_release"); - let _ = fs::remove_file( - missing_release - .join("contract") - .join("release") - .join("publish-set.toml"), - ); + let _ = fs::remove_file(root_release_policy_path(&missing_release)); let missing_release_err = validate_release_preflight(&missing_release).expect_err("missing release"); - assert!(missing_release_err.contains("publish-set.toml")); + assert!(missing_release_err.contains(ROOT_RELEASE_POLICY_RELATIVE)); let _ = fs::remove_dir_all(&missing_release); let missing_required = create_synthetic_workspace("preflight_missing_required"); let _ = fs::remove_file( missing_required - .join("contract") + .join("policy") .join("coverage") .join("policy.toml"), ); @@ -3968,10 +3959,7 @@ require_conformance_vectors = true let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); write_file( - &duplicate_publish - .join("contract") - .join("release") - .join("publish-set.toml"), + &root_release_policy_path(&duplicate_publish), r#"[release] version = "1.0.0" @@ -3993,7 +3981,7 @@ crates = ["radroots_a"] let duplicate_required = create_synthetic_workspace("preflight_duplicate_required"); write_file( &duplicate_required - .join("contract") + .join("policy") .join("coverage") .join("policy.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", @@ -4035,12 +4023,12 @@ edition = "2024" #[test] fn load_contract_bundle_and_validation_report_version_export_and_coverage_errors() { let root = create_synthetic_workspace("bundle_version_export_and_coverage_errors"); - write_file(&root.join("contract").join("version.toml"), "[contract"); + write_file(&root.join("spec").join("version.toml"), "[contract"); let version_parse_err = load_contract_bundle(&root).expect_err("invalid version file"); assert!(version_parse_err.contains("version.toml")); write_file( - &root.join("contract").join("version.toml"), + &root.join("spec").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -4057,14 +4045,14 @@ requires_release_notes = true "#, ); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), "[language", ); let export_parse_err = load_contract_bundle(&root).expect_err("invalid export mapping"); assert!(export_parse_err.contains("ts.toml")); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -4102,7 +4090,7 @@ Volume, "#, ); write_file( - &root.join("contract").join("coverage").join("policy.toml"), + &root.join("policy").join("coverage").join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -4260,10 +4248,12 @@ Volume, #[test] fn coverage_and_release_additional_error_branches_are_reported() { let root = create_synthetic_workspace("coverage_release_extra_errors"); - let contract_root = root.join("contract"); + let contract_root = root.join("spec"); + let coverage_root = coverage_root(&contract_root); + let release_policy_path = root_release_policy_path(&root); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -4280,7 +4270,7 @@ crates = ["radroots_a", "radroots_b", "radroots_extra"] assert!(coverage_extra.contains("includes unknown crates")); write_file( - &contract_root.join("coverage").join("policy.toml"), + &coverage_root.join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -4297,7 +4287,7 @@ crates = ["radroots_a"] assert!(required_list_mismatch.contains("missing workspace crates")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -4316,7 +4306,7 @@ crates = ["radroots_a", "radroots_b"] assert!(release_extra.contains("include unknown crates")); write_file( - &contract_root.join("release").join("publish-set.toml"), + &release_policy_path, r#"[release] version = "1.0.0" @@ -4340,7 +4330,7 @@ crates = ["radroots_a", "radroots_b"] #[test] fn load_contract_bundle_reports_exports_dir_errors_and_skips_non_toml() { let root = create_synthetic_workspace("bundle_exports_dir_errors"); - let exports_dir = root.join("contract").join("exports"); + let exports_dir = root.join("spec").join("exports"); let _ = fs::remove_dir_all(&exports_dir); write_file(&exports_dir, "not-a-dir"); @@ -4385,10 +4375,7 @@ manifest_file = "export-manifest.json" let release_error_root = create_synthetic_workspace("bundle_release_policy_error"); write_file( - &release_error_root - .join("contract") - .join("release") - .join("publish-set.toml"), + &root_release_policy_path(&release_error_root), r#"[release] version = "1.0.0" diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -598,7 +598,7 @@ fn validate_override_threshold( } pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf { - root.join("contract").join("coverage").join("policy.toml") + root.join("policy").join("coverage").join("policy.toml") } pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> { @@ -704,7 +704,7 @@ fn read_coverage_profile( crate_name: &str, ) -> Result<CoverageProfile, String> { let path = workspace_root - .join("contract") + .join("policy") .join("coverage") .join("profiles.toml"); if !path.exists() { @@ -2101,7 +2101,7 @@ mod tests { #[test] fn report_missing_gate_uses_policy_thresholds() { let root = temp_dir_path("report_missing_gate_root"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2145,7 +2145,7 @@ mod tests { #[test] fn report_missing_gate_uses_scope_specific_override_thresholds() { let root = temp_dir_path("report_missing_gate_override_root"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2238,7 +2238,7 @@ mod tests { .expect_err("missing policy should fail"); assert!(policy_err.contains("failed to read coverage policy")); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2266,7 +2266,7 @@ mod tests { #[test] fn refresh_summary_uses_measured_gate_report_values() { let root = temp_dir_path("refresh_summary_root"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2341,7 +2341,7 @@ mod tests { fs::remove_dir_all(root).expect("remove root"); let defaults_root = temp_dir_path("refresh_summary_defaults_root"); - let defaults_coverage_dir = defaults_root.join("contract").join("coverage"); + let defaults_coverage_dir = defaults_root.join("policy").join("coverage"); fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir"); write_file( &defaults_coverage_dir.join("policy.toml"), @@ -2406,7 +2406,7 @@ mod tests { ); let dispatch_root = temp_dir_path("refresh_summary_parentless_root"); - let dispatch_coverage_dir = dispatch_root.join("contract").join("coverage"); + let dispatch_coverage_dir = dispatch_root.join("policy").join("coverage"); fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir"); write_file( &dispatch_coverage_dir.join("policy.toml"), @@ -2494,7 +2494,7 @@ mod tests { #[test] fn refresh_summary_rejects_empty_output_paths() { let root = temp_dir_path("refresh_summary_empty_paths_root"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2579,7 +2579,7 @@ mod tests { #[test] fn refresh_summary_reports_output_parent_creation_failure() { let root = temp_dir_path("refresh_summary_out_parent_fail"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2650,7 +2650,7 @@ mod tests { #[test] fn refresh_summary_reports_status_output_parent_creation_failure() { let root = temp_dir_path("refresh_summary_status_parent_fail"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2738,7 +2738,7 @@ mod tests { .expect_err("missing policy should fail"); assert!(policy_err.contains("failed to read coverage policy")); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), @@ -2908,7 +2908,7 @@ mod tests { #[test] fn coverage_profiles_merge_defaults_and_crate_overrides() { let root = temp_dir_path("profile_merge"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), @@ -2940,7 +2940,7 @@ features = ["rt"] #[test] fn coverage_profiles_accept_positive_test_threads() { let root = temp_dir_path("profile_positive_threads"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), @@ -2958,7 +2958,7 @@ test_threads = 4 #[test] fn coverage_profiles_reject_invalid_feature_and_thread_values() { let root = temp_dir_path("profile_invalid"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), @@ -2981,7 +2981,7 @@ test_threads = 0 #[test] fn coverage_profiles_reject_invalid_toml() { let root = temp_dir_path("profile_invalid_toml"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write(coverage_dir.join("profiles.toml"), "[profiles.default\n") .expect("write invalid profiles"); @@ -2993,7 +2993,7 @@ test_threads = 0 #[test] fn coverage_profiles_reject_zero_test_threads_without_feature_error() { let root = temp_dir_path("profile_invalid_threads"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( coverage_dir.join("profiles.toml"), @@ -3786,7 +3786,7 @@ test_threads = 0 write_minimal_workspace(&profile_root); write_file( &profile_root - .join("contract") + .join("policy") .join("coverage") .join("profiles.toml"), "[profiles.default]\nfeatures = [\"\"]\n", @@ -3878,7 +3878,7 @@ test_threads = 0 #[test] fn report_gate_with_root_uses_scope_specific_override_thresholds() { let root = temp_dir_path("report_gate_override_success"); - let coverage_dir = root.join("contract").join("coverage"); + let coverage_dir = root.join("policy").join("coverage"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( &coverage_dir.join("policy.toml"), diff --git a/crates/xtask/src/export_ts.rs b/crates/xtask/src/export_ts.rs @@ -651,7 +651,7 @@ publish = false "#, ); write_file( - &root.join("contract").join("manifest.toml"), + &root.join("spec").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -669,7 +669,7 @@ require_conformance_vectors = true "#, ); write_file( - &root.join("contract").join("version.toml"), + &root.join("spec").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -686,7 +686,7 @@ requires_release_notes = true "#, ); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -702,7 +702,7 @@ manifest_file = "export-manifest.json" "#, ); write_file( - &root.join("contract").join("coverage").join("policy.toml"), + &root.join("policy").join("coverage").join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -716,9 +716,10 @@ crates = ["radroots_a", "radroots_b"] ); write_file( &root - .join("contract") + .join("contracts") .join("release") - .join("publish-set.toml"), + .join("mounted-rust-crates") + .join("publish-policy.toml"), r#"[release] version = "1.0.0" @@ -1228,7 +1229,7 @@ mod tests { fn export_ts_wasm_artifacts_copies_selected_wasm_package_dist() { let root = create_synthetic_workspace("export_ts_with_wasm_dist", false); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1352,7 +1353,7 @@ manifest_file = "export-manifest.json" fn write_manifest_reports_write_failures() { let root = create_synthetic_workspace("manifest_write_failure", false); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1378,7 +1379,7 @@ manifest_file = "packages" fn write_manifest_reports_parent_create_failures() { let root = create_synthetic_workspace("manifest_create_failure", false); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1503,7 +1504,7 @@ manifest_file = "nested/export-manifest.json" let _guard = workspace_lock().lock().expect("workspace lock"); let root = create_synthetic_workspace("generate_skip_non_ts_rs", true); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1534,7 +1535,7 @@ manifest_file = "export-manifest.json" let _guard = workspace_lock().lock().expect("workspace lock"); let root = create_synthetic_workspace("wrapper_bundle_success", true); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1621,7 +1622,7 @@ manifest_file = "export-manifest.json" let bundle_constants = create_synthetic_workspace("wrapper_bundle_constants_fail", true); write_file( &bundle_constants - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1648,7 +1649,7 @@ manifest_file = "export-manifest.json" let bundle_wasm = create_synthetic_workspace("wrapper_bundle_wasm_fail", true); write_file( - &bundle_wasm.join("contract").join("exports").join("ts.toml"), + &bundle_wasm.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1705,10 +1706,7 @@ manifest_file = "export-manifest.json" let crate_constants = create_synthetic_workspace("wrapper_crate_constants_fail", true); write_file( - &crate_constants - .join("contract") - .join("exports") - .join("ts.toml"), + &crate_constants.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1734,7 +1732,7 @@ manifest_file = "export-manifest.json" let crate_wasm = create_synthetic_workspace("wrapper_crate_wasm_fail", true); write_file( - &crate_wasm.join("contract").join("exports").join("ts.toml"), + &crate_wasm.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -1793,7 +1791,7 @@ manifest_file = "export-manifest.json" let invalid_contract = create_synthetic_workspace("wrapper_invalid_contract", true); write_file( - &invalid_contract.join("contract").join("manifest.toml"), + &invalid_contract.join("spec").join("manifest.toml"), r#"[contract] name = "" version = "1.0.0" @@ -1822,7 +1820,7 @@ require_conformance_vectors = true let missing_ts_export = create_synthetic_workspace("wrapper_missing_ts_export", true); write_file( &missing_ts_export - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1873,7 +1871,7 @@ repository = "sdk-python" let missing_artifacts = create_synthetic_workspace("wrapper_missing_artifacts", true); write_file( &missing_artifacts - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1895,7 +1893,7 @@ repository = "sdk-typescript" let missing_models_dir = create_synthetic_workspace("wrapper_missing_models_dir", true); write_file( &missing_models_dir - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1923,7 +1921,7 @@ manifest_file = "export-manifest.json" create_synthetic_workspace("wrapper_missing_constants_dir", true); write_file( &missing_constants_dir - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1951,7 +1949,7 @@ manifest_file = "export-manifest.json" let missing_wasm_dir = create_synthetic_workspace("wrapper_missing_wasm_dir", true); write_file( &missing_wasm_dir - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -1979,7 +1977,7 @@ manifest_file = "export-manifest.json" create_synthetic_workspace("wrapper_missing_manifest_file", true); write_file( &missing_manifest_file - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] @@ -2054,7 +2052,7 @@ manifest_file = "" let wasm_copy_err_root = create_synthetic_workspace("wrapper_wasm_copy_err", true); write_file( &wasm_copy_err_root - .join("contract") + .join("spec") .join("exports") .join("ts.toml"), r#"[language] diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -340,7 +340,7 @@ publish = false "#, ); write_file( - &root.join("contract").join("manifest.toml"), + &root.join("spec").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -358,7 +358,7 @@ require_conformance_vectors = true "#, ); write_file( - &root.join("contract").join("version.toml"), + &root.join("spec").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -375,7 +375,7 @@ requires_release_notes = true "#, ); write_file( - &root.join("contract").join("exports").join("ts.toml"), + &root.join("spec").join("exports").join("ts.toml"), r#"[language] id = "ts" repository = "sdk-typescript" @@ -391,7 +391,7 @@ manifest_file = "export-manifest.json" "#, ); write_file( - &root.join("contract").join("coverage").join("policy.toml"), + &root.join("policy").join("coverage").join("policy.toml"), r#"[gate] fail_under_exec_lines = 100.0 fail_under_functions = 100.0 @@ -405,9 +405,10 @@ crates = ["radroots_a", "radroots_b"] ); write_file( &root - .join("contract") + .join("contracts") .join("release") - .join("publish-set.toml"), + .join("mounted-rust-crates") + .join("publish-policy.toml"), r#"[release] version = "1.0.0" @@ -580,7 +581,7 @@ crates = ["radroots_a"] let parent = coverage_refresh_path.parent().expect("coverage parent"); fs::create_dir_all(parent).expect("create coverage parent"); let required_raw = - fs::read_to_string(root.join("contract").join("coverage").join("policy.toml")) + fs::read_to_string(root.join("policy").join("coverage").join("policy.toml")) .expect("read coverage policy contract"); let required_toml = toml::from_str::<toml::Value>(&required_raw).expect("parse coverage policy contract"); diff --git a/nix/common.nix b/nix/common.nix @@ -22,7 +22,8 @@ let ../README ../rust-toolchain.toml ../conformance - ../contract + ../spec + ../policy ../crates ../scripts ] diff --git a/contract/coverage/POLICY.md b/policy/coverage/POLICY.md diff --git a/contract/coverage/policy.toml b/policy/coverage/policy.toml diff --git a/contract/coverage/profiles.toml b/policy/coverage/profiles.toml diff --git a/spec/RCLD.md b/spec/RCLD.md @@ -0,0 +1,877 @@ +# Radroots Cross-Language SDK Contract Design + +Status: approved direction, design artifact + +Scope: public Radroots SDK contract for external language SDKs derived from the Rust workspace in this repository + +Canonical source: Rust remains the canonical implementation and conformance source for public contract behavior + +## Purpose + +This document defines the approved operation-first design for the Radroots cross-language SDK contract. + +It replaces the crate-first mental model currently expressed in `spec/manifest.toml` with a public contract shaped around external integration tasks: + +- produce Radroots-compliant Nostr events +- parse Radroots-compliant Nostr events +- validate Radroots-compliant contract behavior +- preserve deterministic cross-language behavior for supported operations + +This document does not require the Rust workspace to stop using crate boundaries internally. Crates remain implementation and provenance boundaries inside Rust. Operations become the public SDK boundary. + +## Problem + +The current repository expresses SDK surface primarily in terms of Rust crates: + +- `surface.model_crates` +- `surface.algorithm_crates` +- `surface.wasm_crates` +- language export manifests keyed by Rust crate name +- `xtask` validation and export logic that assume crate-to-package mapping + +That framing is not aligned with the needs of third-party integrators. Integrators do not want a mirror of the Rust workspace. They want a small, stable, idiomatic SDK that helps them publish and read Radroots-compliant Nostr events. + +The codebase already contains the correct technical boundary: + +- event model types in `radroots_events`, `radroots_trade`, `radroots_identity`, and supporting model crates +- deterministic builders and parsers in `radroots_events_codec` +- shared unsigned event primitives in `radroots_events_codec::wire` +- optional wasm packaging for deterministic helper logic + +The contract must move upward from crate inventory to public operations. + +## Decisions Ratified + +The following decisions are approved and are treated as default design constraints: + +- external SDKs optimize first for third-party app integrations, not for full Radroots internal app parity +- publishing is the first-class use case +- reading and validation are supported for the same Tier 1 domains, but remain secondary to publishing +- Tier 1 domains are `profile`, `farm`, `listing`, and `trade` +- the public contract unit is an operation, not a crate +- networking and signing remain native to each target language +- TypeScript is the first reference SDK for the new contract +- Python follows after TypeScript proves the operation model +- Rust crate names are not part of the public SDK mental model +- migration should be additive first and support old and new manifest shapes during transition + +## Goals + +- define a stable public SDK contract in terms of operations +- preserve Rust as the canonical behavioral implementation +- support idiomatic language SDKs without requiring full Rust API parity +- keep transport, relay IO, and signing runtime-native +- make deterministic encode, parse, normalize, and validate behavior conformance-testable +- provide a migration path from the current crate-keyed contract and export system + +## Non-Goals + +- exporting every Rust function to every language +- standardizing one shared Nostr client implementation across languages +- exposing Radroots app-internal marketplace, replica, moderation, or backoffice surfaces as public SDK APIs +- introducing a flag-day rewrite of the entire contract and export toolchain + +## External Audience + +The public SDK contract is designed for: + +- third-party apps publishing Radroots-compliant profiles, farms, listings, and trade events +- apps that need to parse or validate those supported event families +- language SDK maintainers implementing contract-compliant APIs in TypeScript, Python, Swift, and Kotlin + +The public SDK contract is not designed for: + +- exposing Radroots internal admin flows +- exposing internal replica storage contracts +- exposing internal moderation and backoffice read models + +## Public Contract Principles + +1. Operations are public. Crates are internal. +2. Inputs, outputs, and errors are explicit. +3. Deterministic behavior is contract material. +4. Cross-language conformance is mandatory for approved operations. +5. Runtime choices such as relay transport and signer integration remain language-native. +6. Public surface must be narrower than the full Rust workspace. +7. Internal app-specific projections are excluded unless explicitly promoted. + +## Public Surface Taxonomy + +The public SDK contract has four surface classes: + +### 1. Operations + +Task-oriented public entry points for supported domains. + +Examples: + +- `profile.build_draft` +- `farm.build_draft` +- `listing.build_tags` +- `listing.build_draft` +- `listing.parse_event` +- `trade.build_envelope_draft` +- `trade.parse_envelope` +- `trade.parse_listing_address` +- `trade.validate_listing_event` + +### 2. Shared Types + +Public cross-operation types required for operation inputs and outputs. + +Examples: + +- `WireEventParts` +- `UnsignedEventDraft` +- `RadrootsNostrEvent` +- `RadrootsNostrEventRef` +- `RadrootsTradeListingAddress` + +### 3. Shared Errors + +Public error categories and domain-specific parse and validation errors that languages must preserve semantically. + +Examples: + +- event encode errors +- listing parse errors +- trade envelope parse errors +- listing validation errors + +### 4. Implementation Provenance + +Rust crate and wasm provenance used by maintainers and tooling, but not treated as the public contract unit. + +Examples: + +- operation implemented in `radroots_events_codec` +- type defined in `radroots_events` +- deterministic helper exposed via `radroots_events_codec_wasm` + +## Tier 1 Domains And Operations + +The initial approved public domains are `profile`, `farm`, `listing`, and `trade`. + +The following operations form the recommended Tier 1 surface. + +### Profile + +#### `profile.build_draft` + +Purpose: produce an unsigned Nostr event draft for a Radroots profile event + +Rust implementation sources: + +- `crates/events_codec/src/profile/encode.rs` + +Input: + +- `RadrootsProfile` +- optional `RadrootsProfileType` + +Output: + +- `WireEventParts` +- optional `UnsignedEventDraft` helper via shared draft adapter + +Determinism: + +- required + +Runtime ownership: + +- signing: native +- transport: native + +### Farm + +#### `farm.build_draft` + +Purpose: produce an unsigned Nostr event draft for a farm event + +Rust implementation sources: + +- `crates/events_codec/src/farm/encode.rs` + +Input: + +- `RadrootsFarm` + +Output: + +- `WireEventParts` + +Determinism: + +- required + +Runtime ownership: + +- signing: native +- transport: native + +### Listing + +#### `listing.build_tags` + +Purpose: produce canonical listing tags without creating a full unsigned event + +Rust implementation sources: + +- `crates/events_codec/src/listing/encode.rs` +- `crates/events_codec/src/listing/tags.rs` + +Input: + +- `RadrootsListing` + +Output: + +- `Vec<Vec<String>>` + +Determinism: + +- required + +#### `listing.build_draft` + +Purpose: produce an unsigned listing event contract from a listing model + +Rust implementation sources: + +- `crates/events_codec/src/listing/encode.rs` +- `crates/events_codec/src/wire.rs` + +Input: + +- `RadrootsListing` +- optional listing kind override when explicitly allowed + +Output: + +- `WireEventParts` +- optionally adapted to `UnsignedEventDraft` + +Determinism: + +- required + +#### `listing.parse_event` + +Purpose: parse a listing event into the canonical listing model + +Rust implementation sources: + +- `crates/trade/src/listing/codec.rs` +- `crates/events_codec/src/listing/decode.rs` + +Input: + +- `RadrootsNostrEvent` + +Output: + +- `RadrootsListing` + +Determinism: + +- required + +### Trade + +#### `trade.build_envelope_draft` + +Purpose: produce an unsigned trade envelope event from typed trade payload input + +Rust implementation sources: + +- `crates/events_codec/src/trade/encode.rs` + +Input: + +- recipient pubkey +- trade message type +- listing address +- optional order id +- optional listing event pointer +- optional root event id +- optional previous event id +- typed trade payload + +Output: + +- `WireEventParts` + +Determinism: + +- required + +#### `trade.parse_envelope` + +Purpose: parse a trade event into a typed trade envelope + +Rust implementation sources: + +- `crates/events_codec/src/trade/decode.rs` + +Input: + +- `RadrootsNostrEvent` + +Output: + +- typed `RadrootsTradeEnvelope<T>` + +Determinism: + +- required + +#### `trade.parse_listing_address` + +Purpose: parse and validate the canonical listing address used by trade flows + +Rust implementation sources: + +- `crates/events_codec/src/trade/decode.rs` + +Input: + +- listing address string + +Output: + +- `RadrootsTradeListingAddress` + +Determinism: + +- required + +#### `trade.validate_listing_event` + +Purpose: validate that an event meets Radroots listing contract expectations for trade workflows + +Rust implementation sources: + +- `crates/trade/src/listing/validation.rs` + +Input: + +- `RadrootsNostrEvent` +- optional fetched dependencies if the validation path requires them + +Output: + +- validation result structure or domain validation error + +Determinism: + +- required for local validation logic +- explicitly scoped where external dependency fetch is involved + +## Shared Types + +The public contract should explicitly enumerate a minimal shared type set. + +Recommended Tier 1 shared types: + +- `WireEventParts` +- `UnsignedEventDraft` +- `RadrootsNostrEvent` +- `RadrootsNostrEventRef` +- `RadrootsNostrEventPtr` +- `RadrootsTradeListingAddress` +- public model types required by Tier 1 operations: +- `RadrootsProfile` +- `RadrootsFarm` +- `RadrootsListing` +- trade payload and envelope types required by approved trade operations + +`UnsignedEventDraft` should be a public contract alias or wrapper over the current `EventDraft` concept in `crates/events_codec/src/wire.rs`. The public naming should emphasize unsigned event construction rather than internal adapter mechanics. + +## Shared Errors + +The contract should distinguish between: + +- semantic error categories that are part of the public API +- internal implementation error types that can be mapped privately + +Recommended public error classes: + +- `encode_error` +- `parse_error` +- `validation_error` +- `address_error` + +Recommended public semantic guarantees: + +- required-field failures remain distinguishable +- invalid-kind failures remain distinguishable +- invalid-json failures remain distinguishable where applicable +- domain-specific parse mismatches remain distinguishable for listing and trade operations + +Language SDKs may translate exact type names, but they must preserve error meaning and conformance behavior. + +## Explicit Exclusions From The Public SDK + +The following surfaces remain internal unless separately promoted: + +- `radroots_replica_*` surfaces +- backoffice overlays +- marketplace read models and projections +- internal moderation models +- full `radroots_nostr` client runtime +- internal runtime management contracts + +This exclusion is important because the Rust workspace contains valuable internal app surfaces that are not appropriate to freeze as external SDK contract. + +## Runtime Ownership Model + +The cross-language contract owns: + +- deterministic model encode behavior +- parse behavior +- validation behavior +- canonical tags and content construction +- canonical address and pointer parsing + +The language runtime owns: + +- relay transport +- signer integration +- key management +- subscription lifecycle +- connection policies +- local storage and caching choices + +This means SDKs should primarily produce and consume unsigned or already-signed event shapes rather than wrapping one shared transport stack. + +## Package Strategy + +### Public Package Strategy + +The public package strategy should be operation-first and ergonomic. + +Recommended TypeScript package strategy: + +- one main package, for example `@radroots/sdk` +- one optional deterministic helper package or embedded asset for wasm-backed helpers + +Recommended Python package strategy: + +- one main package, for example `radroots_sdk` +- optional implementation-private native or wasm helper assets + +Recommended Swift and Kotlin strategy: + +- one main package or module namespace per language +- helper implementation details remain private unless explicitly useful + +### What Not To Ship + +Do not use crate-mirror packages as the primary public shape: + +- `@radroots/core` +- `@radroots/types` +- `@radroots/events` +- `@radroots/trade` +- `@radroots/identity` + +Those may remain transitional or internal build artifacts, but they should not be the product definition for external integrators. + +## Contract Schema v2 + +The new contract should be additive first. The repository should support both: + +- the existing crate-keyed contract metadata +- a new operation-keyed contract schema + +The operation-keyed schema should become the public source of truth. Crate metadata should become provenance or migration-only data. + +### Recommended Top-Level Manifest Shape + +```toml +[contract] +name = "radroots-sdk-contract" +version = "0.2.0-alpha.1" +source = "rust" +stability = "draft" + +[public] +domains = ["profile", "farm", "listing", "trade"] + +[shared_types] +public = [ + "WireEventParts", + "UnsignedEventDraft", + "RadrootsNostrEvent", + "RadrootsNostrEventRef", + "RadrootsNostrEventPtr", + "RadrootsTradeListingAddress", + "RadrootsProfile", + "RadrootsFarm", + "RadrootsListing", +] + +[errors] +classes = ["encode_error", "parse_error", "validation_error", "address_error"] + +[operations.profile_build_draft] +domain = "profile" +id = "profile.build_draft" +stability = "beta" +inputs = ["RadrootsProfile", "RadrootsProfileType?"] +outputs = ["WireEventParts"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.profile_build_draft.implementation] +rust_modules = ["crates/events_codec/src/profile/encode.rs"] +rust_types = ["radroots_events::profile::RadrootsProfile"] + +[operations.profile_build_draft.conformance] +vector = "conformance/vectors/profile/build_draft.v1.json" + +[operations.listing_build_draft] +domain = "listing" +id = "listing.build_draft" +stability = "beta" +inputs = ["RadrootsListing"] +outputs = ["WireEventParts"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.listing_build_draft.implementation] +rust_modules = [ + "crates/events_codec/src/listing/encode.rs", + "crates/events_codec/src/listing/tags.rs", + "crates/events_codec/src/wire.rs", +] + +[operations.listing_build_draft.conformance] +vector = "conformance/vectors/listing/build_draft.v1.json" +``` + +### Provenance Section + +During migration, crate provenance should remain available: + +```toml +[implementation_provenance] +model_crates = [ + "radroots_core", + "radroots_types", + "radroots_events", + "radroots_trade", + "radroots_identity", +] +algorithm_crates = ["radroots_events_codec"] +wasm_crates = ["radroots_events_codec_wasm"] +``` + +This keeps current workspace knowledge available without making it the public contract unit. + +## Language Export Manifest v2 + +Language export manifests should stop mapping crate names to packages as the primary concept. + +Instead they should answer: + +- which operations are supported in the language +- where those operations are exposed +- how deterministic logic is implemented +- which shared types and error classes are public + +### Recommended TypeScript Export Shape + +```toml +[language] +id = "ts" +repository = "sdk-typescript" + +[sdk] +package = "@radroots/sdk" +module_format = "esm" +deterministic_codec = "wasm" +signing = "native" +networking = "native" + +[operations] +"profile.build_draft" = "profile.buildDraft" +"farm.build_draft" = "farm.buildDraft" +"listing.build_tags" = "listing.buildTags" +"listing.build_draft" = "listing.buildDraft" +"listing.parse_event" = "listing.parseEvent" +"trade.build_envelope_draft" = "trade.buildEnvelopeDraft" +"trade.parse_envelope" = "trade.parseEnvelope" +"trade.parse_listing_address" = "trade.parseListingAddress" +"trade.validate_listing_event" = "trade.validateListingEvent" + +[shared_types] +"WireEventParts" = "WireEventParts" +"UnsignedEventDraft" = "UnsignedEventDraft" +"RadrootsNostrEvent" = "RadrootsNostrEvent" +"RadrootsTradeListingAddress" = "TradeListingAddress" + +[artifacts] +models_dir = "src/generated" +runtime_dir = "src/runtime" +wasm_dist_dir = "dist" +manifest_file = "export-manifest.json" +``` + +Equivalent manifests for Python, Swift, and Kotlin should use their own naming conventions. + +## Export Manifest Output + +`xtask` should write an export manifest that includes operation coverage metadata, not only file hashes. + +Recommended structure: + +```json +{ + "language": "ts", + "sdk_package": "@radroots/sdk", + "operations": [ + { + "id": "listing.build_draft", + "symbol": "listing.buildDraft", + "deterministic_codec": "wasm" + } + ], + "files": [ + { + "path": "src/generated/listing.ts", + "sha256": "..." + } + ] +} +``` + +## Conformance Model + +Conformance becomes the real multi-language product gate for the public contract. + +### Rules + +- every public operation must have at least one conformance vector suite +- deterministic operations require positive and negative vectors +- parse operations require round-trip or semantic equivalence vectors where applicable +- error behavior that is part of the contract must be vectorized +- language SDKs must pass conformance without local overrides + +### Recommended Vector Layout + +```text +conformance/ + vectors/ + profile/ + build_draft.v1.json + farm/ + build_draft.v1.json + listing/ + build_tags.v1.json + build_draft.v1.json + parse_event.v1.json + trade/ + build_envelope_draft.v1.json + parse_envelope.v1.json + parse_listing_address.v1.json + validate_listing_event.v1.json +``` + +### Minimum Vector Coverage + +For each operation: + +- one minimal valid case +- one rich valid case +- one canonical normalization case if normalization exists +- one required-field failure case +- one invalid-format or invalid-kind failure case where applicable + +## Versioning Policy v2 + +The contract version policy must shift from exported crate surface to operation semantics. + +### Major Version Triggers + +- remove a public operation +- change required operation input shape +- change output shape incompatibly +- change deterministic operation behavior incompatibly +- collapse or remove a public error distinction + +### Minor Version Triggers + +- add a public operation +- add optional input or output fields +- add a new public shared type +- add new conformance vectors that extend supported behavior without breaking old behavior + +### Patch Version Triggers + +- documentation fixes +- packaging fixes without behavior changes +- non-behavioral codegen fixes +- bug fixes that do not change the contract shape and do not invalidate existing conforming clients + +## Rust Implementation Strategy + +The Rust workspace should add a curated facade for approved public operations. + +Recommended shape: + +- add a new crate or dedicated public module namespace, for example `radroots_sdk_contract` +- re-export only approved operations and approved shared types +- keep direct crate internals available for Rust maintainers but do not treat them as the cross-language contract by default + +This facade should: + +- define public operation names +- define any public wrapper naming such as `UnsignedEventDraft` +- centralize contract documentation and source references +- make it easier for generators and future language bindings to target one approved surface + +## `xtask` Migration Strategy + +### Current State + +`xtask` currently: + +- parses a crate-keyed surface +- validates crate-keyed export coverage +- exports TypeScript by iterating crate-to-package mappings +- has tests that assert TypeScript export coverage matches model plus wasm crates + +### Required Changes + +1. add new manifest parsing structs for operation-based contract metadata +2. support dual parsing during migration +3. introduce validation for: +- non-empty public domain list +- unique operation ids +- known shared type references +- conformance vector presence for each public operation +- language export manifests mapping approved operations +4. replace crate-coverage assertions with operation-coverage assertions +5. update export manifest generation to report operation coverage +6. keep current crate provenance checks only as implementation validation + +### Recommended `xtask` Command Evolution + +Keep existing commands temporarily: + +- `sdk export-ts` +- `sdk validate` + +Add new migration-aware behavior behind the same commands: + +- `sdk validate` validates both old and new contract surfaces +- `sdk export-ts` assembles an operation-first TypeScript SDK package from approved operations + +Optional additive commands: + +- `sdk validate-operations` +- `sdk export-ts-sdk` +- `sdk conformance check --language <id>` + +## Language SDK Strategy + +### TypeScript + +TypeScript is the reference external SDK. + +Implementation recommendation: + +- keep Rust-driven generated models where useful +- use wasm for deterministic codec helpers where beneficial +- handwrite the final ergonomic operation surface +- expose one main SDK package + +### Python + +Python follows after TypeScript proves the operation model. + +Implementation recommendation: + +- do not mirror Rust crates directly +- either implement pure-Python adapters on top of contract artifacts or bind a very small deterministic Rust core +- keep packaging centered on one main SDK package + +### Swift And Kotlin + +Swift and Kotlin should wait until the operation contract is stable and conformance coverage is broader. + +Implementation recommendation: + +- keep the same operation contract +- keep runtime integration native +- only introduce shared native bindings for a narrow deterministic core if the maintenance tradeoff is justified + +## Migration Plan + +### Phase 0: Ratify Design + +- adopt this design as the operation-first target +- treat current crate-keyed metadata as migration-only + +### Phase 1: Add New Contract Metadata + +- add operation-based metadata to the contract directory +- keep crate provenance metadata for existing tooling +- do not remove current manifest shape yet + +### Phase 2: Add Curated Rust Facade + +- introduce a public Rust contract facade +- map approved operations to existing implementation functions +- exclude projections, overlays, replica, and backoffice surfaces + +### Phase 3: Expand Conformance + +- add operation-based vectors for Tier 1 operations +- make vector coverage a release-blocking validation rule + +### Phase 4: Migrate TypeScript Export + +- shift `xtask export-ts` to operation-first packaging +- ship one main external TypeScript SDK package +- keep transitional generated artifacts internal if necessary + +### Phase 5: Introduce Python + +- implement Python export or packaging against the same contract and vector set + +### Phase 6: Remove Crate-First Public Assumptions + +- remove tests and validation that require package coverage by Rust crate name +- keep crate provenance only as internal documentation and maintenance metadata + +## Acceptance Criteria + +This design is implemented successfully when: + +- the public contract manifest declares operations, shared types, and error classes +- `xtask validate` enforces operation coverage and conformance presence +- the curated Rust facade exposes only approved public operations +- the TypeScript SDK ships an operation-first public API +- conformance vectors exist for every Tier 1 operation +- public docs describe the SDK in terms of operations, not Rust crates + +## Immediate Next Workstreams + +1. introduce contract schema v2 files and parser structs +2. define the exact Tier 1 operation ids in machine-readable metadata +3. add a Rust public facade crate or module for those operations +4. author conformance vectors for every Tier 1 operation +5. rewrite `xtask` validator assumptions +6. redesign the TypeScript export manifest and package assembly +7. draft the first external TypeScript SDK surface around the approved operations + +## Repository Notes + +This document intentionally does not modify the current crate-keyed contract files in place. The repository currently contains user edits in several existing files, including `spec/README`. The recommended implementation path is to add the new operation-first contract artifacts alongside the current files first, then migrate validation and export tooling incrementally. diff --git a/spec/README.md b/spec/README.md @@ -0,0 +1,83 @@ +# radroots-sdk-contract + +Core contract for the Rad Roots cross-language SDK. + +## Purpose + +This directory defines the Rad Roots SDK contract used to align Rust, TypeScript, Python, Swift, and Kotlin surfaces. +It defines the public interoperability boundary for external integrators, keeps Rust as the canonical source for exported models and transforms, and enforces deterministic, machine-verifiable governance for contract changes and releases. + +## Contract Surface + +Contract metadata is defined in `spec/manifest.toml` and currently includes: + +- model crates: `radroots_core`, `radroots_types`, `radroots_events`, `radroots_trade`, `radroots_identity` +- algorithm crate: `radroots_events_codec` +- wasm crate: `radroots_events_codec_wasm` + +Public SDK exports are intentionally narrower than the full Rust workspace. + +## Export Targets + +Language export mappings and artifact layout rules are defined under `spec/exports/`: + +- `spec/exports/ts.toml` +- `spec/exports/py.toml` +- `spec/exports/swift.toml` +- `spec/exports/kotlin.toml` + +Each export target defines package naming, artifact directories, and runtime expectations. + +## Internal Replica Contract + +Offline-first replica crates are internal contract surfaces and are not public SDK exports. +Replica contract metadata is defined in `spec/replica.toml`. + +Internal replica crate family: + +- `radroots_replica_db_schema` +- `radroots_replica_db` +- `radroots_replica_db_wasm` +- `radroots_replica_sync` +- `radroots_replica_sync_wasm` + +## Governance + +Versioning and compatibility policy is defined in `spec/version.toml`. +Contract evolution is semver-governed and requires conformance updates, export manifest validation, and release notes. + +Repository guards also enforce: + +- deterministic export requirements +- strict no-legacy identifier policy for replica surfaces +- no committed generated TypeScript artifacts in repo export directories (`target/ts-rs/` and `target/sdk-export-ci/`) + +## Coverage Policy + +Coverage governance is defined under `policy/coverage/`: + +- machine-readable policy: `policy/coverage/policy.toml` +- human policy notes: `policy/coverage/POLICY.md` +- per-crate profiles: `policy/coverage/profiles.toml` + +Required Rust crates are gated at `100/100/100/100` (exec lines, functions, branches, regions), with branch records required. + +## Release Policy + +Release crate classification and publish order are defined in the owning monorepo at +`contracts/release/mounted-rust-crates/publish-policy.toml`. +Operator workflow is root-owned and documented in: + +- `contracts/release/mounted-rust-crates/runbook.md` +- `contracts/release/mounted-rust-crates/checklist.md` + +Primary commands: + +- `cargo run -q -p xtask -- sdk validate` +- `cargo run -q -p xtask -- sdk release preflight` +- `./scripts/ci/release_preflight.sh` +- `scripts/release/rr-rs-preflight.sh <plan-id> [crate-list]` from the owning monorepo + +## License + +Licensed under AGPL-3.0. See LICENSE. diff --git a/contract/exports/kotlin.toml b/spec/exports/kotlin.toml diff --git a/contract/exports/py.toml b/spec/exports/py.toml diff --git a/contract/exports/swift.toml b/spec/exports/swift.toml diff --git a/contract/exports/ts.toml b/spec/exports/ts.toml diff --git a/contract/manifest.toml b/spec/manifest.toml diff --git a/contract/operations.toml b/spec/operations.toml diff --git a/contract/replica.toml b/spec/replica.toml diff --git a/contract/sdk-exports/ts.toml b/spec/sdk-exports/ts.toml diff --git a/contract/version.toml b/spec/version.toml