commit d35dc2ae1b690aff29c2f13c8e54ea61867af59f
parent 2c79e6a9b82587a42b3f3de3cfbc4cffaa73eaec
Author: triesap <tyson@radroots.org>
Date: Thu, 30 Jul 2026 08:51:47 +0000
trade: complete package conformance coverage
- execute canonical reducer and workflow mutation vectors
- lock packaged fixtures to the operations contract corpus
- enforce portable exports, traits, and forbidden dependency boundaries
- qualify JSON without std and remove the unused error dependency
Diffstat:
10 files changed, 558 insertions(+), 40 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -5119,7 +5119,6 @@ dependencies = [
"serde",
"serde_json",
"sha2",
- "thiserror 1.0.69",
]
[[package]]
diff --git a/contracts/conformance/vectors/trade/prepare_workflow.v1.json b/contracts/conformance/vectors/trade/prepare_workflow.v1.json
@@ -0,0 +1,74 @@
+{
+ "suite": "trade_workflow",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "trade_prepare_workflow_proposal_private_terms_001",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "proposal_with_private_terms" },
+ "expected": {
+ "mutation_kind": "proposal",
+ "required_actions": ["verify_private_terms", "sign", "persist", "deliver"],
+ "private_terms_artifact_id": "artifact-1"
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_decision_002",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "decision" },
+ "expected": {
+ "mutation_kind": "decision",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_revision_proposal_003",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "revision_proposal_with_private_terms" },
+ "expected": {
+ "mutation_kind": "revision_proposal",
+ "required_actions": ["verify_private_terms", "sign", "persist", "deliver"],
+ "private_terms_artifact_id": "artifact-1"
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_revision_decision_004",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "revision_decision" },
+ "expected": {
+ "mutation_kind": "revision_decision",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_cancellation_005",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "cancellation" },
+ "expected": {
+ "mutation_kind": "cancellation",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_unsupported_schema_006",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "proposal_with_unsupported_schema" },
+ "expected": { "error_kind": "unsupported_schema" }
+ },
+ {
+ "id": "trade_prepare_workflow_missing_mutation_id_007",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "proposal_without_mutation_id" },
+ "expected": { "error_kind": "missing_mutation_id" }
+ },
+ {
+ "id": "trade_prepare_workflow_invalid_transition_008",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "decision_without_parent" },
+ "expected": { "error_kind": "invalid_mutation" }
+ }
+ ]
+}
diff --git a/contracts/operations.toml b/contracts/operations.toml
@@ -1810,15 +1810,43 @@ signing = "native"
transport = "native"
[operations.trade_reduce_records.implementation]
-rust_modules = ["crates/trade/src/workflow.rs"]
+rust_modules = [
+ "crates/trade/src/reducer.rs",
+ "crates/trade/src/reducer_impl.rs",
+]
rust_types = [
- "radroots_trade::workflow::RadrootsTradeReductionInputV1",
- "radroots_trade::workflow::RadrootsTradeProjectionV1",
+ "radroots_trade::reducer::RadrootsTradeReductionInputV1",
+ "radroots_trade::model::RadrootsTradeProjectionV1",
]
[operations.trade_reduce_records.conformance]
vector = "contracts/conformance/vectors/trade/reduce_records.v1.json"
+[operations.trade_prepare_workflow]
+domain = "trade"
+id = "trade.prepare_workflow"
+stability = "beta"
+inputs = ["TradeMutationEnvelopeV1"]
+outputs = ["WorkflowPlan"]
+error_class = "validation_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.trade_prepare_workflow.implementation]
+rust_modules = ["crates/trade/src/workflow.rs"]
+rust_types = [
+ "radroots_trade::workflow::Error",
+ "radroots_trade::workflow::WorkflowPlan",
+]
+
+[operations.trade_prepare_workflow.conformance]
+vector = "contracts/conformance/vectors/trade/prepare_workflow.v1.json"
+case_kinds = [
+ "trade.prepare_workflow.valid",
+ "trade.prepare_workflow.invalid",
+]
+
[operations.trade_validation_validate_operational_listing_event]
domain = "trade_validation"
id = "trade_validation.validate_operational_listing_event"
diff --git a/crates/trade/Cargo.toml b/crates/trade/Cargo.toml
@@ -62,7 +62,6 @@ serde_json = { workspace = true, default-features = false, features = [
"alloc",
], optional = true }
sha2 = { workspace = true, default-features = false, optional = true }
-thiserror = { workspace = true }
[dev-dependencies]
nostr = { workspace = true }
diff --git a/crates/trade/src/reducer_impl.rs b/crates/trade/src/reducer_impl.rs
@@ -1395,8 +1395,9 @@ mod tests {
};
use radroots_test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX};
- const REDUCER_VECTORS: &str =
+ const CANONICAL_REDUCER_VECTORS: &str =
include_str!("../../../contracts/conformance/vectors/trade/reduce_records.v1.json");
+ const PACKAGED_REDUCER_VECTORS: &str = include_str!("../tests/fixtures/reduce_records.v1.json");
fn hex_64(character: char) -> String {
core::iter::repeat_n(character, 64).collect()
@@ -1885,26 +1886,52 @@ mod tests {
}
#[test]
- fn reducer_conformance_inventory_covers_deterministic_edge_cases() {
+ fn reducer_conformance_vectors_execute_deterministic_edge_cases() {
+ assert_eq!(PACKAGED_REDUCER_VECTORS, CANONICAL_REDUCER_VECTORS);
let document: serde_json::Value =
- serde_json::from_str(REDUCER_VECTORS).expect("reducer vectors parse");
- let ids = document["vectors"]
+ serde_json::from_str(PACKAGED_REDUCER_VECTORS).expect("reducer vectors parse");
+ assert_eq!(document["suite"], "trade");
+ assert_eq!(document["contract_version"], "1.0.0");
+ let vectors = document["vectors"]
.as_array()
- .expect("reducer vector array")
- .iter()
- .map(|vector| vector["id"].as_str().expect("reducer vector id"))
- .collect::<BTreeSet<_>>();
-
- for required in [
- "trade_reduce_agreed_projection_digest_001",
- "trade_reduce_contested_claims_002",
- "trade_reduce_attestation_only_003",
- "trade_reduce_missing_parent_004",
- "trade_reduce_unsupported_version_isolated_005",
- "trade_reduce_private_evidence_precedence_006",
- "trade_reduce_attestation_deduplication_007",
- ] {
- assert!(ids.contains(required), "missing reducer vector {required}");
+ .expect("reducer vector array");
+ assert_eq!(vectors.len(), 7);
+ let mut ids = BTreeSet::new();
+
+ for vector in vectors {
+ let id = vector["id"].as_str().expect("reducer vector id");
+ assert!(ids.insert(id), "duplicate reducer vector {id}");
+ assert_eq!(vector["kind"], "trade.reduce_records", "{id}");
+ assert!(vector["input"].is_object(), "{id}: input must be object");
+ assert!(
+ vector["expected"].is_object(),
+ "{id}: expected must be object"
+ );
+
+ match id {
+ "trade_reduce_agreed_projection_digest_001" => {
+ reducer_digest_is_independent_of_input_order_and_duplicates();
+ }
+ "trade_reduce_contested_claims_002" => {
+ reducer_preserves_contested_incompatible_acceptances_without_timestamp_winner();
+ }
+ "trade_reduce_attestation_only_003" => {
+ reducer_attestation_never_commits_or_invalidates_agreement();
+ }
+ "trade_reduce_missing_parent_004" => {
+ reducer_keeps_missing_parents_as_incomplete_evidence();
+ }
+ "trade_reduce_unsupported_version_isolated_005" => {
+ reducer_excludes_unsupported_versions_from_domain_semantics();
+ }
+ "trade_reduce_private_evidence_precedence_006" => {
+ reducer_private_evidence_precedence_is_permutation_independent();
+ }
+ "trade_reduce_attestation_deduplication_007" => {
+ reducer_attestation_order_and_duplicates_do_not_change_digest();
+ }
+ _ => panic!("unsupported reducer vector {id}"),
+ }
}
}
diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs
@@ -2,11 +2,15 @@
#[cfg(not(feature = "std"))]
use alloc::{
+ borrow::ToOwned,
format,
string::{String, ToString},
+ vec,
vec::Vec,
};
+use core::fmt;
+
use base64::Engine as _;
use radroots_event::{
envelope::EventEnvelope,
@@ -18,7 +22,6 @@ use radroots_event::{
use radroots_identity::PublicKey;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
-use thiserror::Error;
pub const VALIDATION_RECEIPT_DOMAIN: &str = "radroots.receipt";
pub const VALIDATION_RECEIPT_VERSION: u32 = 1;
@@ -369,30 +372,58 @@ pub struct RadrootsVerifiedValidationReceipt {
pub tags: RadrootsValidationReceiptTags,
}
-#[derive(Clone, Debug, Error, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RadrootsValidationReceiptError {
- #[error("{0} cannot be empty")]
EmptyField(&'static str),
- #[error("invalid event kind {got}; expected {expected}")]
InvalidKind { expected: u32, got: u32 },
- #[error("invalid validation receipt json")]
InvalidJson,
- #[error("validation receipt json is not canonical")]
NonCanonicalJson,
- #[error("invalid validation receipt field {0}")]
InvalidField(&'static str),
- #[error("invalid validation receipt proof metadata {0}")]
InvalidProofMetadata(&'static str),
- #[error("missing validation receipt tag {0}")]
MissingTag(&'static str),
- #[error("invalid validation receipt tag {0}")]
InvalidTag(&'static str),
- #[error("validation receipt tag {0} does not match content")]
TagMismatch(&'static str),
- #[error("validation receipt expected binding {0} does not match")]
ExpectedBindingMismatch(&'static str),
}
+impl fmt::Display for RadrootsValidationReceiptError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::EmptyField(field) => write!(formatter, "{field} cannot be empty"),
+ Self::InvalidKind { expected, got } => {
+ write!(formatter, "invalid event kind {got}; expected {expected}")
+ }
+ Self::InvalidJson => formatter.write_str("invalid validation receipt json"),
+ Self::NonCanonicalJson => {
+ formatter.write_str("validation receipt json is not canonical")
+ }
+ Self::InvalidField(field) => {
+ write!(formatter, "invalid validation receipt field {field}")
+ }
+ Self::InvalidProofMetadata(field) => {
+ write!(
+ formatter,
+ "invalid validation receipt proof metadata {field}"
+ )
+ }
+ Self::MissingTag(tag) => write!(formatter, "missing validation receipt tag {tag}"),
+ Self::InvalidTag(tag) => write!(formatter, "invalid validation receipt tag {tag}"),
+ Self::TagMismatch(tag) => {
+ write!(
+ formatter,
+ "validation receipt tag {tag} does not match content"
+ )
+ }
+ Self::ExpectedBindingMismatch(field) => write!(
+ formatter,
+ "validation receipt expected binding {field} does not match"
+ ),
+ }
+ }
+}
+
+impl core::error::Error for RadrootsValidationReceiptError {}
+
impl RadrootsValidatorSetV1 {
pub fn validate(&self) -> Result<(), RadrootsValidationReceiptError> {
validate_uuidv7(&self.set_id, "validator_set.set_id")?;
diff --git a/crates/trade/src/workflow.rs b/crates/trade/src/workflow.rs
@@ -282,6 +282,13 @@ mod tests {
use radroots_identity::PublicKey;
use radroots_test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX};
+ #[cfg(feature = "serde_json")]
+ const CANONICAL_WORKFLOW_VECTORS: &str =
+ include_str!("../../../contracts/conformance/vectors/trade/prepare_workflow.v1.json");
+ #[cfg(feature = "serde_json")]
+ const PACKAGED_WORKFLOW_VECTORS: &str =
+ include_str!("../tests/fixtures/prepare_workflow.v1.json");
+
fn pubkey(value: &str) -> PublicKey {
PublicKey::from_hex(value).expect("fixture public key")
}
@@ -509,4 +516,107 @@ mod tests {
ErrorKind::InvalidMutation
);
}
+
+ #[cfg(feature = "serde_json")]
+ #[test]
+ fn workflow_conformance_vectors_execute_every_mutation_and_error_case() {
+ assert_eq!(PACKAGED_WORKFLOW_VECTORS, CANONICAL_WORKFLOW_VECTORS);
+ let suite: serde_json::Value =
+ serde_json::from_str(PACKAGED_WORKFLOW_VECTORS).expect("workflow vector suite");
+ assert_eq!(suite["suite"], "trade_workflow");
+ assert_eq!(suite["contract_version"], "1.0.0");
+ let vectors = suite["vectors"].as_array().expect("workflow vectors");
+ assert_eq!(vectors.len(), 8);
+ let mut ids = std::collections::BTreeSet::new();
+
+ for vector in vectors {
+ let id = vector["id"].as_str().expect("workflow vector id");
+ assert!(ids.insert(id), "duplicate workflow vector {id}");
+ let mutation_fixture = vector["input"]["mutation"]
+ .as_str()
+ .expect("workflow mutation fixture");
+ let result = match mutation_fixture {
+ "proposal_with_private_terms" => {
+ WorkflowPlan::prepare(all_operation_mutations().remove(0))
+ }
+ "decision" => WorkflowPlan::prepare(all_operation_mutations().remove(1)),
+ "revision_proposal_with_private_terms" => {
+ WorkflowPlan::prepare(all_operation_mutations().remove(2))
+ }
+ "revision_decision" => WorkflowPlan::prepare(all_operation_mutations().remove(3)),
+ "cancellation" => WorkflowPlan::prepare(all_operation_mutations().remove(4)),
+ "proposal_with_unsupported_schema" => {
+ let mut mutation = all_operation_mutations().remove(0);
+ mutation.schema_version += 1;
+ WorkflowPlan::prepare(mutation)
+ }
+ "proposal_without_mutation_id" => {
+ let mut mutation = all_operation_mutations().remove(0);
+ mutation.mutation_id = None;
+ WorkflowPlan::prepare(mutation)
+ }
+ "decision_without_parent" => {
+ let mut mutation = all_operation_mutations().remove(1);
+ mutation.parent_mutation_ids.clear();
+ WorkflowPlan::prepare(mutation)
+ }
+ _ => panic!("{id}: unsupported mutation fixture {mutation_fixture}"),
+ };
+
+ let actual = match vector["kind"].as_str().expect("workflow vector kind") {
+ "trade.prepare_workflow.valid" => {
+ let plan = result.unwrap_or_else(|error| panic!("{id}: {error}"));
+ serde_json::json!({
+ "mutation_kind": mutation_kind_label(plan.kind()),
+ "required_actions": plan
+ .required_actions()
+ .iter()
+ .copied()
+ .map(workflow_action_label)
+ .collect::<Vec<_>>(),
+ "private_terms_artifact_id": plan
+ .private_terms()
+ .map(PrivateTermsPlan::artifact_id),
+ })
+ }
+ "trade.prepare_workflow.invalid" => {
+ let error = result.expect_err("invalid workflow vector must fail");
+ serde_json::json!({ "error_kind": error_kind_label(error.kind()) })
+ }
+ kind => panic!("{id}: unsupported workflow vector kind {kind}"),
+ };
+ assert_eq!(actual, vector["expected"], "{id}");
+ }
+ }
+
+ #[cfg(feature = "serde_json")]
+ const fn mutation_kind_label(kind: TradeMutationKindV1) -> &'static str {
+ match kind {
+ TradeMutationKindV1::Proposal => "proposal",
+ TradeMutationKindV1::Decision => "decision",
+ TradeMutationKindV1::RevisionProposal => "revision_proposal",
+ TradeMutationKindV1::RevisionDecision => "revision_decision",
+ TradeMutationKindV1::Cancellation => "cancellation",
+ }
+ }
+
+ #[cfg(feature = "serde_json")]
+ const fn workflow_action_label(action: WorkflowAction) -> &'static str {
+ match action {
+ WorkflowAction::Sign => "sign",
+ WorkflowAction::Persist => "persist",
+ WorkflowAction::Deliver => "deliver",
+ WorkflowAction::VerifyPrivateTerms => "verify_private_terms",
+ }
+ }
+
+ #[cfg(feature = "serde_json")]
+ const fn error_kind_label(kind: ErrorKind) -> &'static str {
+ match kind {
+ ErrorKind::UnsupportedSchema => "unsupported_schema",
+ ErrorKind::MissingMutationId => "missing_mutation_id",
+ ErrorKind::InvalidMutation => "invalid_mutation",
+ ErrorKind::InvalidPrivateTerms => "invalid_private_terms",
+ }
+ }
}
diff --git a/crates/trade/tests/fixtures/prepare_workflow.v1.json b/crates/trade/tests/fixtures/prepare_workflow.v1.json
@@ -0,0 +1,74 @@
+{
+ "suite": "trade_workflow",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "trade_prepare_workflow_proposal_private_terms_001",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "proposal_with_private_terms" },
+ "expected": {
+ "mutation_kind": "proposal",
+ "required_actions": ["verify_private_terms", "sign", "persist", "deliver"],
+ "private_terms_artifact_id": "artifact-1"
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_decision_002",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "decision" },
+ "expected": {
+ "mutation_kind": "decision",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_revision_proposal_003",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "revision_proposal_with_private_terms" },
+ "expected": {
+ "mutation_kind": "revision_proposal",
+ "required_actions": ["verify_private_terms", "sign", "persist", "deliver"],
+ "private_terms_artifact_id": "artifact-1"
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_revision_decision_004",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "revision_decision" },
+ "expected": {
+ "mutation_kind": "revision_decision",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_cancellation_005",
+ "kind": "trade.prepare_workflow.valid",
+ "input": { "mutation": "cancellation" },
+ "expected": {
+ "mutation_kind": "cancellation",
+ "required_actions": ["sign", "persist", "deliver"],
+ "private_terms_artifact_id": null
+ }
+ },
+ {
+ "id": "trade_prepare_workflow_unsupported_schema_006",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "proposal_with_unsupported_schema" },
+ "expected": { "error_kind": "unsupported_schema" }
+ },
+ {
+ "id": "trade_prepare_workflow_missing_mutation_id_007",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "proposal_without_mutation_id" },
+ "expected": { "error_kind": "missing_mutation_id" }
+ },
+ {
+ "id": "trade_prepare_workflow_invalid_transition_008",
+ "kind": "trade.prepare_workflow.invalid",
+ "input": { "mutation": "decision_without_parent" },
+ "expected": { "error_kind": "invalid_mutation" }
+ }
+ ]
+}
diff --git a/crates/trade/tests/fixtures/reduce_records.v1.json b/crates/trade/tests/fixtures/reduce_records.v1.json
@@ -0,0 +1,105 @@
+{
+ "suite": "trade",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "trade_reduce_agreed_projection_digest_001",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "proposal_plus_seller_acceptance_with_live_all_line_reservation",
+ "mutation_order": "arbitrary_with_duplicate_transport_observations",
+ "attestation_records": []
+ },
+ "expected": {
+ "negotiation_state": "open",
+ "agreement_state": "agreed",
+ "evidence_state": "complete",
+ "conflict_state": "none",
+ "attestation_state": "none",
+ "projection_digest": "35a5f555344febe675f4e5e6b15865356400b2b8f7791fc631590e0f1e1fd441"
+ }
+ },
+ {
+ "id": "trade_reduce_contested_claims_002",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "proposal_plus_two_causally_unordered_incompatible_acceptances",
+ "winner_selection": "forbidden"
+ },
+ "expected": {
+ "agreement_state": "contested",
+ "conflict_state": "double_acceptance",
+ "contested_claim_ids": "all_incompatible_claim_mutation_ids_sorted",
+ "forbidden_winner_inputs": [
+ "timestamp",
+ "transport_event_id",
+ "relay_order",
+ "observation_order",
+ "first_seen"
+ ]
+ }
+ },
+ {
+ "id": "trade_reduce_attestation_only_003",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "agreed_claim_plus_invalid_attestation_record"
+ },
+ "expected": {
+ "agreement_state": "agreed",
+ "attestation_state": "present_invalid",
+ "attestation_agreement_authority": false
+ }
+ },
+ {
+ "id": "trade_reduce_missing_parent_004",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "known_acceptance_with_missing_direct_parent"
+ },
+ "expected": {
+ "evidence_state": "missing",
+ "agreement_claim_visibility": "retained_when_local_claim_is_known",
+ "global_invalidity": false
+ }
+ },
+ {
+ "id": "trade_reduce_unsupported_version_isolated_005",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "unsupported_version_proposal_without_supported_records"
+ },
+ "expected": {
+ "evidence_state": "unsupported_version",
+ "negotiation_state": "none",
+ "agreement_state": "none",
+ "unsupported_record_domain_authority": false
+ }
+ },
+ {
+ "id": "trade_reduce_private_evidence_precedence_006",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "agreed_private_terms_candidate_with_conflicting_duplicate_evidence",
+ "evidence_order": "arbitrary"
+ },
+ "expected": {
+ "private_terms_state": "commitment_mismatch",
+ "projection_digest": "input_order_independent"
+ }
+ },
+ {
+ "id": "trade_reduce_attestation_deduplication_007",
+ "kind": "trade.reduce_records",
+ "input": {
+ "shape": "agreed_claim_with_duplicate_valid_and_distinct_invalid_attestations",
+ "attestation_order": "arbitrary"
+ },
+ "expected": {
+ "attestation_state": "conflicting",
+ "duplicate_records": "removed",
+ "projection_digest": "input_order_independent"
+ }
+ }
+ ]
+}
diff --git a/crates/trade/tests/package_boundary.rs b/crates/trade/tests/package_boundary.rs
@@ -7,8 +7,15 @@ const MANIFEST: &str = include_str!("../Cargo.toml");
const DRAFT: &str = include_str!("../src/operational_listing/draft.rs");
const IDENTITY: &str = include_str!("../src/identity.rs");
const MODEL: &str = include_str!("../src/model.rs");
+const OPERATIONS: &str = include_str!("../../../contracts/operations.toml");
const ROOT: &str = include_str!("../src/lib.rs");
const REDUCER_IMPLEMENTATION: &str = include_str!("../src/reducer_impl.rs");
+const CANONICAL_REDUCER_VECTORS: &str =
+ include_str!("../../../contracts/conformance/vectors/trade/reduce_records.v1.json");
+const PACKAGED_REDUCER_VECTORS: &str = include_str!("fixtures/reduce_records.v1.json");
+const CANONICAL_WORKFLOW_VECTORS: &str =
+ include_str!("../../../contracts/conformance/vectors/trade/prepare_workflow.v1.json");
+const PACKAGED_WORKFLOW_VECTORS: &str = include_str!("fixtures/prepare_workflow.v1.json");
const WORKFLOW: &str = include_str!("../src/workflow.rs");
const PACKAGE_TIERS: &str = include_str!("../../../contracts/releases/package_tiers.toml");
@@ -95,15 +102,79 @@ fn trade_feature_graph_has_no_persistence_or_sql_boundary() {
let dev_dependencies = table_keys(MANIFEST, "[dev-dependencies]");
assert!(!features.contains("event_store"));
- assert!(!dependencies.contains("radroots_event_store"));
- assert!(!dependencies.contains("sqlx"));
- assert!(!dev_dependencies.contains("sqlx"));
- assert!(!dev_dependencies.contains("tokio"));
+ for forbidden in [
+ "radroots_authority",
+ "radroots_event_store",
+ "radroots_outbox",
+ "radroots_transport",
+ "reqwest",
+ "sqlx",
+ "tokio",
+ ] {
+ assert!(
+ !dependencies.contains(forbidden),
+ "trade acquired forbidden production dependency {forbidden}"
+ );
+ }
+ for forbidden in ["sqlx", "tokio"] {
+ assert!(
+ !dev_dependencies.contains(forbidden),
+ "trade retained forbidden development dependency {forbidden}"
+ );
+ }
assert!(!MANIFEST.contains("sqlite-bundled"));
assert!(!MANIFEST.contains("runtime-tokio"));
}
#[test]
+fn portable_root_exports_and_native_traits_are_compile_checked() {
+ use radroots_trade::{Error, Projection, ReducerIssue, ReductionInput, WorkflowPlan};
+
+ fn assert_portable<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {}
+ fn assert_native_error<T: core::error::Error + Send + Sync>() {}
+
+ assert_portable::<Projection>();
+ assert_portable::<ReducerIssue>();
+ assert_portable::<ReductionInput>();
+ assert_portable::<WorkflowPlan>();
+ assert_native_error::<Error>();
+
+ assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]"));
+ assert!(!ROOT.contains("pub trait "));
+ for export in [
+ "pub use model::RadrootsTradeProjectionV1 as Projection;",
+ "RadrootsTradeReducerIssueV1 as ReducerIssue",
+ "RadrootsTradeReductionInputV1 as ReductionInput",
+ "pub use workflow::{Error, WorkflowPlan};",
+ ] {
+ assert!(
+ ROOT.contains(export),
+ "missing canonical root export {export}"
+ );
+ }
+}
+
+#[test]
+fn packaged_trade_vectors_match_canonical_operation_contracts() {
+ assert_eq!(PACKAGED_REDUCER_VECTORS, CANONICAL_REDUCER_VECTORS);
+ assert_eq!(PACKAGED_WORKFLOW_VECTORS, CANONICAL_WORKFLOW_VECTORS);
+ for required in [
+ "id = \"trade.reduce_records\"",
+ "crates/trade/src/reducer.rs",
+ "radroots_trade::model::RadrootsTradeProjectionV1",
+ "id = \"trade.prepare_workflow\"",
+ "crates/trade/src/workflow.rs",
+ "contracts/conformance/vectors/trade/prepare_workflow.v1.json",
+ ] {
+ assert!(
+ OPERATIONS.contains(required),
+ "operation contract is missing {required}"
+ );
+ }
+ assert!(!OPERATIONS.contains("radroots_trade::workflow::RadrootsTradeProjectionV1"));
+}
+
+#[test]
fn trade_model_reducer_and_evidence_have_final_public_owners() {
use radroots_trade::{Projection, ReducerIssue, ReductionInput};
use radroots_trade::{evidence::RadrootsTradeEvidenceStateV1, reducer::reduce_trade_records};