commit 2a2e48496d5f2a1734dfd8fabfd596b9f1413374 parent 63d76f4ec1c753bad09d1d39af80fd9d9eb38dbe Author: triesap <tyson@radroots.org> Date: Mon, 22 Jun 2026 02:02:24 +0000 contracts: consolidate core contract root Diffstat:
38 files changed, 903 insertions(+), 2059 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`, and `spec/README.md`. +- Read this file, `AGENT_INSTRUCTIONS.md`, and `README`. - When touching Nix behavior, read `flake.nix` and the active Nix implementation files under `nix/` until the approved `build/nix/` migration lands. - Enter the canonical environment with `nix develop` or `direnv allow` before targeted cargo work. @@ -59,7 +59,7 @@ Before editing code: ## 6. Contract and release discipline -- `spec/` (including `spec/conformance/`) and `crates/xtask` are authoritative for public SDK contract, export, and release governance. +- `contracts/` and `crates/xtask` are authoritative for core-library contracts, conformance, coverage, and release-candidate 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` and `spec/README.md` when the change touches workflow, exports, or public surfaces. +- Read `README` when the change touches workflow or public surfaces. - When touching Nix behavior, read `flake.nix` and the active Nix implementation files under `nix/` until the approved `build/nix/` migration lands. - Read the relevant crate manifest, implementation files, and nearby tests before proposing a new structure. @@ -65,9 +65,9 @@ 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 -- `spec/` - - public SDK contract metadata, export policy, release policy, and coverage governance -- `spec/conformance/` +- `contracts/` + - core-library contract metadata, release-candidate policy, coverage governance, and public conformance assets +- `contracts/conformance/` - cross-language and cross-surface vector expectations - `docs/` - durable workflow and environment documentation @@ -76,7 +76,7 @@ Use this mental model: - `scripts/` - repo-owned automation used by canonical lanes -Do not duplicate contract knowledge between crates when `spec/`, `spec/conformance/`, or `xtask` already owns it. +Do not duplicate contract knowledge between crates when `contracts/`, `contracts/conformance/`, or `xtask` already owns it. ## 5. Rust engineering standards @@ -136,7 +136,7 @@ Do not duplicate contract knowledge between crates when `spec/`, `spec/conforman ## 6. Contract, conformance, and release workflow -`spec/`, `spec/conformance/`, and `crates/xtask` are first-class parts of the product surface, not secondary metadata. +`contracts/`, `contracts/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/contracts/conformance/schema/vector.schema.json b/contracts/conformance/schema/vector.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://radroots.org/core/conformance/vector.schema.json", + "title": "radroots core conformance vector", + "type": "object", + "required": [ + "suite", + "contract_version", + "vectors" + ], + "properties": { + "suite": { + "type": "string", + "minLength": 1 + }, + "contract_version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "vectors": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "kind", + "input", + "expected" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "input": {}, + "expected": {} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/spec/conformance/vectors/events/listing_tags_full.v1.json b/contracts/conformance/vectors/events/listing_tags_full.v1.json diff --git a/spec/conformance/vectors/farm/build_draft.v1.json b/contracts/conformance/vectors/farm/build_draft.v1.json diff --git a/spec/conformance/vectors/identity/.gitkeep b/contracts/conformance/vectors/identity/.gitkeep diff --git a/spec/conformance/vectors/listing/build_draft.v1.json b/contracts/conformance/vectors/listing/build_draft.v1.json diff --git a/spec/conformance/vectors/listing/build_tags.v1.json b/contracts/conformance/vectors/listing/build_tags.v1.json diff --git a/spec/conformance/vectors/listing/parse_event.v1.json b/contracts/conformance/vectors/listing/parse_event.v1.json diff --git a/spec/conformance/vectors/order/build_order_decision_draft.v1.json b/contracts/conformance/vectors/order/build_order_decision_draft.v1.json diff --git a/spec/conformance/vectors/order/build_order_request_draft.v1.json b/contracts/conformance/vectors/order/build_order_request_draft.v1.json diff --git a/spec/conformance/vectors/order/parse_listing_address.v1.json b/contracts/conformance/vectors/order/parse_listing_address.v1.json diff --git a/spec/conformance/vectors/order/parse_order_decision.v1.json b/contracts/conformance/vectors/order/parse_order_decision.v1.json diff --git a/spec/conformance/vectors/order/parse_order_request.v1.json b/contracts/conformance/vectors/order/parse_order_request.v1.json diff --git a/spec/conformance/vectors/profile/build_draft.v1.json b/contracts/conformance/vectors/profile/build_draft.v1.json diff --git a/spec/conformance/vectors/social/mvp.v1.json b/contracts/conformance/vectors/social/mvp.v1.json diff --git a/spec/conformance/vectors/social/production.v1.json b/contracts/conformance/vectors/social/production.v1.json diff --git a/spec/conformance/vectors/social/upgraded_boundaries.v1.json b/contracts/conformance/vectors/social/upgraded_boundaries.v1.json diff --git a/spec/conformance/vectors/trade_validation/validate_listing_event.v1.json b/contracts/conformance/vectors/trade_validation/validate_listing_event.v1.json diff --git a/policy/coverage/profiles.toml b/contracts/coverage-profiles.toml diff --git a/policy/coverage/policy.toml b/contracts/coverage.toml diff --git a/contracts/events/social-events.md b/contracts/events/social-events.md @@ -0,0 +1,124 @@ +# Public Social Event Substrate + +Status: active implementation contract + +Scope: public Radroots social Nostr event models, codecs, and deterministic conformance vectors in +this repository. + +## Purpose + +The public social event substrate extends the Radroots event family beyond profile, farm, listing, +and trade workflows while keeping relay runtime behavior, application projections, moderation +services, and private Field business documents outside this repository's event-contract boundary. + +The target implementation is standards-first and Radroots-named. Event models live in +`radroots_events`, canonical encode/decode behavior lives in `radroots_events_codec`, and +deterministic fixtures live under `contracts/conformance`. + +## Implementation Inventory + +The repository implements public social support for kind `1` `RadrootsPost`, kind `1111` +`RadrootsComment`, kind `7` `RadrootsReaction`, generic `RadrootsList` entries, listing draft kind +`30403` through `RadrootsListing`, articles, generic public file metadata, calendar date events, +calendar time events, reposts, generic reposts, calendar collections, RSVP events, and reports. + +The closeout contract requires: + +- complete model and codec coverage for the approved public social event families +- kind and tag constants for the approved NIP surface +- `RadrootsPost` preservation for optional social metadata +- strict NIP-22 `RadrootsComment` behavior without legacy `e_root` or `e_prev` fallback tags +- strict NIP-25 `RadrootsReaction` behavior where empty content is a valid like +- explicit optional `published_at` support for NIP-99 listing parity +- NIP-65 relay-list validation evidence through `RadrootsList` +- conformance vectors and canonical-event witnesses for every new or upgraded social event family + +## Approved Event Families + +The MVP public social substrate includes: + +- `RadrootsPost` for ordinary NIP-01 kind `1` notes plus optional Radroots social metadata +- `RadrootsArticle` for NIP-23 kind `30023` long-form content +- generic public `RadrootsFileMetadata` for NIP-94 kind `1063` +- `RadrootsCalendarDateEvent` for NIP-52 kind `31922` +- `RadrootsCalendarTimeEvent` for NIP-52 kind `31923` + +The production-v1 public social substrate includes: + +- `RadrootsRepost` for NIP-18 kind `6` +- `RadrootsGenericRepost` for NIP-18 kind `16` +- `RadrootsCalendar` for NIP-52 kind `31924` +- `RadrootsCalendarEventRsvp` for NIP-52 kind `31925` +- `RadrootsReport` for NIP-56 kind `1984` +- listing draft kind `30403` validation through `RadrootsListing` +- relay-list kind `10002` validation through `RadrootsList` + +## Contract Decisions + +`RadrootsPost` remains compatible with ordinary kind `1` text notes. Content-only notes must remain +valid. Optional farm or address references, media metadata, geohash, topics, and quote references +must be preserved when present and must use serde defaults so existing simple JSON fixtures remain +valid. + +`RadrootsComment` uses strict NIP-22 semantics. The target and scope model must support event-id, +address, and external roots or parents through `E`/`e`, `A`/`a`, and `I`/`i` tags with matching +`K`/`k` kind metadata. Canonical encode and decode must reject ordinary kind `1` short text note +targets; kind `1` replies belong to NIP-10 text-note reply semantics instead. Canonical decode must +reject legacy `e_root` and `e_prev` fallback tags. + +`RadrootsReaction` uses strict NIP-25 semantics. Empty content, `+`, `-`, emoji, and custom reaction +content are valid when the target tags are valid. Missing targets remain invalid. + +`RadrootsReport` intentionally tightens NIP-56 for the Radroots type: a reported pubkey `p` tag is +required for a valid report, including event and file or blob reports. + +Generic public `RadrootsFileMetadata` remains separate from private `RadrootsFarmFileMetadata` even +though both use kind `1063`. The public generic model must cover the current simple NIP-94 tags, +including URL, MIME type, SHA-256 hash, original hash, size, dimensions, blurhash, thumbnail, image, +summary, alt text, fallback, `magnet`, `i`, and `service`. + +`RadrootsCalendarDateEvent`, `RadrootsCalendarTimeEvent`, and `RadrootsCalendar` use NIP-52 +description content. Optional `description` data is encoded as event content and empty content +decodes to no description. Calendar date events use lowercase `d` for the replaceable identifier and +optional uppercase `D` tags for covered all-day dates. Calendar time events require at least one +uppercase `D` tag so timestamped events retain a deterministic calendar-date anchor across codecs. + +Product routing uses surface-specific kind classifiers rather than a broad public-social set. Home, +Events, Market, Map, and Profile public-content candidates are explicit. Active listing kind `30402` +can appear in public product surfaces, but listing draft kind `30403` is limited to draft-owner +contexts. Report kind `1984` is a moderation/admin candidate, not normal feed content. Relay and HTTP +auth kinds are transient and excluded from durable social and farm-ops candidate sets. Private farm +operations candidates include the farm workspace manifest, farm CRDT change envelope, farm file +metadata, and the supported NIP-29 group event subset. + +`RadrootsListingDraft` and `RadrootsRelayList` are not separate model types in the target contract. +Listing draft kind `30403` is represented through `RadrootsListing`, and NIP-51 standard and +list-set entries, including NIP-65 relay metadata kind `10002`, are represented through +`RadrootsList`. + +## Exclusions + +This substrate does not include `RadrootsFeedItem`, `RadrootsMapPin`, NIP-72 community events, +checkout or payment events, or public task, harvest, work-session, approval, or other Field business +document event types. + +Task records, work sessions, harvest records, approvals, and similar Field business objects remain +CRDT document semantics carried inside the CRDT change envelope unless a later contract explicitly +promotes them. + +## Consumer Boundary + +The public social surface is event and codec substrate first. Consumer packages may wrap these +models and codecs, but this repository owns only the core Rust contracts and deterministic +conformance evidence. Package-specific operation maps, bindings, and generated artifacts are outside +this contract boundary. + +## Conformance Boundary + +Every new social codec and every upgraded existing social codec must have deterministic valid and +invalid conformance vectors before closeout. Upgraded vectors must include the strict comment, +reaction, listing, farm, list, and list-set behavior whose public contract changes during the +refactor. + +Social vectors are repo-owned and synthetic. They must not depend on application relay state, local +databases, external services, root fixture catalogs, or ambient machine state. diff --git a/contracts/manifest.toml b/contracts/manifest.toml @@ -0,0 +1,70 @@ +[contract] +name = "radroots_core_contract" +version = "0.1.0-alpha.2" +source = "rust" + +[surface] +model_crates = [ + "radroots_core", + "radroots_events", + "radroots_trade", + "radroots_identity", +] +algorithm_crates = ["radroots_events_codec"] + +[surface.rust_crate_tiers] +advanced_substrate = [ + "radroots_core", + "radroots_events", + "radroots_events_codec", + "radroots_trade", + "radroots_identity", + "radroots_nostr", + "radroots_nostr_connect", + "radroots_nostr_signer", + "radroots_nostr_accounts", + "radroots_secret_vault", + "radroots_protected_store", + "radroots_runtime_paths", +] +published_support = [ + "radroots_log", + "radroots_runtime", + "radroots_runtime_distribution", + "radroots_runtime_manager", + "radroots_geocoder", + "radroots_events_indexed", + "radroots_sql_core", + "radroots_replica_db_schema", + "radroots_replica_db", + "radroots_replica_sync", +] +deferred_publication = [ + "radroots_types", + "radroots_authority", + "radroots_event_store", + "radroots_outbox", + "radroots_relay_transport", + "radroots_net", + "radroots_nostr_runtime", + "radroots_nostr_ndb", + "radroots_simplex_chat_proto", + "radroots_simplex_smp_proto", + "radroots_sp1_guest_trade", + "radroots_sp1_host_trade", +] + +[surface.internal_replica_crates] +schema = "radroots_replica_db_schema" +storage = "radroots_replica_db" +sync = "radroots_replica_sync" + +[policy] +exclude_internal_workspace_crates = true +require_reproducible_exports = true +require_conformance_vectors = true + +[policy.replica] +forbid_legacy_alias_identifiers = true +require_transport_agnostic_sync_contract = true +require_deterministic_emit_ingest = true diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -0,0 +1,402 @@ +[contract] +name = "radroots_core_contract" +version = "0.1.0-alpha.2" +source = "rust" + +[public] +domains = ["profile", "farm", "listing", "order", "trade_validation", "social"] + +[shared_types] +public = [ + "WireEventParts", + "RadrootsFrozenEventDraft", + "RadrootsSignedNostrEvent", + "RadrootsNostrEvent", + "RadrootsNostrEventRef", + "RadrootsNostrEventPtr", + "RadrootsListingAddress", + "RadrootsProfile", + "RadrootsFarm", + "RadrootsListing", + "RadrootsPost", + "RadrootsComment", + "RadrootsReaction", + "RadrootsArticle", + "RadrootsFileMetadata", + "RadrootsCalendarDateEvent", + "RadrootsCalendarTimeEvent", + "RadrootsOrderEnvelope", + "RadrootsOrderEventType", + "RadrootsOrderItem", + "RadrootsOrderPricingBasis", + "RadrootsOrderEconomicLineKind", + "RadrootsOrderEconomicActor", + "RadrootsOrderEconomicEffect", + "RadrootsOrderEconomicItem", + "RadrootsOrderEconomicLine", + "RadrootsOrderEconomicTotals", + "RadrootsOrderEconomics", + "RadrootsOrderRequest", + "RadrootsOrderInventoryCommitment", + "RadrootsOrderDecisionOutcome", + "RadrootsOrderDecision", +] + +[errors] +classes = ["encode_error", "parse_error", "validation_error", "address_error"] + +[implementation_provenance] +model_crates = [ + "radroots_core", + "radroots_events", + "radroots_trade", + "radroots_identity", +] +algorithm_crates = ["radroots_events_codec"] + +[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 = "contracts/conformance/vectors/profile/build_draft.v1.json" + +[operations.farm_build_draft] +domain = "farm" +id = "farm.build_draft" +stability = "beta" +inputs = ["RadrootsFarm"] +outputs = ["WireEventParts"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.farm_build_draft.implementation] +rust_modules = ["crates/events_codec/src/farm/encode.rs"] +rust_types = ["radroots_events::farm::RadrootsFarm"] + +[operations.farm_build_draft.conformance] +vector = "contracts/conformance/vectors/farm/build_draft.v1.json" + +[operations.listing_build_tags] +domain = "listing" +id = "listing.build_tags" +stability = "beta" +inputs = ["RadrootsListing"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.listing_build_tags.implementation] +rust_modules = [ + "crates/events_codec/src/listing/encode.rs", + "crates/events_codec/src/listing/tags.rs", +] +rust_types = ["radroots_events::listing::RadrootsListing"] + +[operations.listing_build_tags.conformance] +vector = "contracts/conformance/vectors/listing/build_tags.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/wire.rs", +] +rust_types = ["radroots_events::listing::RadrootsListing"] + +[operations.listing_build_draft.conformance] +vector = "contracts/conformance/vectors/listing/build_draft.v1.json" + +[operations.listing_parse_event] +domain = "listing" +id = "listing.parse_event" +stability = "beta" +inputs = ["RadrootsNostrEvent"] +outputs = ["RadrootsListing"] +error_class = "parse_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.listing_parse_event.implementation] +rust_modules = ["crates/trade/src/listing/codec.rs"] +rust_types = [ + "radroots_events::RadrootsNostrEvent", + "radroots_events::listing::RadrootsListing", +] + +[operations.listing_parse_event.conformance] +vector = "contracts/conformance/vectors/listing/parse_event.v1.json" + +[operations.social_post_build_tags] +domain = "social" +id = "social.post.build_tags" +stability = "beta" +inputs = ["RadrootsPost"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_post_build_tags.implementation] +rust_modules = ["crates/events_codec/src/post/encode.rs"] +rust_types = ["radroots_events::post::RadrootsPost"] + +[operations.social_post_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_comment_build_tags] +domain = "social" +id = "social.comment.build_tags" +stability = "beta" +inputs = ["RadrootsComment"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_comment_build_tags.implementation] +rust_modules = ["crates/events_codec/src/comment/encode.rs"] +rust_types = ["radroots_events::comment::RadrootsComment"] + +[operations.social_comment_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_reaction_build_tags] +domain = "social" +id = "social.reaction.build_tags" +stability = "beta" +inputs = ["RadrootsReaction"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_reaction_build_tags.implementation] +rust_modules = ["crates/events_codec/src/reaction/encode.rs"] +rust_types = ["radroots_events::reaction::RadrootsReaction"] + +[operations.social_reaction_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_article_build_tags] +domain = "social" +id = "social.article.build_tags" +stability = "beta" +inputs = ["RadrootsArticle"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_article_build_tags.implementation] +rust_modules = ["crates/events_codec/src/article/encode.rs"] +rust_types = ["radroots_events::article::RadrootsArticle"] + +[operations.social_article_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_file_metadata_build_tags] +domain = "social" +id = "social.file_metadata.build_tags" +stability = "beta" +inputs = ["RadrootsFileMetadata"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_file_metadata_build_tags.implementation] +rust_modules = ["crates/events_codec/src/file_metadata/encode.rs"] +rust_types = ["radroots_events::file_metadata::RadrootsFileMetadata"] + +[operations.social_file_metadata_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_calendar_date_event_build_tags] +domain = "social" +id = "social.calendar_date_event.build_tags" +stability = "beta" +inputs = ["RadrootsCalendarDateEvent"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_calendar_date_event_build_tags.implementation] +rust_modules = ["crates/events_codec/src/calendar/encode.rs"] +rust_types = ["radroots_events::calendar::RadrootsCalendarDateEvent"] + +[operations.social_calendar_date_event_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.social_calendar_time_event_build_tags] +domain = "social" +id = "social.calendar_time_event.build_tags" +stability = "beta" +inputs = ["RadrootsCalendarTimeEvent"] +outputs = ["NostrTags"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.social_calendar_time_event_build_tags.implementation] +rust_modules = ["crates/events_codec/src/calendar/encode.rs"] +rust_types = ["radroots_events::calendar::RadrootsCalendarTimeEvent"] + +[operations.social_calendar_time_event_build_tags.conformance] +vector = "contracts/conformance/vectors/social/mvp.v1.json" + +[operations.order_build_order_request_draft] +domain = "order" +id = "order.build_order_request_draft" +stability = "beta" +inputs = ["RadrootsOrderRequest", "RadrootsNostrEventPtr"] +outputs = ["WireEventParts"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.order_build_order_request_draft.implementation] +rust_modules = ["crates/events_codec/src/order/encode.rs"] +rust_types = [ + "radroots_events::RadrootsNostrEventPtr", + "radroots_events::order::RadrootsOrderRequest", +] + +[operations.order_build_order_request_draft.conformance] +vector = "contracts/conformance/vectors/order/build_order_request_draft.v1.json" + +[operations.order_build_order_decision_draft] +domain = "order" +id = "order.build_order_decision_draft" +stability = "beta" +inputs = ["root_event_id", "prev_event_id", "RadrootsOrderDecision"] +outputs = ["WireEventParts"] +error_class = "encode_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.order_build_order_decision_draft.implementation] +rust_modules = ["crates/events_codec/src/order/encode.rs"] +rust_types = ["radroots_events::order::RadrootsOrderDecision"] + +[operations.order_build_order_decision_draft.conformance] +vector = "contracts/conformance/vectors/order/build_order_decision_draft.v1.json" + +[operations.order_parse_order_request] +domain = "order" +id = "order.parse_order_request" +stability = "beta" +inputs = ["RadrootsNostrEvent"] +outputs = ["RadrootsOrderEnvelope", "RadrootsOrderRequest"] +error_class = "parse_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.order_parse_order_request.implementation] +rust_modules = ["crates/events_codec/src/order/decode.rs"] +rust_types = [ + "radroots_events::RadrootsNostrEvent", + "radroots_events::order::RadrootsOrderEnvelope", + "radroots_events::order::RadrootsOrderRequest", +] + +[operations.order_parse_order_request.conformance] +vector = "contracts/conformance/vectors/order/parse_order_request.v1.json" + +[operations.order_parse_order_decision] +domain = "order" +id = "order.parse_order_decision" +stability = "beta" +inputs = ["RadrootsNostrEvent"] +outputs = ["RadrootsOrderEnvelope", "RadrootsOrderDecision"] +error_class = "parse_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.order_parse_order_decision.implementation] +rust_modules = ["crates/events_codec/src/order/decode.rs"] +rust_types = [ + "radroots_events::RadrootsNostrEvent", + "radroots_events::order::RadrootsOrderEnvelope", + "radroots_events::order::RadrootsOrderDecision", +] + +[operations.order_parse_order_decision.conformance] +vector = "contracts/conformance/vectors/order/parse_order_decision.v1.json" + +[operations.order_parse_listing_address] +domain = "order" +id = "order.parse_listing_address" +stability = "beta" +inputs = ["listing_addr"] +outputs = ["RadrootsListingAddress"] +error_class = "address_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.order_parse_listing_address.implementation] +rust_modules = ["crates/events/src/ids.rs"] +rust_types = ["radroots_events::ids::RadrootsListingAddress"] + +[operations.order_parse_listing_address.conformance] +vector = "contracts/conformance/vectors/order/parse_listing_address.v1.json" + +[operations.trade_validation_validate_listing_event] +domain = "trade_validation" +id = "trade_validation.validate_listing_event" +stability = "beta" +inputs = ["RadrootsNostrEvent"] +outputs = ["TradeListingValidateResult"] +error_class = "validation_error" +deterministic = true +signing = "native" +transport = "native" + +[operations.trade_validation_validate_listing_event.implementation] +rust_modules = ["crates/trade/src/listing/validation.rs"] +rust_types = [ + "radroots_events::RadrootsNostrEvent", + "radroots_trade::listing::validation::RadrootsTradeListing", +] + +[operations.trade_validation_validate_listing_event.conformance] +vector = "contracts/conformance/vectors/trade_validation/validate_listing_event.v1.json" diff --git a/contracts/replica.toml b/contracts/replica.toml @@ -0,0 +1,14 @@ +[replica] +name = "radroots_replica_contract" +version = "0.1.0" +purpose = "offline-first local replica state and deterministic sync" + +[crate_family] +schema = "radroots_replica_db_schema" +storage = "radroots_replica_db" +sync = "radroots_replica_sync" + +[policy] +transport_agnostic_sync_core = true +deterministic_emit_and_ingest = true +forbid_legacy_alias_identifiers = true diff --git a/contracts/version.toml b/contracts/version.toml @@ -0,0 +1,30 @@ +[contract] +version = "0.1.0-alpha.2" +stability = "draft" + +[semver] +major_on = [ + "remove_exported_type", + "remove_exported_field", + "change_exported_field_type", + "change_exported_enum_variant", + "change_exported_algorithm_behavior", + "rename_internal_replica_crate_family", +] +minor_on = [ + "add_exported_type", + "add_optional_field", + "add_enum_variant", + "add_conformance_vector", + "add_replica_transport_adapter", +] +patch_on = [ + "fix_docs", + "fix_non_behavioral_contract_metadata", + "fix_packaging_metadata", +] + +[compatibility] +requires_conformance_pass = true +requires_contract_manifest_diff = true +requires_release_notes = true diff --git a/crates/xtask/src/contract.rs b/crates/xtask/src/contract.rs @@ -10,8 +10,8 @@ use std::path::{Path, PathBuf}; const ROOT_RELEASE_POLICY_RELATIVE: &str = "foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml"; -const CONFORMANCE_ROOT_RELATIVE: &str = "spec/conformance"; -const CONFORMANCE_SCHEMA_RELATIVE: &str = "spec/conformance/schema/vector.schema.json"; +const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance"; +const CONFORMANCE_SCHEMA_RELATIVE: &str = "contracts/conformance/schema/vector.schema.json"; const RELEASE_POLICY_ENV: &str = "RADROOTS_MOUNTED_RUST_CRATE_PUBLISH_POLICY"; const EVENT_BOUNDARY_MATRIX_ENV: &str = "RADROOTS_EVENT_BOUNDARY_MATRIX"; const COVERAGE_REQUIRED_THRESHOLD: f64 = 98.0; @@ -30,6 +30,7 @@ pub struct ContractManifest { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ManifestContract { pub name: String, pub version: String, @@ -41,7 +42,6 @@ pub struct ManifestContract { pub struct Surface { pub model_crates: Vec<String>, pub algorithm_crates: Vec<String>, - pub wasm_crates: Vec<String>, pub rust_crate_tiers: Option<RustCrateTiers>, pub internal_replica_crates: Option<InternalReplicaCrates>, } @@ -59,16 +59,24 @@ pub struct RustCrateTiers { pub struct InternalReplicaCrates { pub schema: String, pub storage: String, - pub external_storage_wasm_binding_crate: String, pub sync: String, - pub external_sync_wasm_binding_crate: String, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Policy { pub exclude_internal_workspace_crates: bool, pub require_reproducible_exports: bool, pub require_conformance_vectors: bool, + pub replica: Option<ReplicaPolicy>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ReplicaPolicy { + pub forbid_legacy_alias_identifiers: bool, + pub require_transport_agnostic_sync_contract: bool, + pub require_deterministic_emit_ingest: bool, } #[derive(Debug, Deserialize)] @@ -83,28 +91,32 @@ pub struct OperationsContractManifest { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct PublicContract { pub domains: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct SharedTypesContract { pub public: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ErrorClassesContract { pub classes: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct ImplementationProvenance { pub model_crates: Vec<String>, pub algorithm_crates: Vec<String>, - pub wasm_crates: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct PublicOperationContract { pub domain: String, pub id: String, @@ -121,17 +133,20 @@ pub struct PublicOperationContract { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct PublicOperationImplementation { pub rust_modules: Vec<String>, pub rust_types: Vec<String>, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct PublicOperationConformance { pub vector: String, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct VersionPolicy { pub contract: VersionContract, pub semver: SemverRules, @@ -139,12 +154,14 @@ pub struct VersionPolicy { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct VersionContract { pub version: String, pub stability: String, } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct SemverRules { pub major_on: Vec<String>, pub minor_on: Vec<String>, @@ -152,9 +169,10 @@ pub struct SemverRules { } #[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CompatibilityRules { pub requires_conformance_pass: bool, - pub requires_export_manifest_diff: bool, + pub requires_contract_manifest_diff: bool, pub requires_release_notes: bool, } @@ -1614,7 +1632,7 @@ pub fn validate_canonical_event_boundary(workspace_root: &Path) -> Result<(), St } fn contract_root(workspace_root: &Path) -> PathBuf { - workspace_root.join("spec") + workspace_root.join("contracts") } fn conformance_root(workspace_root: &Path) -> PathBuf { @@ -2089,15 +2107,11 @@ 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(&coverage_root(contract_root).join("policy.toml")) + read_coverage_policy(&coverage_root(contract_root).join("coverage.toml")) } fn coverage_root(contract_root: &Path) -> PathBuf { - contract_root - .parent() - .unwrap_or(contract_root) - .join("policy") - .join("coverage") + contract_root.to_path_buf() } #[cfg_attr(not(test), allow(dead_code))] @@ -2199,11 +2213,13 @@ fn should_synthesize_owner_contracts_for_tests(workspace_root: &Path) -> bool { .join("trade") .join("Cargo.toml") .is_file() - && workspace_root.join("spec").join("manifest.toml").is_file() && workspace_root - .join("policy") - .join("coverage") - .join("policy.toml") + .join("contracts") + .join("manifest.toml") + .is_file() + && workspace_root + .join("contracts") + .join("coverage.toml") .is_file() } @@ -2359,19 +2375,29 @@ fn validate_surface_metadata(surface: &Surface) -> Result<(), String> { validate_crate_identifier(&replica.schema, "surface.internal_replica_crates.schema")?; validate_crate_identifier(&replica.storage, "surface.internal_replica_crates.storage")?; validate_crate_identifier(&replica.sync, "surface.internal_replica_crates.sync")?; - validate_crate_identifier( - &replica.external_storage_wasm_binding_crate, - "surface.internal_replica_crates.external_storage_wasm_binding_crate", - )?; - validate_crate_identifier( - &replica.external_sync_wasm_binding_crate, - "surface.internal_replica_crates.external_sync_wasm_binding_crate", - )?; } Ok(()) } +fn validate_policy_metadata(policy: &Policy) -> Result<(), String> { + if !policy.exclude_internal_workspace_crates + || !policy.require_reproducible_exports + || !policy.require_conformance_vectors + { + return Err("contract policy flags must all be true".to_string()); + } + if let Some(replica) = &policy.replica { + if !replica.forbid_legacy_alias_identifiers + || !replica.require_transport_agnostic_sync_contract + || !replica.require_deterministic_emit_ingest + { + return Err("contract replica policy flags must all be true".to_string()); + } + } + Ok(()) +} + fn validate_operations_contract( bundle: &ContractBundle, operations_manifest: &OperationsContractManifest, @@ -2427,8 +2453,6 @@ fn validate_operations_contract( &bundle.manifest.surface.algorithm_crates, "surface.algorithm_crates", )?; - let manifest_wasm = - collect_unique_set(&bundle.manifest.surface.wasm_crates, "surface.wasm_crates")?; let provenance_models = collect_unique_set( &provenance.model_crates, "implementation_provenance.model_crates", @@ -2437,14 +2461,7 @@ fn validate_operations_contract( &provenance.algorithm_crates, "implementation_provenance.algorithm_crates", )?; - let provenance_wasm = collect_unique_set( - &provenance.wasm_crates, - "implementation_provenance.wasm_crates", - )?; - if provenance_models != manifest_models - || provenance_algorithms != manifest_algorithms - || provenance_wasm != manifest_wasm - { + if provenance_models != manifest_models || provenance_algorithms != manifest_algorithms { return Err( "operations implementation_provenance must match manifest surface crates" .to_string(), @@ -2547,10 +2564,10 @@ fn validate_operations_contract( if !operation .conformance .vector - .starts_with("spec/conformance/") + .starts_with("contracts/conformance/") { return Err(format!( - "operation {} conformance.vector must live under spec/conformance/", + "operation {} conformance.vector must live under contracts/conformance/", operation.id )); } @@ -3325,18 +3342,13 @@ fn validate_contract_bundle_with_release_policy_override( if !bundle.version.compatibility.requires_conformance_pass { return Err("compatibility.requires_conformance_pass must be true".to_string()); } - if !bundle.version.compatibility.requires_export_manifest_diff { - return Err("compatibility.requires_export_manifest_diff must be true".to_string()); + if !bundle.version.compatibility.requires_contract_manifest_diff { + return Err("compatibility.requires_contract_manifest_diff must be true".to_string()); } if !bundle.version.compatibility.requires_release_notes { return Err("compatibility.requires_release_notes must be true".to_string()); } - if !bundle.manifest.policy.exclude_internal_workspace_crates - || !bundle.manifest.policy.require_reproducible_exports - || !bundle.manifest.policy.require_conformance_vectors - { - return Err("contract policy flags must all be true".to_string()); - } + validate_policy_metadata(&bundle.manifest.policy)?; let workspace_root = bundle .root .parent() @@ -3604,6 +3616,7 @@ fn toml_inline_array(values: &[String]) -> String { } pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, String> { + reject_legacy_contract_roots(workspace_root)?; let root = contract_root(workspace_root); let manifest = parse_toml::<ContractManifest>(&root.join("manifest.toml"))?; let version = parse_toml::<VersionPolicy>(&root.join("version.toml"))?; @@ -3623,6 +3636,19 @@ pub fn load_contract_bundle(workspace_root: &Path) -> Result<ContractBundle, Str }) } +fn reject_legacy_contract_roots(workspace_root: &Path) -> Result<(), String> { + for relative in ["spec", "policy"] { + let legacy_root = workspace_root.join(relative); + if legacy_root.exists() { + return Err(format!( + "legacy contract root {} is forbidden; use contracts/", + legacy_root.display() + )); + } + } + Ok(()) +} + pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { if bundle.manifest.contract.name.trim().is_empty() { return Err("contract name is required".to_string()); @@ -3655,18 +3681,13 @@ pub fn validate_contract_bundle(bundle: &ContractBundle) -> Result<(), String> { if !bundle.version.compatibility.requires_conformance_pass { return Err("compatibility.requires_conformance_pass must be true".to_string()); } - if !bundle.version.compatibility.requires_export_manifest_diff { - return Err("compatibility.requires_export_manifest_diff must be true".to_string()); + if !bundle.version.compatibility.requires_contract_manifest_diff { + return Err("compatibility.requires_contract_manifest_diff must be true".to_string()); } if !bundle.version.compatibility.requires_release_notes { return Err("compatibility.requires_release_notes must be true".to_string()); } - if !bundle.manifest.policy.exclude_internal_workspace_crates - || !bundle.manifest.policy.require_reproducible_exports - || !bundle.manifest.policy.require_conformance_vectors - { - return Err("contract policy flags must all be true".to_string()); - } + validate_policy_metadata(&bundle.manifest.policy)?; let workspace_root = bundle .root .parent() @@ -3885,7 +3906,7 @@ publish = false ); write_file( - &root.join("spec").join("manifest.toml"), + &root.join("contracts").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -3894,7 +3915,6 @@ source = "synthetic" [surface] model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] [policy] exclude_internal_workspace_crates = true @@ -3903,7 +3923,7 @@ require_conformance_vectors = true "#, ); write_file( - &root.join("spec").join("version.toml"), + &root.join("contracts").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -3915,12 +3935,12 @@ patch_on = ["fix"] [compatibility] requires_conformance_pass = true -requires_export_manifest_diff = true +requires_contract_manifest_diff = true requires_release_notes = true "#, ); write_file( - &root.join("policy").join("coverage").join("policy.toml"), + &root.join("contracts").join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -3959,7 +3979,7 @@ crates = ["radroots_a"] fn add_operation_contract_files(root: &Path) { write_file( - &root.join("spec").join("operations.toml"), + &root.join("contracts").join("operations.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -3988,7 +4008,6 @@ classes = ["encode_error", "parse_error", "validation_error", "address_error"] [implementation_provenance] model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] [operations.profile_build_draft] domain = "profile" @@ -4006,7 +4025,7 @@ rust_modules = ["crates/core/src/unit.rs"] rust_types = ["radroots_events::profile::RadrootsProfile"] [operations.profile_build_draft.conformance] -vector = "spec/conformance/vectors/profile/build_draft.v1.json" +vector = "contracts/conformance/vectors/profile/build_draft.v1.json" [operations.listing_build_draft] domain = "listing" @@ -4024,19 +4043,19 @@ rust_modules = ["crates/core/src/unit.rs"] rust_types = ["radroots_events::listing::RadrootsListing"] [operations.listing_build_draft.conformance] -vector = "spec/conformance/vectors/listing/build_draft.v1.json" +vector = "contracts/conformance/vectors/listing/build_draft.v1.json" "#, ); write_file( &root - .join("spec") + .join("contracts") .join("conformance") .join("schema") .join("vector.schema.json"), r#"{ "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://radroots.org/sdk/conformance/vector.schema.json", - "title": "radroots sdk conformance vector", + "$id": "https://radroots.org/core/conformance/vector.schema.json", + "title": "radroots core conformance vector", "type": "object", "required": ["suite", "contract_version", "vectors"], "properties": { @@ -4075,7 +4094,7 @@ vector = "spec/conformance/vectors/listing/build_draft.v1.json" ); write_file( &root - .join("spec") + .join("contracts") .join("conformance") .join("vectors") .join("profile") @@ -4096,7 +4115,7 @@ vector = "spec/conformance/vectors/listing/build_draft.v1.json" ); write_file( &root - .join("spec") + .join("contracts") .join("conformance") .join("vectors") .join("listing") @@ -4143,7 +4162,7 @@ publish = false ); } write_file( - &root.join("policy").join("coverage").join("policy.toml"), + &root.join("contracts").join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -4249,7 +4268,7 @@ pub enum RadrootsCoreUnitDimension { let root = workspace_root(); let expected_names = coverage_required_workspace_crates(&root).expect("workspace coverage crates"); - let policy = load_coverage_policy(&root.join("spec")).expect("coverage policy"); + let policy = load_coverage_policy(&root.join("contracts")).expect("coverage policy"); let required_names = policy .required_crates() .expect("required crates") @@ -4321,7 +4340,7 @@ edition = "2024" #[test] fn coverage_required_crates_match_policy_required_status() { let root = workspace_root(); - let contract_root = root.join("spec"); + let contract_root = root.join("contracts"); let policy = load_coverage_policy(&contract_root).expect("coverage policy"); let required = CoverageRequiredFile { required: CoverageRequiredSection { @@ -4346,15 +4365,15 @@ edition = "2024" let missing_root = temp_root("load_coverage_required_missing_policy"); let missing_err = load_coverage_policy(&missing_root).expect_err("missing policy should fail"); - assert!(missing_err.contains("policy.toml")); + assert!(missing_err.contains("coverage.toml")); let _ = fs::remove_dir_all(&missing_root); let duplicate_root = create_synthetic_workspace("load_coverage_required_duplicate_required"); - let contract_root = duplicate_root.join("spec"); + let contract_root = duplicate_root.join("contracts"); let coverage_root = coverage_root(&contract_root); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", ); let duplicate_err = @@ -4461,10 +4480,10 @@ edition = "2024" }, ], ); - let policy_dir = root.join("policy").join("coverage"); + let policy_dir = root.join("contracts"); fs::create_dir_all(&policy_dir).expect("create policy dir"); fs::write( - policy_dir.join("policy.toml"), + policy_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[overrides.radroots_events_codec]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 99.946\nfail_under_branches = 100.0\ntemporary = true\nreason = \"publish 0.1.0-alpha.2 temporary coverage override\"\n\n[overrides.radroots_log]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = false\ntemporary = true\nreason = \"branch coverage is not applicable while the crate has no measured branch records\"\n\n[required]\ncrates = [\"radroots_events_codec\", \"radroots_log\"]\n", ) .expect("write coverage policy"); @@ -4474,7 +4493,7 @@ edition = "2024" ] .into_iter() .collect::<BTreeSet<_>>(); - let policy = read_coverage_policy(&policy_dir.join("policy.toml")) + let policy = read_coverage_policy(&policy_dir.join("coverage.toml")) .expect("parse override coverage policy"); validate_required_coverage_summary_with_policy(&root, &required, &policy) .expect("coverage summary should honor override"); @@ -4494,13 +4513,13 @@ edition = "2024" let required = ["radroots_a".to_string()] .into_iter() .collect::<BTreeSet<_>>(); - let policy_dir = root.join("policy").join("coverage"); + let policy_dir = root.join("contracts"); write_file( - &policy_dir.join("policy.toml"), + &policy_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let policy = - read_coverage_policy(&policy_dir.join("policy.toml")).expect("parse coverage policy"); + read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) .expect_err("synthetic report path should fail"); assert!(err.contains("coverage gate report")); @@ -4524,13 +4543,13 @@ edition = "2024" let required = ["radroots_a".to_string()] .into_iter() .collect::<BTreeSet<_>>(); - let policy_dir = root.join("policy").join("coverage"); + let policy_dir = root.join("contracts"); write_file( - &policy_dir.join("policy.toml"), + &policy_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let policy = - read_coverage_policy(&policy_dir.join("policy.toml")).expect("parse coverage policy"); + read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) .expect_err("stale threshold report should fail"); assert!(err.contains("thresholds do not match policy")); @@ -4563,13 +4582,13 @@ edition = "2024" let required = ["radroots_a".to_string()] .into_iter() .collect::<BTreeSet<_>>(); - let policy_dir = root.join("policy").join("coverage"); + let policy_dir = root.join("contracts"); write_file( - &policy_dir.join("policy.toml"), + &policy_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let policy = - read_coverage_policy(&policy_dir.join("policy.toml")).expect("parse coverage policy"); + read_coverage_policy(&policy_dir.join("coverage.toml")).expect("parse coverage policy"); let err = validate_required_coverage_summary_with_policy(&root, &required, &policy) .expect_err("row and report mismatch should fail"); assert!(err.contains("does not match coverage gate report")); @@ -4930,11 +4949,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("spec"); + let contract_root = root.join("contracts"); let coverage_root = coverage_root(&contract_root); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -4951,7 +4970,7 @@ crates = [] assert!(empty_required.contains("required crates list must not be empty")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 97.0 fail_under_functions = 98.0 @@ -4968,7 +4987,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_gate.contains("98/98/98/98")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 97.0 @@ -4985,7 +5004,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_functions.contains("98/98/98/98")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5002,7 +5021,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_regions.contains("98/98/98/98")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5019,7 +5038,7 @@ crates = ["radroots_a", "radroots_b"] assert!(invalid_branches.contains("98/98/98/98")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5036,7 +5055,7 @@ crates = ["radroots_a", "radroots_a"] assert!(duplicate_required.contains("duplicate crate")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5053,7 +5072,7 @@ crates = ["radroots_a", "radroots_b"] assert!(branches_optional.contains("required branches")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5070,7 +5089,7 @@ crates = ["radroots_b"] assert!(missing_workspace.contains("missing workspace crates")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5092,7 +5111,7 @@ crates = ["unknown"] #[test] fn release_publish_policy_reports_contract_errors() { let root = create_synthetic_workspace("release_policy_errors"); - let contract_root = root.join("spec"); + let contract_root = root.join("contracts"); let release_policy_path = root_release_policy_path(&root); write_file( @@ -5372,14 +5391,12 @@ edition = "2024" bundle.manifest.surface.algorithm_crates.clear(); }); assert_bundle_error( - "surface.internal_replica_crates.external_storage_wasm_binding_crate must be a crate identifier", + "surface.internal_replica_crates.storage must be a crate identifier", |bundle| { bundle.manifest.surface.internal_replica_crates = Some(InternalReplicaCrates { schema: "radroots_replica_db_schema".to_string(), - storage: "radroots_replica_db".to_string(), - external_storage_wasm_binding_crate: "crates/replica_db_wasm".to_string(), + storage: "crates/replica_db".to_string(), sync: "radroots_replica_sync".to_string(), - external_sync_wasm_binding_crate: "radroots_replica_sync_wasm".to_string(), }); }, ); @@ -5405,9 +5422,9 @@ edition = "2024" }, ); assert_bundle_error( - "compatibility.requires_export_manifest_diff must be true", + "compatibility.requires_contract_manifest_diff must be true", |bundle| { - bundle.version.compatibility.requires_export_manifest_diff = false; + bundle.version.compatibility.requires_contract_manifest_diff = false; }, ); assert_bundle_error( @@ -5425,6 +5442,13 @@ edition = "2024" assert_bundle_error("contract policy flags must all be true", |bundle| { bundle.manifest.policy.require_conformance_vectors = false; }); + assert_bundle_error("contract replica policy flags must all be true", |bundle| { + bundle.manifest.policy.replica = Some(ReplicaPolicy { + forbid_legacy_alias_identifiers: false, + require_transport_agnostic_sync_contract: true, + require_deterministic_emit_ingest: true, + }); + }); let _ = fs::remove_dir_all(root); } @@ -5432,7 +5456,7 @@ edition = "2024" #[test] fn load_contract_bundle_rejects_stale_consumer_sdk_tables() { let stale_manifest_root = create_synthetic_workspace("stale_manifest_consumer_sdk"); - let manifest_path = stale_manifest_root.join("spec").join("manifest.toml"); + let manifest_path = stale_manifest_root.join("contracts").join("manifest.toml"); let mut manifest = fs::read_to_string(&manifest_path).expect("manifest"); manifest.push_str( r#" @@ -5449,7 +5473,9 @@ rust_package = "radroots_sdk" let stale_operations_root = create_synthetic_workspace("stale_operations_consumer_sdk"); add_operation_contract_files(&stale_operations_root); - let operations_path = stale_operations_root.join("spec").join("operations.toml"); + let operations_path = stale_operations_root + .join("contracts") + .join("operations.toml"); let mut operations = fs::read_to_string(&operations_path).expect("operations"); operations.push_str( r#" @@ -5466,6 +5492,23 @@ rust_package = "radroots_sdk" } #[test] + fn load_contract_bundle_rejects_legacy_contract_roots() { + let stale_spec_root = create_synthetic_workspace("stale_spec_root"); + fs::create_dir_all(stale_spec_root.join("spec")).expect("create spec root"); + let spec_err = load_contract_bundle(&stale_spec_root).expect_err("stale spec root"); + assert!(spec_err.contains("legacy contract root")); + assert!(spec_err.contains("spec")); + let _ = fs::remove_dir_all(stale_spec_root); + + let stale_policy_root = create_synthetic_workspace("stale_policy_root"); + fs::create_dir_all(stale_policy_root.join("policy")).expect("create policy root"); + let policy_err = load_contract_bundle(&stale_policy_root).expect_err("stale policy root"); + assert!(policy_err.contains("legacy contract root")); + assert!(policy_err.contains("policy")); + let _ = fs::remove_dir_all(stale_policy_root); + } + + #[test] fn validate_contract_bundle_reports_operation_contract_errors() { let root = create_synthetic_workspace("operation_contract_bundle_errors"); add_operation_contract_files(&root); @@ -5503,7 +5546,7 @@ rust_package = "radroots_sdk" add_operation_contract_files(&invalid_vector_root); write_file( &invalid_vector_root - .join("spec") + .join("contracts") .join("conformance") .join("vectors") .join("profile") @@ -5540,7 +5583,7 @@ rust_package = "radroots_sdk" .conformance .vector = "conformance/vectors/profile/build_draft.v1.json".to_string(); let err = validate_contract_bundle(&bundle).expect_err("legacy path should fail"); - assert!(err.contains("must live under spec/conformance/")); + assert!(err.contains("must live under contracts/conformance/")); let _ = fs::remove_dir_all(root); } @@ -5768,7 +5811,7 @@ 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("spec"); + let contract_root = root.join("contracts"); let coverage_root = coverage_root(&contract_root); let release_policy_path = root_release_policy_path(&root); @@ -5779,12 +5822,12 @@ publish = false assert!(policy_workspace_err.contains("Cargo.toml")); let _ = fs::remove_dir_all(&missing_workspace); - let _ = fs::remove_file(coverage_root.join("policy.toml")); + let _ = fs::remove_file(coverage_root.join("coverage.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")); + assert!(policy_load_err.contains("coverage.toml")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -5910,7 +5953,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("spec"); + let contract_root = root.join("contracts"); let missing_override = root.join("missing-release-policy.toml"); let override_err = load_release_contract_with_override( @@ -6018,7 +6061,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("spec"), "1.0.0") + let err = validate_release_publish_policy(&root, &root.join("contracts"), "1.0.0") .expect_err("invalid non-public classification should fail"); assert!(err.contains(expected), "{label} err: {err}"); @@ -6036,7 +6079,7 @@ crates = ["radroots_a"] let invalid_bundle = create_synthetic_workspace("preflight_invalid_bundle"); write_file( - &invalid_bundle.join("spec").join("manifest.toml"), + &invalid_bundle.join("contracts").join("manifest.toml"), r#"[contract] name = "radroots_contract" version = "1.0.0" @@ -6045,7 +6088,6 @@ source = "synthetic" [surface] model_crates = ["radroots_a"] algorithm_crates = ["radroots_b"] -wasm_crates = ["radroots_a_wasm"] [policy] exclude_internal_workspace_crates = false @@ -6066,15 +6108,10 @@ require_conformance_vectors = true let _ = fs::remove_dir_all(&missing_release); let missing_required = create_synthetic_workspace("preflight_missing_required"); - let _ = fs::remove_file( - missing_required - .join("policy") - .join("coverage") - .join("policy.toml"), - ); + let _ = fs::remove_file(missing_required.join("contracts").join("coverage.toml")); let missing_required_err = validate_release_preflight(&missing_required).expect_err("missing required list"); - assert!(missing_required_err.contains("policy.toml")); + assert!(missing_required_err.contains("coverage.toml")); let _ = fs::remove_dir_all(&missing_required); let duplicate_publish = create_synthetic_workspace("preflight_duplicate_publish"); @@ -6100,10 +6137,7 @@ crates = ["radroots_a"] let duplicate_required = create_synthetic_workspace("preflight_duplicate_required"); write_file( - &duplicate_required - .join("policy") - .join("coverage") - .join("policy.toml"), + &duplicate_required.join("contracts").join("coverage.toml"), "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_a\"]\n", ); let duplicate_required_err = @@ -6143,12 +6177,12 @@ edition = "2024" #[test] fn load_contract_bundle_and_validation_report_version_core_and_coverage_errors() { let root = create_synthetic_workspace("bundle_version_core_and_coverage_errors"); - write_file(&root.join("spec").join("version.toml"), "[contract"); + write_file(&root.join("contracts").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("spec").join("version.toml"), + &root.join("contracts").join("version.toml"), r#"[contract] version = "1.0.0" stability = "alpha" @@ -6160,7 +6194,7 @@ patch_on = ["fix"] [compatibility] requires_conformance_pass = true -requires_export_manifest_diff = true +requires_contract_manifest_diff = true requires_release_notes = true "#, ); @@ -6187,7 +6221,7 @@ Volume, "#, ); write_file( - &root.join("policy").join("coverage").join("policy.toml"), + &root.join("contracts").join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -6345,12 +6379,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("spec"); + let contract_root = root.join("contracts"); let coverage_root = coverage_root(&contract_root); let release_policy_path = root_release_policy_path(&root); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 @@ -6367,7 +6401,7 @@ crates = ["radroots_a", "radroots_b", "radroots_extra"] assert!(coverage_extra.contains("includes excluded or unknown crates")); write_file( - &coverage_root.join("policy.toml"), + &coverage_root.join("coverage.toml"), r#"[gate] fail_under_exec_lines = 98.0 fail_under_functions = 98.0 diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -596,7 +596,7 @@ fn validate_override_threshold( } pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf { - root.join("policy").join("coverage").join("policy.toml") + root.join("contracts").join("coverage.toml") } pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> { @@ -702,9 +702,8 @@ fn read_coverage_profile( crate_name: &str, ) -> Result<CoverageProfile, String> { let path = workspace_root - .join("policy") - .join("coverage") - .join("profiles.toml"); + .join("contracts") + .join("coverage-profiles.toml"); if !path.exists() { return Ok(CoverageProfile { no_default_features: false, @@ -2354,10 +2353,10 @@ mod tests { #[test] fn report_missing_gate_uses_policy_thresholds() { let root = temp_dir_path("report_missing_gate_root"); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let out_path = root.join("gate-report.json"); @@ -2398,10 +2397,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", ); let out_path = root.join("gate-report.json"); @@ -2491,10 +2490,10 @@ mod tests { .expect_err("missing policy should fail"); assert!(policy_err.contains("failed to read coverage policy")); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let out_path = root.join("gate-report.json"); @@ -2519,10 +2518,10 @@ mod tests { #[test] fn refresh_summary_uses_measured_gate_report_values() { let root = temp_dir_path("refresh_summary_root"); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n", ); @@ -2638,10 +2637,10 @@ 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("policy").join("coverage"); + let defaults_coverage_dir = defaults_root.join("contracts"); fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir"); write_file( - &defaults_coverage_dir.join("policy.toml"), + &defaults_coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); write_file( @@ -2703,10 +2702,10 @@ mod tests { ); let dispatch_root = temp_dir_path("refresh_summary_parentless_root"); - let dispatch_coverage_dir = dispatch_root.join("policy").join("coverage"); + let dispatch_coverage_dir = dispatch_root.join("contracts"); fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir"); write_file( - &dispatch_coverage_dir.join("policy.toml"), + &dispatch_coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); write_file( @@ -2791,10 +2790,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); write_file( @@ -2876,10 +2875,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); write_file( @@ -2947,10 +2946,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); write_file( @@ -3035,10 +3034,10 @@ mod tests { .expect_err("missing policy should fail"); assert!(policy_err.contains("failed to read coverage policy")); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", ); let gate_err = run_with_root( @@ -3205,10 +3204,10 @@ mod tests { #[test] fn coverage_profiles_merge_defaults_and_crate_overrides() { let root = temp_dir_path("profile_merge"); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( - coverage_dir.join("profiles.toml"), + coverage_dir.join("coverage-profiles.toml"), r#"[profiles.default] no_default_features = false features = ["std"] @@ -3237,10 +3236,10 @@ features = ["rt"] #[test] fn coverage_profiles_accept_positive_test_threads() { let root = temp_dir_path("profile_positive_threads"); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( - coverage_dir.join("profiles.toml"), + coverage_dir.join("coverage-profiles.toml"), r#"[profiles.crates."radroots_log"] test_threads = 4 "#, @@ -3255,10 +3254,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( - coverage_dir.join("profiles.toml"), + coverage_dir.join("coverage-profiles.toml"), r#"[profiles.crates."radroots_log"] features = [""] test_threads = 0 @@ -3278,10 +3277,13 @@ test_threads = 0 #[test] fn coverage_profiles_reject_invalid_toml() { let root = temp_dir_path("profile_invalid_toml"); - let coverage_dir = root.join("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write(coverage_dir.join("profiles.toml"), "[profiles.default\n") - .expect("write invalid profiles"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + "[profiles.default\n", + ) + .expect("write invalid profiles"); let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid toml"); assert!(err.contains("failed to parse")); fs::remove_dir_all(root).expect("remove root"); @@ -3290,10 +3292,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); fs::write( - coverage_dir.join("profiles.toml"), + coverage_dir.join("coverage-profiles.toml"), r#"[profiles.crates."radroots_log"] test_threads = 0 "#, @@ -4084,9 +4086,8 @@ test_threads = 0 write_minimal_workspace(&profile_root); write_file( &profile_root - .join("policy") - .join("coverage") - .join("profiles.toml"), + .join("contracts") + .join("coverage-profiles.toml"), "[profiles.default]\nfeatures = [\"\"]\n", ); let profile_args = vec![ @@ -4256,10 +4257,10 @@ 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("policy").join("coverage"); + let coverage_dir = root.join("contracts"); fs::create_dir_all(&coverage_dir).expect("create coverage dir"); write_file( - &coverage_dir.join("policy.toml"), + &coverage_dir.join("coverage.toml"), "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", ); diff --git a/policy/coverage/POLICY.md b/policy/coverage/POLICY.md @@ -1,50 +0,0 @@ -# Radroots Core Libraries Rust Coverage Policy - -This document defines the required coverage gate for the Radroots Core Libraries Rust workspace. -The authoritative machine-readable contract is `policy/coverage/policy.toml`. - -## gate contract - -- executable lines coverage: 98.0 -- function coverage: 98.0 -- region coverage: 98.0 -- branch coverage: 98.0 -- branch records must be present in lcov data unless a crate-specific policy override marks branch coverage as not applicable - -All four thresholds are release-blocking for required crates. This is the -heavy-development coverage gate, not a 100% coverage requirement. - -Coverage work should prioritize required behavior, protocol contracts, -conformance vectors, parsing, validation, and state-transition invariants. -Do not add low-value tests solely to chase crate-wide 100% coverage. - -## toolchain contract - -- use nightly rust for coverage runs -- use `cargo llvm-cov` with `--branch` -- generate json summary and lcov reports for each run -- evaluate coverage using deterministic parsing rules - -## enforcement contract - -- run coverage checks per crate, not only aggregate workspace totals -- a crate cannot be promoted to required unless it satisfies the active gate -- once required, the crate remains blocking on every canonical release-preflight run and any external automation that wraps that run -- `coverage-refresh.tsv` must be generated from measured per-crate gate reports, not from synthetic pass rows -- temporary threshold overrides below 98/98/98/98 are not part of the active gate -- branch-record presence overrides are allowed only for crates whose coverage report has no measured branch records; when branch records exist, the active branch threshold remains binding - -## required crate contract - -- every workspace crate is required except SimpleX crates -- the required blocking crate list is tracked in `policy/coverage/policy.toml` -- workspace membership changes must update `policy/coverage/policy.toml` in the same change -- crates are not expected to reach 100% coverage during heavy development - -## local override policy - -Local override env vars may exist for smoke runs, but canonical release and coverage lanes must read the gate from `policy/coverage/policy.toml`. - -## toolchain pin - -The pinned nightly used for coverage lives in `rust-toolchain-coverage.toml`. diff --git a/spec/RCLD.md b/spec/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 `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` -- downstream package metadata 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`, `order`, and `trade_validation` -- 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 order 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` -- `order.build_order_request_draft` -- `order.parse_order_request` -- `order.parse_listing_address` -- `trade_validation.validate_listing_event` - -### 2. Shared Types - -Public cross-operation types required for operation inputs and outputs. - -Examples: - -- `WireEventParts` -- `RadrootsFrozenEventDraft` -- `RadrootsSignedNostrEvent` -- `RadrootsNostrEvent` -- `RadrootsNostrEventRef` -- `RadrootsListingAddress` - -### 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 -- order 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 by SDK-owned wasm bindings - -## Tier 1 Domains And Operations - -The initial approved public domains are `profile`, `farm`, `listing`, `order`, and `trade_validation`. - -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 `RadrootsFrozenEventDraft` 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 `RadrootsFrozenEventDraft` - -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 - -#### `order.build_order_request_draft` - -Purpose: produce an unsigned order envelope event from typed order payload input - -Rust implementation sources: - -- `crates/events_codec/src/order/encode.rs` - -Input: - -- recipient pubkey -- order event type -- listing address -- optional order id -- optional listing event pointer -- optional root event id -- optional previous event id -- typed order payload - -Output: - -- `WireEventParts` - -Determinism: - -- required - -#### `order.parse_order_request` - -Purpose: parse a order event into a typed order envelope - -Rust implementation sources: - -- `crates/events_codec/src/order/decode.rs` - -Input: - -- `RadrootsNostrEvent` - -Output: - -- typed `RadrootsOrderEnvelope<T>` - -Determinism: - -- required - -#### `order.parse_listing_address` - -Purpose: parse and validate the canonical listing address used by trade flows - -Rust implementation sources: - -- `crates/events/src/ids.rs` - -Input: - -- listing address string - -Output: - -- `RadrootsListingAddress` - -Determinism: - -- required - -#### `trade_validation.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` -- `RadrootsFrozenEventDraft` -- `RadrootsSignedNostrEvent` -- `RadrootsNostrEvent` -- `RadrootsNostrEventRef` -- `RadrootsNostrEventPtr` -- `RadrootsListingAddress` -- public model types required by Tier 1 operations: -- `RadrootsProfile` -- `RadrootsFarm` -- `RadrootsListing` -- trade payload and envelope types required by approved trade operations - -`RadrootsFrozenEventDraft` should be a public frozen draft contract type in `crates/events/src/draft.rs`, with `crates/events_codec/src/wire.rs` adapting codec output into the shared type. The public naming should emphasize frozen 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", - "RadrootsFrozenEventDraft", - "RadrootsSignedNostrEvent", - "RadrootsNostrEvent", - "RadrootsNostrEventRef", - "RadrootsNostrEventPtr", - "RadrootsListingAddress", - "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 = "spec/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 = "spec/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 = [] -``` - -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" -"order.build_order_request_draft" = "order.buildOrderRequestDraft" -"order.parse_order_request" = "order.parseOrderRequest" -"order.parse_listing_address" = "order.parseListingAddress" -"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent" - -[shared_types] -"WireEventParts" = "WireEventParts" -"RadrootsFrozenEventDraft" = "RadrootsFrozenEventDraft" -"RadrootsSignedNostrEvent" = "RadrootsSignedNostrEvent" -"RadrootsNostrEvent" = "RadrootsNostrEvent" -"RadrootsListingAddress" = "ListingAddress" - -[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 -spec/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_order_request_draft.v1.json - parse_order_request.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 draft naming such as `RadrootsFrozenEventDraft` -- 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 -- retains TypeScript package metadata as part of the spec surface -- no longer owns downstream SDK packaging or repo-sync orchestration - -### 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 -- SDK package manifests mapping approved operations -4. replace crate-coverage assertions with operation-coverage assertions -5. keep current crate provenance checks only as implementation validation - -### Recommended `xtask` Command Evolution - -Keep only the validation commands in the mounted repo: - -- `sdk validate` - -Add migration-aware behavior behind the retained validation command: - -- `sdk validate` validates both old and new contract surfaces - -Optional additive commands: - -- `sdk validate-operations` -- `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 - -- move TypeScript package assembly to the owning downstream SDK repo or monorepo control plane -- ship one main external TypeScript SDK package -- keep only the spec metadata needed to describe that package here - -### 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 @@ -1,213 +0,0 @@ -# radroots_core_contract - -Core wire, event, codec, and replica contract for Rad Roots SDK consumers. - -## Purpose - -This directory defines the rr-rs core contract consumed by the first-class -Rad Roots SDK repository. It keeps Rust event models, wire codecs, replica core -semantics, conformance vectors, and release governance deterministic and -machine-verifiable. - -## Contract Surface - -Core contract metadata is defined in `spec/manifest.toml` and currently includes: - -- model crates: `radroots_core`, `radroots_events`, `radroots_trade`, `radroots_identity` -- algorithm crate: `radroots_events_codec` - -The first-class Rust SDK and WebAssembly package surfaces are owned outside -rr-rs by the SDK repository. The crate list above records rr-rs implementation -provenance for the core contract surface; it is not a promise that every listed -crate is a first-class end-user SDK package. - -`spec/manifest.toml` and `spec/operations.toml` do not carry consumer SDK -ownership tables. Curated language package authority lives under -`spec/sdk-exports/`; SDK-owned Rust and WebAssembly package assembly lives in -the SDK repository. - -Public SDK exports are intentionally narrower than the full Rust workspace. - -## Field Event Substrate - -Field-oriented farming operations are represented in the public Rust substrate -through `radroots_events` and `radroots_events_codec`. - -The substrate includes workspace manifests, CRDT change envelopes, farm file -metadata, NIP-42 relay auth, NIP-98 HTTP auth, and the supported NIP-29 group -event subset covering `9000`, `9001`, `9002`, `9005`, `9007`, `9008`, `9009`, -`9021`, `9022`, `39000`, `39001`, `39002`, and `39003`. These are event and -codec APIs, not curated SDK operations by default. The active NIP-29 subset uses -bare metadata markers, `supported_kinds`, and `code` tags for invite and join -flows, and preserves optional user management and moderation reason content; -LiveKit room metadata and live participant state are deferred. - -Task records, work sessions, harvest records, approvals, and similar Field -business objects are CRDT document semantics carried inside the CRDT change -envelope. They are outside the `rr-rs` event-contract boundary unless a future -contract slice explicitly promotes them into a curated SDK operation surface with -matching conformance vectors and language export mappings. - -## Public Social Event Substrate - -Public social events are represented as event and codec substrate in -`radroots_events` and `radroots_events_codec`. - -The active social-event contract is defined in `spec/social-events.md`. It covers -ordinary posts, comments, reactions, articles, public generic file metadata, -calendar events, reposts, reports, listing drafts through `RadrootsListing`, and -NIP-65 relay lists through `RadrootsList`. - -The social surface is substrate-first. MVP social tag builders for posts, -comments, reactions, articles, generic public file metadata, calendar date -events, and calendar time events are promoted into curated SDK operation -metadata after their Rust models, codecs, SDK-owned wasm helpers, and -deterministic conformance vectors exist. Production-v1 repost, report, calendar -collection, and RSVP behavior remains available through event and codec APIs by -default and is covered by conformance vectors. - -## Rust Crate Tiers - -The public Rust story is tiered explicitly. - -- Advanced substrate crates: - - `radroots_core` - - `radroots_events` - - `radroots_events_codec` - - `radroots_trade` - - `radroots_identity` - - `radroots_nostr` - - `radroots_nostr_connect` - - `radroots_nostr_signer` - - `radroots_nostr_accounts` - - `radroots_secret_vault` - - `radroots_protected_store` - - `radroots_runtime_paths` -- Published support crates: - - `radroots_log` - - `radroots_runtime` - - `radroots_runtime_distribution` - - `radroots_runtime_manager` - - `radroots_geocoder` - - `radroots_events_indexed` - - `radroots_sql_core` - - `radroots_replica_db_schema` - - `radroots_replica_db` - - `radroots_replica_sync` -- Deferred crates.io publication: - - `radroots_types` - - `radroots_authority` - - `radroots_event_store` - - `radroots_outbox` - - `radroots_relay_transport` - - `radroots_net` - - `radroots_nostr_runtime` - - `radroots_nostr_ndb` - - `radroots_sql_wasm_bridge` - - `radroots_sql_wasm_core` - - `radroots_simplex_chat_proto` - - `radroots_simplex_smp_proto` - -This tiering is the curated product posture for crates.io. A crate may remain -open source and part of the `rr-rs` workspace without being a recommended -external SDK entrypoint or an active crates.io publication target. - -## Export Targets - -Language export metadata is split into two layers: - -- `spec/sdk-exports/`: curated public SDK package definitions, operation maps, - and shared-type maps -- `spec/exports/`: lower-level package and artifact provenance mappings used by - tooling and generated artifact layout - -Curated public SDK package definitions are defined under `spec/sdk-exports/`: - -- `spec/sdk-exports/ts.toml` -- `spec/sdk-exports/swift.toml` -- `spec/sdk-exports/kotlin.toml` -- `spec/sdk-exports/py.toml` -- `spec/sdk-exports/go.toml` - -Lower-level language package mappings and artifact layout rules remain defined -under `spec/exports/`: - -- `spec/exports/ts.toml` -- `spec/exports/py.toml` -- `spec/exports/swift.toml` -- `spec/exports/kotlin.toml` -- `spec/exports/go.toml` - -The `sdk-exports` files are the authoritative public package model. -The `exports` files remain the lower-level substrate and artifact mapping layer. -For every language target, that lower-level provenance must still resolve to -the same single curated SDK package defined in `sdk-exports/`, rather than a -crate-mirrored package set. - -Rollout order is also explicit in `sdk-exports/`: - -- TypeScript is active now -- Swift and Kotlin are next -- Python and Go remain deferred until the Rust and TypeScript lines are proven - -## 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_sync` - -External SDK-owned wasm binding artifact identifiers for replica storage and -sync are recorded in `spec/replica.toml`. They are provenance identifiers, not -local rr-rs crate paths or runnable package commands. - -## Governance - -Versioning and compatibility policy is defined in `spec/version.toml`. -Contract evolution is semver-governed and requires conformance updates, export target validation, and release notes. - -Repository guards also enforce: - -- deterministic export requirements -- strict no-legacy identifier policy for replica surfaces - -## 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` - -Every non-SimpleX workspace crate is gated at `98/98/98/98` (exec lines, -functions, regions, branches), with branch records required unless a -crate-specific policy override marks branch coverage as not applicable and the -measured report confirms that no branch records exist. Crates are not expected -to reach 100% coverage during heavy development. Temporary crate-specific -threshold overrides below `98/98/98/98` are not part of the active gate, and -branch-record presence overrides must remain explicit in the machine-readable -policy. - -## Release Policy - -Release crate classification and publish order are defined in the owning monorepo at -`foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml`. -Operator workflow is root-owned and documented in: - -- `docs/operations/runbooks/mounted-rust-crate-release.md` -- `docs/operations/runbooks/mounted-rust-crate-release-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/spec/conformance/README.md b/spec/conformance/README.md @@ -1,33 +0,0 @@ -# conformance vectors - -Conformance vectors define canonical cross-language expectations for the Rad Roots SDK contract. - -Each fixture must be deterministic and machine-readable. - -## layout - -- `schema/vector.schema.json`: json schema for vector documents -- `vectors/events/*.json`: event model and tag/codec vectors -- `vectors/trade/*.json`: trade model and transform vectors -- `vectors/identity/*.json`: identity model vectors - -## rules - -- vectors are generated from canonical rust implementations. -- every contract behavior change must update vectors in the same change. -- language sdk test harnesses must validate vectors without local overrides. - -## social event vectors - -Social event vectors are required for every new social codec and every existing -codec whose public behavior changes under `spec/social-events.md`. - -The social vector set must cover valid, invalid, and round-trip behavior for the -approved public social event families. It must include strict NIP-22 comment -targets, empty-content NIP-25 reactions, public NIP-94 generic file metadata, -NIP-99 listing `published_at`, NIP-65 relay-list `r` tags, and private Field -business-document isolation. - -Social event vectors must be deterministic, synthetic, and repo-owned. They must -not depend on relay databases, application runtime state, external services, or -fixture roots outside this repository. diff --git a/spec/conformance/schema/vector.schema.json b/spec/conformance/schema/vector.schema.json @@ -1,47 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://radroots.org/sdk/conformance/vector.schema.json", - "title": "radroots sdk conformance vector", - "type": "object", - "required": [ - "suite", - "contract_version", - "vectors" - ], - "properties": { - "suite": { - "type": "string", - "minLength": 1 - }, - "contract_version": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" - }, - "vectors": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "kind", - "input", - "expected" - ], - "properties": { - "id": { - "type": "string", - "minLength": 1 - }, - "kind": { - "type": "string", - "minLength": 1 - }, - "input": {}, - "expected": {} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false -} diff --git a/spec/manifest.toml b/spec/manifest.toml @@ -1,73 +0,0 @@ -[contract] -name = "radroots_core_contract" -version = "0.1.0-alpha.2" -source = "rust" - -[surface] -model_crates = [ - "radroots_core", - "radroots_events", - "radroots_trade", - "radroots_identity", -] -algorithm_crates = ["radroots_events_codec"] -wasm_crates = [] - -[surface.rust_crate_tiers] -advanced_substrate = [ - "radroots_core", - "radroots_events", - "radroots_events_codec", - "radroots_trade", - "radroots_identity", - "radroots_nostr", - "radroots_nostr_connect", - "radroots_nostr_signer", - "radroots_nostr_accounts", - "radroots_secret_vault", - "radroots_protected_store", - "radroots_runtime_paths", -] -published_support = [ - "radroots_log", - "radroots_runtime", - "radroots_runtime_distribution", - "radroots_runtime_manager", - "radroots_geocoder", - "radroots_events_indexed", - "radroots_sql_core", - "radroots_replica_db_schema", - "radroots_replica_db", - "radroots_replica_sync", -] -deferred_publication = [ - "radroots_types", - "radroots_authority", - "radroots_event_store", - "radroots_outbox", - "radroots_relay_transport", - "radroots_net", - "radroots_nostr_runtime", - "radroots_nostr_ndb", - "radroots_simplex_chat_proto", - "radroots_simplex_smp_proto", - "radroots_sp1_guest_trade", - "radroots_sp1_host_trade", -] - -[surface.internal_replica_crates] -schema = "radroots_replica_db_schema" -storage = "radroots_replica_db" -external_storage_wasm_binding_crate = "radroots_replica_db_wasm" -sync = "radroots_replica_sync" -external_sync_wasm_binding_crate = "radroots_replica_sync_wasm" - -[policy] -exclude_internal_workspace_crates = true -require_reproducible_exports = true -require_conformance_vectors = true - -[policy.replica] -forbid_legacy_alias_identifiers = true -require_transport_agnostic_sync_contract = true -require_deterministic_emit_ingest = true diff --git a/spec/operations.toml b/spec/operations.toml @@ -1,403 +0,0 @@ -[contract] -name = "radroots_core_contract" -version = "0.1.0-alpha.2" -source = "rust" - -[public] -domains = ["profile", "farm", "listing", "order", "trade_validation", "social"] - -[shared_types] -public = [ - "WireEventParts", - "RadrootsFrozenEventDraft", - "RadrootsSignedNostrEvent", - "RadrootsNostrEvent", - "RadrootsNostrEventRef", - "RadrootsNostrEventPtr", - "RadrootsListingAddress", - "RadrootsProfile", - "RadrootsFarm", - "RadrootsListing", - "RadrootsPost", - "RadrootsComment", - "RadrootsReaction", - "RadrootsArticle", - "RadrootsFileMetadata", - "RadrootsCalendarDateEvent", - "RadrootsCalendarTimeEvent", - "RadrootsOrderEnvelope", - "RadrootsOrderEventType", - "RadrootsOrderItem", - "RadrootsOrderPricingBasis", - "RadrootsOrderEconomicLineKind", - "RadrootsOrderEconomicActor", - "RadrootsOrderEconomicEffect", - "RadrootsOrderEconomicItem", - "RadrootsOrderEconomicLine", - "RadrootsOrderEconomicTotals", - "RadrootsOrderEconomics", - "RadrootsOrderRequest", - "RadrootsOrderInventoryCommitment", - "RadrootsOrderDecisionOutcome", - "RadrootsOrderDecision", -] - -[errors] -classes = ["encode_error", "parse_error", "validation_error", "address_error"] - -[implementation_provenance] -model_crates = [ - "radroots_core", - "radroots_events", - "radroots_trade", - "radroots_identity", -] -algorithm_crates = ["radroots_events_codec"] -wasm_crates = [] - -[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 = "spec/conformance/vectors/profile/build_draft.v1.json" - -[operations.farm_build_draft] -domain = "farm" -id = "farm.build_draft" -stability = "beta" -inputs = ["RadrootsFarm"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.farm_build_draft.implementation] -rust_modules = ["crates/events_codec/src/farm/encode.rs"] -rust_types = ["radroots_events::farm::RadrootsFarm"] - -[operations.farm_build_draft.conformance] -vector = "spec/conformance/vectors/farm/build_draft.v1.json" - -[operations.listing_build_tags] -domain = "listing" -id = "listing.build_tags" -stability = "beta" -inputs = ["RadrootsListing"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.listing_build_tags.implementation] -rust_modules = [ - "crates/events_codec/src/listing/encode.rs", - "crates/events_codec/src/listing/tags.rs", -] -rust_types = ["radroots_events::listing::RadrootsListing"] - -[operations.listing_build_tags.conformance] -vector = "spec/conformance/vectors/listing/build_tags.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/wire.rs", -] -rust_types = ["radroots_events::listing::RadrootsListing"] - -[operations.listing_build_draft.conformance] -vector = "spec/conformance/vectors/listing/build_draft.v1.json" - -[operations.listing_parse_event] -domain = "listing" -id = "listing.parse_event" -stability = "beta" -inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsListing"] -error_class = "parse_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.listing_parse_event.implementation] -rust_modules = ["crates/trade/src/listing/codec.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEvent", - "radroots_events::listing::RadrootsListing", -] - -[operations.listing_parse_event.conformance] -vector = "spec/conformance/vectors/listing/parse_event.v1.json" - -[operations.social_post_build_tags] -domain = "social" -id = "social.post.build_tags" -stability = "beta" -inputs = ["RadrootsPost"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_post_build_tags.implementation] -rust_modules = ["crates/events_codec/src/post/encode.rs"] -rust_types = ["radroots_events::post::RadrootsPost"] - -[operations.social_post_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_comment_build_tags] -domain = "social" -id = "social.comment.build_tags" -stability = "beta" -inputs = ["RadrootsComment"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_comment_build_tags.implementation] -rust_modules = ["crates/events_codec/src/comment/encode.rs"] -rust_types = ["radroots_events::comment::RadrootsComment"] - -[operations.social_comment_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_reaction_build_tags] -domain = "social" -id = "social.reaction.build_tags" -stability = "beta" -inputs = ["RadrootsReaction"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_reaction_build_tags.implementation] -rust_modules = ["crates/events_codec/src/reaction/encode.rs"] -rust_types = ["radroots_events::reaction::RadrootsReaction"] - -[operations.social_reaction_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_article_build_tags] -domain = "social" -id = "social.article.build_tags" -stability = "beta" -inputs = ["RadrootsArticle"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_article_build_tags.implementation] -rust_modules = ["crates/events_codec/src/article/encode.rs"] -rust_types = ["radroots_events::article::RadrootsArticle"] - -[operations.social_article_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_file_metadata_build_tags] -domain = "social" -id = "social.file_metadata.build_tags" -stability = "beta" -inputs = ["RadrootsFileMetadata"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_file_metadata_build_tags.implementation] -rust_modules = ["crates/events_codec/src/file_metadata/encode.rs"] -rust_types = ["radroots_events::file_metadata::RadrootsFileMetadata"] - -[operations.social_file_metadata_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_calendar_date_event_build_tags] -domain = "social" -id = "social.calendar_date_event.build_tags" -stability = "beta" -inputs = ["RadrootsCalendarDateEvent"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_calendar_date_event_build_tags.implementation] -rust_modules = ["crates/events_codec/src/calendar/encode.rs"] -rust_types = ["radroots_events::calendar::RadrootsCalendarDateEvent"] - -[operations.social_calendar_date_event_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.social_calendar_time_event_build_tags] -domain = "social" -id = "social.calendar_time_event.build_tags" -stability = "beta" -inputs = ["RadrootsCalendarTimeEvent"] -outputs = ["NostrTags"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.social_calendar_time_event_build_tags.implementation] -rust_modules = ["crates/events_codec/src/calendar/encode.rs"] -rust_types = ["radroots_events::calendar::RadrootsCalendarTimeEvent"] - -[operations.social_calendar_time_event_build_tags.conformance] -vector = "spec/conformance/vectors/social/mvp.v1.json" - -[operations.order_build_order_request_draft] -domain = "order" -id = "order.build_order_request_draft" -stability = "beta" -inputs = ["RadrootsOrderRequest", "RadrootsNostrEventPtr"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.order_build_order_request_draft.implementation] -rust_modules = ["crates/events_codec/src/order/encode.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEventPtr", - "radroots_events::order::RadrootsOrderRequest", -] - -[operations.order_build_order_request_draft.conformance] -vector = "spec/conformance/vectors/order/build_order_request_draft.v1.json" - -[operations.order_build_order_decision_draft] -domain = "order" -id = "order.build_order_decision_draft" -stability = "beta" -inputs = ["root_event_id", "prev_event_id", "RadrootsOrderDecision"] -outputs = ["WireEventParts"] -error_class = "encode_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.order_build_order_decision_draft.implementation] -rust_modules = ["crates/events_codec/src/order/encode.rs"] -rust_types = ["radroots_events::order::RadrootsOrderDecision"] - -[operations.order_build_order_decision_draft.conformance] -vector = "spec/conformance/vectors/order/build_order_decision_draft.v1.json" - -[operations.order_parse_order_request] -domain = "order" -id = "order.parse_order_request" -stability = "beta" -inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsOrderEnvelope", "RadrootsOrderRequest"] -error_class = "parse_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.order_parse_order_request.implementation] -rust_modules = ["crates/events_codec/src/order/decode.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEvent", - "radroots_events::order::RadrootsOrderEnvelope", - "radroots_events::order::RadrootsOrderRequest", -] - -[operations.order_parse_order_request.conformance] -vector = "spec/conformance/vectors/order/parse_order_request.v1.json" - -[operations.order_parse_order_decision] -domain = "order" -id = "order.parse_order_decision" -stability = "beta" -inputs = ["RadrootsNostrEvent"] -outputs = ["RadrootsOrderEnvelope", "RadrootsOrderDecision"] -error_class = "parse_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.order_parse_order_decision.implementation] -rust_modules = ["crates/events_codec/src/order/decode.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEvent", - "radroots_events::order::RadrootsOrderEnvelope", - "radroots_events::order::RadrootsOrderDecision", -] - -[operations.order_parse_order_decision.conformance] -vector = "spec/conformance/vectors/order/parse_order_decision.v1.json" - -[operations.order_parse_listing_address] -domain = "order" -id = "order.parse_listing_address" -stability = "beta" -inputs = ["listing_addr"] -outputs = ["RadrootsListingAddress"] -error_class = "address_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.order_parse_listing_address.implementation] -rust_modules = ["crates/events/src/ids.rs"] -rust_types = ["radroots_events::ids::RadrootsListingAddress"] - -[operations.order_parse_listing_address.conformance] -vector = "spec/conformance/vectors/order/parse_listing_address.v1.json" - -[operations.trade_validation_validate_listing_event] -domain = "trade_validation" -id = "trade_validation.validate_listing_event" -stability = "beta" -inputs = ["RadrootsNostrEvent"] -outputs = ["TradeListingValidateResult"] -error_class = "validation_error" -deterministic = true -signing = "native" -transport = "native" - -[operations.trade_validation_validate_listing_event.implementation] -rust_modules = ["crates/trade/src/listing/validation.rs"] -rust_types = [ - "radroots_events::RadrootsNostrEvent", - "radroots_trade::listing::validation::RadrootsTradeListing", -] - -[operations.trade_validation_validate_listing_event.conformance] -vector = "spec/conformance/vectors/trade_validation/validate_listing_event.v1.json" diff --git a/spec/replica.toml b/spec/replica.toml @@ -1,19 +0,0 @@ -[replica] -name = "radroots_replica_contract" -version = "0.1.0" -purpose = "offline-first local replica state and deterministic sync" - -[crate_family] -schema = "radroots_replica_db_schema" -storage = "radroots_replica_db" -sync = "radroots_replica_sync" - -[external_sdk_wasm_bindings] -storage = "radroots_replica_db_wasm" -sync = "radroots_replica_sync_wasm" - -[policy] -transport_agnostic_sync_core = true -deterministic_emit_and_ingest = true -wasm_exports_prefix = "replica_" -forbid_legacy_alias_identifiers = true diff --git a/spec/social-events.md b/spec/social-events.md @@ -1,135 +0,0 @@ -# Public Social Event Substrate - -Status: active implementation contract - -Scope: public Radroots social Nostr event models, codecs, SDK-owned wasm builders, and deterministic -conformance vectors in this repository. - -## Purpose - -The public social event substrate extends the Radroots event family beyond profile, farm, listing, -and trade workflows while keeping relay runtime behavior, application projections, moderation -services, and private Field business documents outside this repository's event-contract boundary. - -The target implementation is standards-first and Radroots-named. Event models live in -`radroots_events`, canonical encode/decode behavior lives in `radroots_events_codec`, optional JSON -to tags helpers are provided by SDK-owned wasm bindings, and deterministic fixtures live under -`spec/conformance`. - -## Implementation Inventory - -The repository implements public social support for kind `1` `RadrootsPost`, kind `1111` -`RadrootsComment`, kind `7` `RadrootsReaction`, generic `RadrootsList` entries, listing draft kind -`30403` through `RadrootsListing`, articles, generic public file metadata, calendar date events, -calendar time events, reposts, generic reposts, calendar collections, RSVP events, and reports. - -The closeout contract requires: - -- complete model and codec coverage for the approved public social event families -- kind and tag constants for the approved NIP surface -- `RadrootsPost` preservation for optional social metadata -- strict NIP-22 `RadrootsComment` behavior without legacy `e_root` or `e_prev` fallback tags -- strict NIP-25 `RadrootsReaction` behavior where empty content is a valid like -- explicit optional `published_at` support for NIP-99 listing parity -- NIP-65 relay-list validation evidence through `RadrootsList` -- conformance vectors and canonical-event witnesses for every new or upgraded social event family - -## Approved Event Families - -The MVP public social substrate includes: - -- `RadrootsPost` for ordinary NIP-01 kind `1` notes plus optional Radroots social metadata -- `RadrootsArticle` for NIP-23 kind `30023` long-form content -- generic public `RadrootsFileMetadata` for NIP-94 kind `1063` -- `RadrootsCalendarDateEvent` for NIP-52 kind `31922` -- `RadrootsCalendarTimeEvent` for NIP-52 kind `31923` - -The production-v1 public social substrate includes: - -- `RadrootsRepost` for NIP-18 kind `6` -- `RadrootsGenericRepost` for NIP-18 kind `16` -- `RadrootsCalendar` for NIP-52 kind `31924` -- `RadrootsCalendarEventRsvp` for NIP-52 kind `31925` -- `RadrootsReport` for NIP-56 kind `1984` -- listing draft kind `30403` validation through `RadrootsListing` -- relay-list kind `10002` validation through `RadrootsList` - -## Contract Decisions - -`RadrootsPost` remains compatible with ordinary kind `1` text notes. Content-only notes must remain -valid. Optional farm or address references, media metadata, geohash, topics, and quote references -must be preserved when present and must use serde defaults so existing simple JSON fixtures remain -valid. - -`RadrootsComment` uses strict NIP-22 semantics. The target and scope model must support event-id, -address, and external roots or parents through `E`/`e`, `A`/`a`, and `I`/`i` tags with matching -`K`/`k` kind metadata. Canonical encode and decode must reject ordinary kind `1` short text note -targets; kind `1` replies belong to NIP-10 text-note reply semantics instead. Canonical decode must -reject legacy `e_root` and `e_prev` fallback tags. - -`RadrootsReaction` uses strict NIP-25 semantics. Empty content, `+`, `-`, emoji, and custom reaction -content are valid when the target tags are valid. Missing targets remain invalid. - -`RadrootsReport` intentionally tightens NIP-56 for the Radroots type: a reported pubkey `p` tag is -required for a valid report, including event and file or blob reports. - -Generic public `RadrootsFileMetadata` remains separate from private `RadrootsFarmFileMetadata` even -though both use kind `1063`. The public generic model must cover the current simple NIP-94 tags, -including URL, MIME type, SHA-256 hash, original hash, size, dimensions, blurhash, thumbnail, image, -summary, alt text, fallback, `magnet`, `i`, and `service`. - -`RadrootsCalendarDateEvent`, `RadrootsCalendarTimeEvent`, and `RadrootsCalendar` use NIP-52 -description content. Optional `description` data is encoded as event content and empty content -decodes to no description. Calendar date events use lowercase `d` for the replaceable identifier and -optional uppercase `D` tags for covered all-day dates. Calendar time events require at least one -uppercase `D` tag so timestamped events retain a deterministic calendar-date anchor across codecs and -SDK package surfaces. - -Product routing uses surface-specific kind classifiers rather than a broad public-social set. Home, -Events, Market, Map, and Profile public-content candidates are explicit. Active listing kind `30402` -can appear in public product surfaces, but listing draft kind `30403` is limited to draft-owner -contexts. Report kind `1984` is a moderation/admin candidate, not normal feed content. Relay and HTTP -auth kinds are transient and excluded from durable social and farm-ops candidate sets. Private farm -operations candidates include the farm workspace manifest, farm CRDT change envelope, farm file -metadata, and the supported NIP-29 group event subset. - -`RadrootsListingDraft` and `RadrootsRelayList` are not separate model types in the target contract. -Listing draft kind `30403` is represented through `RadrootsListing`, and NIP-51 standard and -list-set entries, including NIP-65 relay metadata kind `10002`, are represented through -`RadrootsList`. - -## Exclusions - -This substrate does not include `RadrootsFeedItem`, `RadrootsMapPin`, NIP-72 community events, -checkout or payment events, or public task, harvest, work-session, approval, or other Field business -document event types. - -Task records, work sessions, harvest records, approvals, and similar Field business objects remain -CRDT document semantics carried inside the CRDT change envelope unless a later contract explicitly -promotes them. - -## SDK Boundary - -The public social surface is event and codec substrate first. Curated SDK operation metadata -promotes the MVP social tag-builder surface after the corresponding Rust models, codecs, wasm -helpers, and conformance vectors exist. Production-v1 repost, report, calendar collection, and RSVP -behavior remains substrate-visible by default unless a consumer proves that it should be promoted -into the curated operation surface. - -The SDK-owned events codec wasm binding exposes the canonical JSON-to-tags helper names `post_tags`, -`comment_tags`, `reaction_tags`, `article_tags`, `file_metadata_tags`, -`calendar_date_event_tags`, `calendar_time_event_tags`, `calendar_tags`, -`calendar_event_rsvp_tags`, `repost_tags`, `generic_repost_tags`, and `report_tags` for the public -social substrate. The same SDK-owned wasm binding exposes `farm_workspace_manifest_tags`, -`farm_crdt_change_tags`, `farm_file_metadata_tags`, `relay_auth_tags`, and `http_auth_tags` for the -field event substrate. - -## Conformance Boundary - -Every new social codec and every upgraded existing social codec must have deterministic valid and -invalid conformance vectors before closeout. Upgraded vectors must include the strict comment, -reaction, listing, farm, list, and list-set behavior whose public contract changes during the -refactor. - -Social vectors are repo-owned and synthetic. They must not depend on application relay state, local -databases, external services, root fixture catalogs, or ambient machine state. diff --git a/spec/version.toml b/spec/version.toml @@ -1,28 +0,0 @@ -[contract] -version = "0.1.0-alpha.2" -stability = "draft" - -[semver] -major_on = [ - "remove_exported_type", - "remove_exported_field", - "change_exported_field_type", - "change_exported_enum_variant", - "change_exported_algorithm_behavior", - "change_wasm_function_signature", - "rename_internal_replica_crate_family", -] -minor_on = [ - "add_exported_type", - "add_optional_field", - "add_enum_variant", - "add_wasm_function", - "add_conformance_vector", - "add_replica_transport_adapter", -] -patch_on = ["fix_docs", "fix_non_behavioral_codegen", "fix_packaging_metadata"] - -[compatibility] -requires_conformance_pass = true -requires_export_manifest_diff = true -requires_release_notes = true