lib

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

commit dadd796c4cc113ec3fd8511342b7707d553124b0
parent cb89c8cc66b71fcccdb369563a9d567535cd5015
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 16:01:46 +0000

core: complete package conformance coverage

- Add deterministic numeric properties and canonical serialization vectors
- Lock exact core features dependencies modules and root exports in architecture policy
- Prove no-std host and WASM builds plus sealed rustdoc boundaries
- Add a durable Nix conformance lane and refresh governed evidence

Diffstat:
Mbuild/nix/checks.nix | 23+++++++++++++++++++++++
Acontracts/conformance/vectors/core/value_serialization.v1.json | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/core/src/currency.rs | 4++--
Mcrates/core/src/decimal.rs | 4++--
Mcrates/core/src/discount.rs | 16++++++++--------
Mcrates/core/src/lib.rs | 31+++++++++++++++++++++++++++++--
Mcrates/core/src/money.rs | 4++--
Mcrates/core/src/percent.rs | 6+++---
Mcrates/core/src/quantity.rs | 6+++---
Mcrates/core/src/quantity_price.rs | 21+++++++++++++++++++--
Mcrates/core/src/unit.rs | 32++++++++++++++++----------------
Mcrates/core/tests/error.rs | 2++
Acrates/core/tests/properties.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/core/tests/serde.rs | 48+++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/event_store/contracts/source_maintenance_v1.manifest.json | 28++++++++++++++--------------
Mcrates/event_store/contracts/source_maintenance_v1.manifest.sha256 | 2+-
Mcrates/event_store/src/generated/source_maintenance_manifest.rs | 4++--
Mtools/xtask/src/architecture.rs | 2++
Atools/xtask/src/architecture/core_contract.rs | 655+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
19 files changed, 1039 insertions(+), 58 deletions(-)

diff --git a/build/nix/checks.nix b/build/nix/checks.nix @@ -25,6 +25,28 @@ let installPhaseCommand = "mkdir -p $out"; } ); + coreConformance = common.craneLib.mkCargoDerivation ( + common.commonCraneArgs + // { + inherit (common) cargoArtifacts; + pname = "radroots-core-conformance"; + doCheck = false; + buildPhaseCargoCommand = '' + cargo check -p radroots-core --all-targets --no-default-features --locked + cargo check -p radroots-core --all-targets --no-default-features --features std --locked + cargo check -p radroots-core --all-targets --no-default-features --features serde --locked + cargo check -p radroots-core --all-targets --locked + cargo check -p radroots-core --all-targets --all-features --locked + cargo clippy -p radroots-core --all-targets --all-features --locked -- -D warnings + cargo test -p radroots-core --all-targets --all-features --locked + cargo check -p radroots-core --no-default-features --target wasm32-unknown-unknown --locked + cargo check -p radroots-core --no-default-features --features serde --target wasm32-unknown-unknown --locked + RUSTDOCFLAGS="-D warnings" cargo doc -p radroots-core --all-features --no-deps --locked + cargo test -p radroots-core --all-features --doc --locked + ''; + installPhaseCommand = "mkdir -p $out"; + } + ); mkReplicaSyncLane = { pname, @@ -61,6 +83,7 @@ in cargo-fmt = cargoFmt; cargo-check = cargoCheck; cargo-test = cargoTest; + core-conformance = coreConformance; replica-sync-default-check = replicaSyncDefaultCheck; replica-sync-default-test = replicaSyncDefaultTest; replica-sync-legacy-ingest-check = replicaSyncLegacyCheck; diff --git a/contracts/conformance/vectors/core/value_serialization.v1.json b/contracts/conformance/vectors/core/value_serialization.v1.json @@ -0,0 +1,102 @@ +{ + "suite": "core_value_serialization", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "core_decimal_normalizes_scale_001", + "kind": "core.decimal.serde", + "input": "1.2300", + "expected": "1.23" + }, + { + "id": "core_decimal_normalizes_signed_zero_002", + "kind": "core.decimal.serde", + "input": "-0.000", + "expected": "0" + }, + { + "id": "core_currency_normalizes_case_003", + "kind": "core.currency.serde", + "input": "usd", + "expected": "USD" + }, + { + "id": "core_money_normalizes_values_004", + "kind": "core.money.serde", + "input": { + "amount": "2.500", + "currency": "usd" + }, + "expected": { + "amount": "2.5", + "currency": "USD" + } + }, + { + "id": "core_percent_normalizes_value_005", + "kind": "core.percent.serde", + "input": { + "value": "12.500" + }, + "expected": { + "value": "12.5" + } + }, + { + "id": "core_quantity_normalizes_unit_006", + "kind": "core.quantity.serde", + "input": { + "amount": "1.2500", + "unit": "kilograms", + "label": "crate" + }, + "expected": { + "amount": "1.25", + "unit": "kg", + "label": "crate" + } + }, + { + "id": "core_quantity_omits_absent_label_007", + "kind": "core.quantity.serde", + "input": { + "amount": "3.000", + "unit": "each" + }, + "expected": { + "amount": "3", + "unit": "each" + } + }, + { + "id": "core_unit_normalizes_synonym_008", + "kind": "core.unit.serde", + "input": "pounds", + "expected": "lb" + }, + { + "id": "core_quantity_price_normalizes_nested_values_009", + "kind": "core.quantity_price.serde", + "input": { + "amount": { + "amount": "5.000", + "currency": "eur" + }, + "quantity": { + "amount": "2.000", + "unit": "kilograms" + } + }, + "expected": { + "amount": { + "amount": "5", + "currency": "EUR" + }, + "quantity": { + "amount": "2", + "unit": "kg" + } + } + } + ] +} diff --git a/crates/core/src/currency.rs b/crates/core/src/currency.rs @@ -10,8 +10,8 @@ use std::string::String; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(as = "string"))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(as = "string"))] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Currency([u8; 3]); diff --git a/crates/core/src/decimal.rs b/crates/core/src/decimal.rs @@ -12,8 +12,8 @@ use alloc::{format, string::ToString}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError}; -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(as = "string"))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(as = "string"))] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct Decimal(pub RustDecimal); diff --git a/crates/core/src/discount.rs b/crates/core/src/discount.rs @@ -4,8 +4,8 @@ use alloc::string::String; use std::string::String; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum DiscountScope { @@ -14,8 +14,8 @@ pub enum DiscountScope { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "serde", @@ -27,8 +27,8 @@ pub enum DiscountThreshold { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "serde", @@ -40,8 +40,8 @@ pub enum DiscountValue { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub struct Discount { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -1,12 +1,39 @@ +//! Portable, deterministic value types for Radroots domain packages. +//! +//! Checked constructors and arithmetic preserve value invariants at trust +//! boundaries: +//! +//! ``` +//! use radroots_core::{Currency, Decimal, Money}; +//! +//! let price = Money::try_new("12.50".parse::<Decimal>()?, Currency::USD)?; +//! let total = price.checked_mul_decimal(Decimal::from(2_u32))?; +//! assert_eq!(total.amount().to_string(), "25"); +//! # Ok::<(), radroots_core::Error>(()) +//! ``` +//! +//! Implementation-only serialization and code-generation modules are not +//! public API: +//! +//! ```compile_fail +//! use radroots_core::serde_ext; +//! ``` +//! +//! ```compile_fail +//! use radroots_core::dto; +//! ``` + #![cfg_attr(not(feature = "std"), no_std)] #![forbid(unsafe_code)] #[cfg(not(feature = "std"))] extern crate alloc; +#[cfg(all(test, not(feature = "std")))] +extern crate std; pub mod currency; pub mod decimal; mod discount; -#[cfg(test)] +#[cfg(all(test, feature = "std"))] mod dto; mod error; pub mod money; @@ -15,7 +42,7 @@ pub mod pricing; pub mod quantity; mod quantity_price; #[cfg(feature = "serde")] -pub mod serde_ext; +mod serde_ext; pub mod unit; pub use currency::Currency; diff --git a/crates/core/src/money.rs b/crates/core/src/money.rs @@ -4,8 +4,8 @@ use rust_decimal::RoundingStrategy; use rust_decimal::prelude::ToPrimitive; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Money { pub amount: crate::Decimal, diff --git a/crates/core/src/percent.rs b/crates/core/src/percent.rs @@ -5,12 +5,12 @@ use crate::Decimal; use crate::money::Money; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Percent { #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))] - #[cfg_attr(test, dto(as = "string"))] + #[cfg_attr(all(test, feature = "std"), dto(as = "string"))] pub value: Decimal, } diff --git a/crates/core/src/quantity.rs b/crates/core/src/quantity.rs @@ -9,12 +9,12 @@ use alloc::string::String; use std::string::String; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct Quantity { #[cfg_attr(feature = "serde", serde(with = "crate::serde_ext::decimal_str"))] - #[cfg_attr(test, dto(as = "string"))] + #[cfg_attr(all(test, feature = "std"), dto(as = "string"))] pub amount: Decimal, pub unit: Unit, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] diff --git a/crates/core/src/quantity_price.rs b/crates/core/src/quantity_price.rs @@ -3,8 +3,8 @@ use core::fmt; use crate::{Decimal, Money, Quantity, Unit}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct QuantityPrice { pub amount: Money, @@ -63,6 +63,23 @@ mod sealed { /// The `try_*` methods are the canonical checked surface. The infallible /// methods remain temporarily for first-party source compatibility and return /// zero when the corresponding checked operation fails. +/// +/// Downstream crates may call this trait but cannot implement it: +/// +/// ```compile_fail +/// use radroots_core::{Money, Quantity}; +/// use radroots_core::pricing::{Error, QuantityPriceOps}; +/// +/// struct ForeignPrice; +/// +/// impl QuantityPriceOps for ForeignPrice { +/// fn cost_for(&self, _: &Quantity) -> Money { panic!() } +/// fn cost_for_rounded(&self, _: &Quantity) -> Money { panic!() } +/// fn cost_for_with_quantized_price(&self, _: &Quantity) -> Money { panic!() } +/// fn try_cost_for(&self, _: &Quantity) -> Result<Money, Error> { panic!() } +/// fn try_cost_for_rounded(&self, _: &Quantity) -> Result<Money, Error> { panic!() } +/// } +/// ``` pub trait QuantityPriceOps: sealed::Sealed { /// Compatibility operation that returns zero on invalid input. #[must_use] diff --git a/crates/core/src/unit.rs b/crates/core/src/unit.rs @@ -15,37 +15,37 @@ use crate::Decimal; const GRAMS_PER_OUNCE: Decimal = Decimal::from_parts(2_579_719_349, 6, 0, 9); const GRAMS_PER_POUND: Decimal = Decimal::from_parts(45_359_237, 0, 0, 5); -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] -#[cfg_attr(test, dto(as = "string_enum"))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] +#[cfg_attr(all(test, feature = "std"), dto(as = "string_enum"))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum UnitDimension { - #[cfg_attr(test, dto(rename = "count"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "count"))] Count, - #[cfg_attr(test, dto(rename = "mass"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "mass"))] Mass, - #[cfg_attr(test, dto(rename = "volume"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "volume"))] Volume, } -#[cfg_attr(test, derive(dto_bindgen::Dto))] -#[cfg_attr(test, dto(export))] -#[cfg_attr(test, dto(as = "string_enum"))] +#[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))] +#[cfg_attr(all(test, feature = "std"), dto(export))] +#[cfg_attr(all(test, feature = "std"), dto(as = "string_enum"))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Unit { - #[cfg_attr(test, dto(rename = "each"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "each"))] Each, - #[cfg_attr(test, dto(rename = "kg"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "kg"))] MassKg, - #[cfg_attr(test, dto(rename = "g"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "g"))] MassG, - #[cfg_attr(test, dto(rename = "oz"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "oz"))] MassOz, - #[cfg_attr(test, dto(rename = "lb"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "lb"))] MassLb, - #[cfg_attr(test, dto(rename = "l"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "l"))] VolumeL, - #[cfg_attr(test, dto(rename = "ml"))] + #[cfg_attr(all(test, feature = "std"), dto(rename = "ml"))] VolumeMl, } diff --git a/crates/core/tests/error.rs b/crates/core/tests/error.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "std")] use std::error::Error as _; use radroots_core::{Error, Unit, currency, decimal, money, percent, pricing, quantity, unit}; @@ -20,6 +21,7 @@ fn aggregate_error_accepts_every_scoped_error() { ]; for error in errors { + #[cfg(feature = "std")] assert!(error.source().is_some()); assert!(!error.to_string().is_empty()); } diff --git a/crates/core/tests/properties.rs b/crates/core/tests/properties.rs @@ -0,0 +1,107 @@ +mod common; + +use radroots_core::{Currency, Decimal, Money, Quantity, Unit}; + +const VALUES: [&str; 8] = ["-100", "-1.25", "-0.01", "0", "0.01", "1.25", "42", "100"]; +const NON_NEGATIVE_VALUES: [&str; 6] = ["0", "0.01", "1", "1.25", "42", "100"]; + +#[test] +fn checked_decimal_arithmetic_obeys_ring_laws_on_representative_values() { + for left in VALUES.map(common::dec) { + assert_eq!(left.checked_add(Decimal::ZERO).unwrap(), left); + assert_eq!(left.checked_mul(Decimal::ONE).unwrap(), left); + for right in VALUES.map(common::dec) { + assert_eq!( + left.checked_add(right).unwrap(), + right.checked_add(left).unwrap() + ); + assert_eq!( + left.checked_mul(right).unwrap(), + right.checked_mul(left).unwrap() + ); + assert_eq!( + left.checked_add(right).unwrap().checked_sub(right).unwrap(), + left + ); + } + } +} + +#[test] +fn checked_decimal_multiplication_distributes_over_addition() { + for left in VALUES.map(common::dec) { + for middle in VALUES.map(common::dec) { + for right in VALUES.map(common::dec) { + let combined = left + .checked_add(middle) + .unwrap() + .checked_mul(right) + .unwrap(); + let distributed = left + .checked_mul(right) + .unwrap() + .checked_add(middle.checked_mul(right).unwrap()) + .unwrap(); + assert_eq!(combined, distributed); + } + } + } +} + +#[test] +fn checked_money_addition_is_commutative_and_associative() { + for first in NON_NEGATIVE_VALUES.map(common::dec) { + for second in NON_NEGATIVE_VALUES.map(common::dec) { + for third in NON_NEGATIVE_VALUES.map(common::dec) { + let first = Money::try_new(first, Currency::USD).unwrap(); + let second = Money::try_new(second, Currency::USD).unwrap(); + let third = Money::try_new(third, Currency::USD).unwrap(); + assert_eq!(first.checked_add(&second), second.checked_add(&first)); + assert_eq!( + first.checked_add(&second).unwrap().checked_add(&third), + first.checked_add(&second.checked_add(&third).unwrap()) + ); + } + } + } +} + +#[test] +fn checked_quantity_addition_preserves_unit_and_is_associative() { + for unit in [Unit::Each, Unit::MassG, Unit::VolumeMl] { + for first in NON_NEGATIVE_VALUES.map(common::dec) { + for second in NON_NEGATIVE_VALUES.map(common::dec) { + let first = Quantity::try_new(first, unit).unwrap(); + let second = Quantity::try_new(second, unit).unwrap(); + let sum = first.try_add(&second).unwrap(); + assert_eq!(sum.unit(), unit); + assert_eq!( + sum.amount(), + first.amount().checked_add(second.amount()).unwrap() + ); + assert_eq!(first.try_add(&second), second.try_add(&first)); + } + } + } +} + +#[test] +fn canonical_unit_conversion_is_idempotent() { + for (unit, canonical) in [ + (Unit::Each, Unit::Each), + (Unit::MassKg, Unit::MassG), + (Unit::MassG, Unit::MassG), + (Unit::MassOz, Unit::MassG), + (Unit::MassLb, Unit::MassG), + (Unit::VolumeL, Unit::VolumeMl), + (Unit::VolumeMl, Unit::VolumeMl), + ] { + for amount in NON_NEGATIVE_VALUES.map(common::dec) { + let quantity = Quantity::try_new(amount, unit).unwrap(); + let once = quantity.to_canonical().unwrap(); + let twice = once.to_canonical().unwrap(); + assert_eq!(once, twice); + assert_eq!(once.unit(), canonical); + } + } +} diff --git a/crates/core/tests/serde.rs b/crates/core/tests/serde.rs @@ -3,10 +3,14 @@ mod common; use core::str::FromStr; +use std::collections::BTreeSet; -use radroots_core::{Currency, Decimal, Money, Percent, Quantity, Unit}; +use radroots_core::{Currency, Decimal, Money, Percent, Quantity, QuantityPrice, Unit}; use serde_json::Value; +const SERIALIZATION_VECTORS: &str = + include_str!("../../../contracts/conformance/vectors/core/value_serialization.v1.json"); + #[test] fn decimal_serializes_as_string() { let d = common::dec("1.2300"); @@ -61,3 +65,45 @@ fn currency_serializes_as_code() { let json = serde_json::to_string(&c).unwrap(); assert_eq!(json, "\"USD\""); } + +#[test] +fn canonical_serialization_vectors_roundtrip_through_public_types() { + let suite: Value = serde_json::from_str(SERIALIZATION_VECTORS).unwrap(); + assert_eq!(suite["suite"], "core_value_serialization"); + assert_eq!(suite["contract_version"], "1.0.0"); + + let vectors = suite["vectors"].as_array().unwrap(); + let mut ids = BTreeSet::new(); + for vector in vectors { + let id = vector["id"].as_str().unwrap(); + assert!(ids.insert(id), "duplicate vector id {id}"); + let input = vector["input"].clone(); + let actual = match vector["kind"].as_str().unwrap() { + "core.decimal.serde" => { + serde_json::to_value(serde_json::from_value::<Decimal>(input).unwrap()).unwrap() + } + "core.currency.serde" => { + serde_json::to_value(serde_json::from_value::<Currency>(input).unwrap()).unwrap() + } + "core.money.serde" => { + serde_json::to_value(serde_json::from_value::<Money>(input).unwrap()).unwrap() + } + "core.percent.serde" => { + serde_json::to_value(serde_json::from_value::<Percent>(input).unwrap()).unwrap() + } + "core.quantity.serde" => { + serde_json::to_value(serde_json::from_value::<Quantity>(input).unwrap()).unwrap() + } + "core.unit.serde" => { + serde_json::to_value(serde_json::from_value::<Unit>(input).unwrap()).unwrap() + } + "core.quantity_price.serde" => { + serde_json::to_value(serde_json::from_value::<QuantityPrice>(input).unwrap()) + .unwrap() + } + kind => panic!("unsupported serialization vector kind {kind}"), + }; + assert_eq!(actual, vector["expected"], "vector {id}"); + } + assert_eq!(ids.len(), 9); +} diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.json b/crates/event_store/contracts/source_maintenance_v1.manifest.json @@ -159,50 +159,50 @@ { "role": "core_currency_value_authority", "path": "crates/core/src/currency.rs", - "byte_length": 4268, - "sha256": "a4a7cc26cfda954b878ba6b6065d5ff063bcecc8aa799582b16295fba2db328f", + "byte_length": 4312, + "sha256": "adfae0ef03de95c2cd3bd7175f32a6c12723dace67b344367fad0f4d4af1e603", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_decimal_value_authority", "path": "crates/core/src/decimal.rs", - "byte_length": 8356, - "sha256": "edc5bb46c540a0a6040bd568e6108e816e3af0235c45d55ddd33d0d4a6dccbb8", + "byte_length": 8400, + "sha256": "93267e36f093ea64e488351bec54c5a142ba94c6de6985f352e9d75419d3e0ab", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_money_value_authority", "path": "crates/core/src/money.rs", - "byte_length": 9103, - "sha256": "d2cf3d8e13e4989841cffe6794870911ca34356e7777e26a4d998be5eb21c299", + "byte_length": 9147, + "sha256": "4f761da36418b2b0b2c909b55b4d80a42d14983b59fc1ebf118862557b527a93", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_percent_value_authority", "path": "crates/core/src/percent.rs", - "byte_length": 3202, - "sha256": "7908f8ed4c30349cd67fff6f1da95d275194357fe3d742183dabb0daef47561d", + "byte_length": 3268, + "sha256": "4cfe54a631f911947bcc6e3f0d1607214d88009e87669589cf30b1e078bf8ca0", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_quantity_value_authority", "path": "crates/core/src/quantity.rs", - "byte_length": 7969, - "sha256": "789fb647dfdbbd92dab3b03c6556c9280078cfc79df33eddcb345a5c575acb63", + "byte_length": 8035, + "sha256": "a8bcfe6b9038d3baf7a833f811cdd7ac467dc670123a156918ab208e901fdaf5", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_quantity_price_value_authority", "path": "crates/core/src/quantity_price.rs", - "byte_length": 8266, - "sha256": "0bfa11f76a27e92d007c71c38cf2b43f6dc40707a62d44fa753b59c9407ad39d", + "byte_length": 8988, + "sha256": "0b6fa25c5ad4c7d809bd81cdc2c054f5c4ad9d7963fe0824843df5ce9acddba7", "hash_algorithm": "sha256_bytes_v1" }, { "role": "core_unit_value_authority", "path": "crates/core/src/unit.rs", - "byte_length": 10084, - "sha256": "e3b28d3132d9317c2cb07b867866f760cd742510faa0e4db40e52f7f1f05bd3a", + "byte_length": 10436, + "sha256": "fe17fb780c75a18d3b4678d9268726ba97814ed8da7775b65a03724df120560f", "hash_algorithm": "sha256_bytes_v1" }, { diff --git a/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 b/crates/event_store/contracts/source_maintenance_v1.manifest.sha256 @@ -1 +1 @@ -26932b5fec2167ef24f14474205c5556371ecd1765b268e5e43757a8be780147 +ecfb3ba4e68e5628d880fd8dfc8f7f1cbbc4123ce1f6510113107210682b38d9 diff --git a/crates/event_store/src/generated/source_maintenance_manifest.rs b/crates/event_store/src/generated/source_maintenance_manifest.rs @@ -1,8 +1,8 @@ // @generated by `cargo xtask contract source-maintenance-manifest --write`; do not edit. -pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"ad4a6c8ae9488fc8033792bc6952af04687f312901c1847d8c668a62913bb642\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 11506,\n \"sha256\": \"af871d7567594f75da8385691297a5ce170f51ae54c4c318db10b99d458a3335\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4268,\n \"sha256\": \"a4a7cc26cfda954b878ba6b6065d5ff063bcecc8aa799582b16295fba2db328f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 8356,\n \"sha256\": \"edc5bb46c540a0a6040bd568e6108e816e3af0235c45d55ddd33d0d4a6dccbb8\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 9103,\n \"sha256\": \"d2cf3d8e13e4989841cffe6794870911ca34356e7777e26a4d998be5eb21c299\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 3202,\n \"sha256\": \"7908f8ed4c30349cd67fff6f1da95d275194357fe3d742183dabb0daef47561d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 7969,\n \"sha256\": \"789fb647dfdbbd92dab3b03c6556c9280078cfc79df33eddcb345a5c575acb63\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 8266,\n \"sha256\": \"0bfa11f76a27e92d007c71c38cf2b43f6dc40707a62d44fa753b59c9407ad39d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10084,\n \"sha256\": \"e3b28d3132d9317c2cb07b867866f760cd742510faa0e4db40e52f7f1f05bd3a\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19421,\n \"sha256\": \"4772e041cb20a4963afb2f3159804c777e2f2be61bfdb6ee267e7a7c04258972\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33617,\n \"sha256\": \"79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184407,\n \"sha256\": \"c455d40fc736e3db264f567c7809af7bd897d89be8a33dfb21667a6ef6b8d6c6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 394574,\n \"sha256\": \"db57dc3e35e64c7194683142fe55edba853671a829269449dd2273056dfc3a0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 30140,\n \"sha256\": \"210112eeaa6975a3b4fbb97d5c52588f8c6d8d07975e531d39737fd11235de51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51756,\n \"sha256\": \"f8d5b62f0613104aa86658d5bf1baade92c7df83f00ef0cddadd734b9797afca\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 38279,\n \"sha256\": \"f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 194594,\n \"sha256\": \"4b2961fc69c8204ad5470c0e51c93533063e49d7a14da00a53598c3e8d0cdf6b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 835558,\n \"sha256\": \"51a01360e2c32c83056cb36e22b850e6b09d2ad4e4a749097564b4a100709a16\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 177996,\n \"sha256\": \"c309da38f12101d77c4de1caa49ef443252cac6238b49692182851c2cf894bda\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 500241,\n \"sha256\": \"972e2a6b1099267e11d9a34e73fd38b20fc217a3dd79bd9a0f1f060b680ba2ba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 14976,\n \"sha256\": \"42b59a8576e4f86b091941a64349ac9eaa20b1e3b52aef367bb3d5947d8ccd1d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; +pub(crate) const SOURCE_MAINTENANCE_MANIFEST_JSON: &str = "{\n \"schema_version\": 1,\n \"contract_id\": \"radroots_event_store.source_maintenance_v1\",\n \"hook_id\": \"source_maintenance_v1\",\n \"manifest_schema\": {\n \"path\": \"crates/event_store/contracts/source_maintenance_v1.manifest.schema.json\",\n \"byte_length\": 12315,\n \"sha256\": \"ad4a6c8ae9488fc8033792bc6952af04687f312901c1847d8c668a62913bb642\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"predecessor\": {\n \"hook_id\": \"food_availability_projection_v1\",\n \"manifest\": {\n \"path\": \"crates/event_store/contracts/food_availability_projection_v1.manifest.json\",\n \"byte_length\": 17455,\n \"sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n },\n \"migration\": {\n \"version\": 4,\n \"name\": \"source_maintenance\",\n \"up\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.up.sql\",\n \"byte_length\": 19841,\n \"sha256\": \"425dc799f392b87f265a6fb81f89c4a1c7a5db8391ab0380970708cb9c66704d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"down\": {\n \"path\": \"crates/event_store/migrations/0004_source_maintenance.down.sql\",\n \"byte_length\": 5172,\n \"sha256\": \"fe44fd53c51545c08ea479b385e6781079dab70fc63da2a3c205d727a00ce860\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n \"schema_sha256\": \"d526d96ea02be12b4b0aed99e97cfdde17c4474ace67111506a7b900ee78b186\",\n \"catalog\": {\n \"objects\": [\n \"radroots_event_store_source_capacity_delete_guard\",\n \"radroots_event_store_source_capacity_insert_guard\",\n \"radroots_event_store_source_capacity_marker_close_guard\",\n \"radroots_event_store_source_capacity_update_guard\",\n \"radroots_event_store_source_capacity_v1\",\n \"radroots_event_store_source_generation_capacity_advance\",\n \"radroots_event_store_source_generation_capacity_guard\"\n ],\n \"replaced_objects\": [\n \"radroots_event_store_food_availability_image_delete_guard\",\n \"radroots_event_store_food_availability_projection_delete_guard\",\n \"radroots_event_store_source_rebuild_marker_insert_guard\"\n ],\n \"tables\": [\n \"radroots_event_store_source_capacity_v1\"\n ],\n \"fts5_tables\": []\n }\n },\n \"source_maintenance\": {\n \"version\": 1,\n \"event_contract_registry_version\": 7,\n \"capacity_authority_id\": \"radroots_event_store_source_capacity_v1\",\n \"accounting\": {\n \"algorithm\": \"sqlite_cast_blob_octet_sum_v1\",\n \"raw_event_columns\": [\n \"event_id\",\n \"pubkey\",\n \"tags_json\",\n \"content\",\n \"sig\",\n \"raw_json\"\n ],\n \"raw_tag_columns\": [\n \"event_id\",\n \"tag_name\",\n \"tag_value\",\n \"tag_json\"\n ],\n \"nullable_raw_tag_columns\": [\n \"tag_value\"\n ]\n },\n \"limits\": {\n \"raw_events\": 25000,\n \"raw_tags\": 250000,\n \"raw_event_text_bytes\": 67108864,\n \"raw_tag_text_bytes\": 33554432,\n \"retained_source_generations\": 8\n },\n \"reopen_validation\": {\n \"mode\": \"bounded_full_raw_recount_v1\",\n \"raw_event_rejection_scan_bound\": 25001,\n \"raw_tag_rejection_scan_bound\": 250001,\n \"generation_history_validation\": \"bounded_count_plus_active_ordinal_v1\",\n \"retained_generation_rejection_scan_bound\": 9\n },\n \"rebuild_seal\": {\n \"nip09_hook_id\": \"nip09_reconciliation_v1\",\n \"nip09_manifest_sha256\": \"74af832420ffbaa9805e89df3c0b34f126a443e1598f757e3372f407f9003b77\",\n \"food_hook_id\": \"food_availability_projection_v1\",\n \"food_manifest_sha256\": \"33b93a3c87ce428e8aa6f5e92643c77203d9aa006c53ce96f3562fe6d68ffd23\",\n \"food_scope_fingerprint_sha256\": \"8b63c5ddc48a2cc7db69295238b96d5f814dba50427c80b4d0079f061e6d3de0\",\n \"active_generation_authority\": \"radroots_event_store_source_state\",\n \"marker_close_authority\": \"radroots_event_store_source_capacity_marker_close_guard\"\n }\n },\n \"entry_points\": [\n {\n \"role\": \"migration_registry\",\n \"rust_path\": \"radroots_event_store::migrations::EVENT_STORE_MIGRATIONS[3]\"\n },\n {\n \"role\": \"migration_apply_hook\",\n \"rust_path\": \"radroots_event_store::schema::apply_migration_hook\"\n },\n {\n \"role\": \"migration_validation_hook\",\n \"rust_path\": \"radroots_event_store::schema::validate_migration_hook_state\"\n },\n {\n \"role\": \"capacity_query\",\n \"rust_path\": \"radroots_event_store::RadrootsEventStore::source_capacity_v1\"\n },\n {\n \"role\": \"raw_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_unique_raw_source_append_v1\"\n },\n {\n \"role\": \"raw_append_advance\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::advance_source_capacity_after_insert_v1\"\n },\n {\n \"role\": \"generation_append_preflight\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::preflight_source_generation_append_v1\"\n },\n {\n \"role\": \"generation_rebuild_bind\",\n \"rust_path\": \"radroots_event_store::source_maintenance_v1::bind_source_capacity_to_generation_v1\"\n },\n {\n \"role\": \"sqlite_encoding_preflight\",\n \"rust_path\": \"radroots_event_store::store::validate_main_database_encoding\"\n },\n {\n \"role\": \"source_generation_history_rollback_guard\",\n \"rust_path\": \"radroots_event_store::schema::validate_rollback_preserves_source_generation_history\"\n },\n {\n \"role\": \"result_vector_executor\",\n \"rust_path\": \"source_maintenance_v1_result_vector\"\n }\n ],\n \"source_files\": [\n {\n \"role\": \"workspace_dependency_authority\",\n \"path\": \"Cargo.toml\",\n \"byte_length\": 11506,\n \"sha256\": \"af871d7567594f75da8385691297a5ce170f51ae54c4c318db10b99d458a3335\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_currency_value_authority\",\n \"path\": \"crates/core/src/currency.rs\",\n \"byte_length\": 4312,\n \"sha256\": \"adfae0ef03de95c2cd3bd7175f32a6c12723dace67b344367fad0f4d4af1e603\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_decimal_value_authority\",\n \"path\": \"crates/core/src/decimal.rs\",\n \"byte_length\": 8400,\n \"sha256\": \"93267e36f093ea64e488351bec54c5a142ba94c6de6985f352e9d75419d3e0ab\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_money_value_authority\",\n \"path\": \"crates/core/src/money.rs\",\n \"byte_length\": 9147,\n \"sha256\": \"4f761da36418b2b0b2c909b55b4d80a42d14983b59fc1ebf118862557b527a93\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_percent_value_authority\",\n \"path\": \"crates/core/src/percent.rs\",\n \"byte_length\": 3268,\n \"sha256\": \"4cfe54a631f911947bcc6e3f0d1607214d88009e87669589cf30b1e078bf8ca0\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_value_authority\",\n \"path\": \"crates/core/src/quantity.rs\",\n \"byte_length\": 8035,\n \"sha256\": \"a8bcfe6b9038d3baf7a833f811cdd7ac467dc670123a156918ab208e901fdaf5\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_quantity_price_value_authority\",\n \"path\": \"crates/core/src/quantity_price.rs\",\n \"byte_length\": 8988,\n \"sha256\": \"0b6fa25c5ad4c7d809bd81cdc2c054f5c4ad9d7963fe0824843df5ce9acddba7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"core_unit_value_authority\",\n \"path\": \"crates/core/src/unit.rs\",\n \"byte_length\": 10436,\n \"sha256\": \"fe17fb780c75a18d3b4678d9268726ba97814ed8da7775b65a03724df120560f\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"event_store_error_and_limits\",\n \"path\": \"crates/event_store/src/error.rs\",\n \"byte_length\": 19421,\n \"sha256\": \"4772e041cb20a4963afb2f3159804c777e2f2be61bfdb6ee267e7a7c04258972\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"generated_descriptor_registration\",\n \"path\": \"crates/event_store/src/generated.rs\",\n \"byte_length\": 144,\n \"sha256\": \"6b0a8d6f249bd4fc3f878d37cb5e418680f0f1be2d9eec2518dedf03efc47121\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_surface\",\n \"path\": \"crates/event_store/src/lib.rs\",\n \"byte_length\": 3844,\n \"sha256\": \"3cd9653bcb752fb3c4442d4904b98a0a6208011a9a238125b7b7073d7f4e312b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"migration_registry\",\n \"path\": \"crates/event_store/src/migrations.rs\",\n \"byte_length\": 73585,\n \"sha256\": \"a47477d04759ec6e71d14d3d05459864eae78fce0e3c2481a1b03147f04637f7\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_model_public_surface\",\n \"path\": \"crates/event_store/src/model.rs\",\n \"byte_length\": 33617,\n \"sha256\": \"79296b8f263aa06d17005795e4515f769f064ea6fd971eeb1296e1151debaf20\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_generation_rebuild_authority\",\n \"path\": \"crates/event_store/src/nip09/reconciliation_v1.rs\",\n \"byte_length\": 184407,\n \"sha256\": \"c455d40fc736e3db264f567c7809af7bd897d89be8a33dfb21667a6ef6b8d6c6\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"schema_migration_and_reopen_authority\",\n \"path\": \"crates/event_store/src/schema.rs\",\n \"byte_length\": 146146,\n \"sha256\": \"93b060e80d3edd73f86208e4bf698fa9d53eaf1eeb04526c9261fb8b5726fb0d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"public_store_and_transaction_authority\",\n \"path\": \"crates/event_store/src/store.rs\",\n \"byte_length\": 394574,\n \"sha256\": \"db57dc3e35e64c7194683142fe55edba853671a829269449dd2273056dfc3a0e\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"raw_ingest_capacity_authority\",\n \"path\": \"crates/event_store/src/store/protocol_reconciliation_v1.rs\",\n \"byte_length\": 30140,\n \"sha256\": \"210112eeaa6975a3b4fbb97d5c52588f8c6d8d07975e531d39737fd11235de51\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_runtime\",\n \"path\": \"crates/event_store/src/source_maintenance_v1.rs\",\n \"byte_length\": 51756,\n \"sha256\": \"f8d5b62f0613104aa86658d5bf1baade92c7df83f00ef0cddadd734b9797afca\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"artifact_transaction_authority\",\n \"path\": \"tools/xtask/src/contract/artifact_bundle.rs\",\n \"byte_length\": 38279,\n \"sha256\": \"f326ea57b56d40135f95b6b1e15961f66eed363337180a6d12a5ea903e1a9a29\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"predecessor_successor_governance\",\n \"path\": \"tools/xtask/src/contract/food_availability_projection.rs\",\n \"byte_length\": 194594,\n \"sha256\": \"4b2961fc69c8204ad5470c0e51c93533063e49d7a14da00a53598c3e8d0cdf6b\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"transitive_predecessor_membership_governance\",\n \"path\": \"tools/xtask/src/contract/nip09_reconciliation.rs\",\n \"byte_length\": 835558,\n \"sha256\": \"51a01360e2c32c83056cb36e22b850e6b09d2ad4e4a749097564b4a100709a16\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"source_maintenance_governance\",\n \"path\": \"tools/xtask/src/contract/source_maintenance.rs\",\n \"byte_length\": 177996,\n \"sha256\": \"c309da38f12101d77c4de1caa49ef443252cac6238b49692182851c2cf894bda\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"contract_command_authority\",\n \"path\": \"tools/xtask/src/contract.rs\",\n \"byte_length\": 500241,\n \"sha256\": \"972e2a6b1099267e11d9a34e73fd38b20fc217a3dd79bd9a0f1f060b680ba2ba\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n },\n {\n \"role\": \"xtask_dispatch_and_release_preflight\",\n \"path\": \"tools/xtask/src/main.rs\",\n \"byte_length\": 14976,\n \"sha256\": \"42b59a8576e4f86b091941a64349ac9eaa20b1e3b52aef367bb3d5947d8ccd1d\",\n \"hash_algorithm\": \"sha256_bytes_v1\"\n }\n ],\n \"public_api\": {\n \"inherited_predecessor_symbols\": [\n \"RADROOTS_ADDRESSABLE_TRANSITION_CURSOR_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_D_TAG_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_FEED_VERSION_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_LIMIT_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_RAW_JSON_MAX_BYTES_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_PAGE_SCAN_MAX_V1\",\n \"RADROOTS_ADDRESSABLE_TRANSITION_SCOPE_KIND_MAX_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_APPLY_PAGE_LIMIT_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_PROJECTION_VERSION_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_BYTES_V1\",\n \"RADROOTS_FOOD_AVAILABILITY_SEARCH_QUERY_MAX_TERMS_V1\",\n \"RadrootsAddressableTransitionCauseV1\",\n \"RadrootsAddressableTransitionCoordinateV1\",\n \"RadrootsAddressableTransitionCursorV1\",\n \"RadrootsAddressableTransitionEventReferenceV1\",\n \"RadrootsAddressableTransitionOriginV1\",\n \"RadrootsAddressableTransitionPageV1\",\n \"RadrootsAddressableTransitionRawHeadDecisionV1\",\n \"RadrootsAddressableTransitionScopeFingerprintV1\",\n \"RadrootsAddressableTransitionScopeV1\",\n \"RadrootsAddressableTransitionV1\",\n \"RadrootsAddressableTransitionVisibilityV1\",\n \"RadrootsCurrentEventVisibilityV1\",\n \"RadrootsCurrentVisibilityDecisionV1\",\n \"RadrootsFoodAvailabilitySearchQueryV1\",\n \"RadrootsFoodAvailabilityStatusFilterV1\",\n \"RadrootsNip09SuppressionEvidenceV1\",\n \"RadrootsNip09SuppressionOutcome\",\n \"RadrootsNip09SuppressionReason\",\n \"RadrootsStoreProducedCanonicalEventV1\",\n \"RadrootsStoredFoodAvailabilityImageV1\",\n \"RadrootsStoredFoodAvailabilityV1\"\n ],\n \"added_symbols\": [\n \"RADROOTS_EVENT_STORE_RAW_EVENT_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_EVENT_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_COUNT_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RAW_TAG_TEXT_BYTES_LIMIT_V1\",\n \"RADROOTS_EVENT_STORE_RETAINED_SOURCE_GENERATION_LIMIT_V1\",\n \"RadrootsEventStoreSourceCapacityResourceV1\",\n \"RadrootsEventStoreSourceCapacityV1\"\n ],\n \"methods\": [\n \"RadrootsEventStore::source_capacity_v1\",\n \"RadrootsEventStoreSourceCapacityResourceV1::as_str\",\n \"RadrootsEventStoreSourceCapacityV1::source_generation\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_count\",\n \"RadrootsEventStoreSourceCapacityV1::raw_event_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_tag_text_bytes\",\n \"RadrootsEventStoreSourceCapacityV1::raw_high_water_seq\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_count\",\n \"RadrootsEventStoreSourceCapacityV1::retained_generation_limit\"\n ],\n \"error_variants\": [\n \"SourceCapacityExceeded\",\n \"SourceGenerationHistoryLimitReached\",\n \"PersistedEphemeralRawEvent\",\n \"SourceCapacityStateDrift\",\n \"SqliteMainDatabaseEncodingNotUtf8\",\n \"RollbackWouldDiscardSourceGenerationHistory\"\n ],\n \"removed_symbols\": [\n \"RadrootsEventStoreReconciliationResource\",\n \"RadrootsEventStoreError::ReconciliationCapacityExceeded\"\n ],\n \"breaking_replacements\": [\n {\n \"removed\": \"RadrootsEventStoreReconciliationResource\",\n \"replacement\": \"RadrootsEventStoreSourceCapacityResourceV1\"\n },\n {\n \"removed\": \"RadrootsEventStoreError::ReconciliationCapacityExceeded\",\n \"replacement\": \"RadrootsEventStoreError::SourceCapacityExceeded\"\n }\n ]\n },\n \"result_vector\": {\n \"canonical_path\": \"contracts/conformance/vectors/event_store/source_maintenance.v1.json\",\n \"mirror_path\": \"crates/event_store/tests/fixtures/source_maintenance.v1.json\",\n \"byte_length\": 16253,\n \"sha256\": \"997aba2604a2b9d199fb87dc9d07942ca50d91863aeadcf3eeacf16d191dd71f\",\n \"hash_algorithm\": \"sha256_bytes_v1\",\n \"executor_id\": \"radroots_event_store.source_maintenance_v1.result_vector_executor.v1\",\n \"executor_path\": \"crates/event_store/tests/source_maintenance_v1_result_vector.rs\",\n \"executor_test\": \"source_maintenance_v1_result_vector\",\n \"executor_byte_length\": 23510,\n \"executor_sha256\": \"a7487afdfe19fc5fc794811d0f0e6035203e1aabcf0a33a1d398f6b3555d38f3\",\n \"executor_hash_algorithm\": \"sha256_bytes_v1\"\n }\n}\n"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_BYTE_LENGTH: usize = 16260; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SHA256: &str = - "26932b5fec2167ef24f14474205c5556371ecd1765b268e5e43757a8be780147"; + "ecfb3ba4e68e5628d880fd8dfc8f7f1cbbc4123ce1f6510113107210682b38d9"; pub(crate) const SOURCE_MAINTENANCE_MANIFEST_SCHEMA_VERSION: u32 = 1; pub(crate) const SOURCE_MAINTENANCE_CONTRACT_ID: &str = "radroots_event_store.source_maintenance_v1"; diff --git a/tools/xtask/src/architecture.rs b/tools/xtask/src/architecture.rs @@ -7,6 +7,7 @@ use std::{ use serde::Deserialize; mod api_leakage; +mod core_contract; mod dependency_boundary; const DEVIATIONS_RELATIVE: &str = "docs/implementation/deviations.toml"; @@ -159,6 +160,7 @@ pub fn validate(workspace_root: &Path) -> Result<(), String> { .iter() .map(|package| package.name.clone()) .collect::<BTreeSet<_>>(); + core_contract::validate(workspace_root)?; api_leakage::validate_policy_catalog(workspace_root, &architecture_packages)?; dependency_boundary::validate_policy_catalog(workspace_root, &architecture_packages)?; validate_workspace_toolchain(workspace_root, &architecture)?; diff --git a/tools/xtask/src/architecture/core_contract.rs b/tools/xtask/src/architecture/core_contract.rs @@ -0,0 +1,655 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::{Path, PathBuf}, +}; + +use syn::{Item, UseTree, Visibility}; + +const ARCHITECTURE_RELATIVE: &str = "docs/specs/radroots_crates_release_v1.toml"; +const CORE_MANIFEST_RELATIVE: &str = "crates/core/Cargo.toml"; +const CORE_LIB_RELATIVE: &str = "crates/core/src/lib.rs"; + +const CORE_RUNTIME_DEPENDENCIES: [&str; 2] = ["rust_decimal", "serde"]; +const CORE_DEV_DEPENDENCIES: [&str; 3] = ["dto_bindgen", "rust_decimal", "serde_json"]; +const CORE_ROOT_EXPORTS: [(&str, &str); 8] = [ + ("Currency", "currency::Currency"), + ("Decimal", "decimal::Decimal"), + ("Error", "error::Error"), + ("Money", "money::Money"), + ("Percent", "percent::Percent"), + ("Quantity", "quantity::Quantity"), + ("QuantityPrice", "pricing::QuantityPrice"), + ("Unit", "unit::Unit"), +]; +const TEMPORARY_ROOT_EXPORTS: [(&str, &str); 20] = [ + ("RadrootsCoreCurrency", "currency::RadrootsCoreCurrency"), + ( + "RadrootsCoreCurrencyParseError", + "currency::RadrootsCoreCurrencyParseError", + ), + ("RadrootsCoreDecimal", "decimal::RadrootsCoreDecimal"), + ("RadrootsCoreDiscount", "discount::RadrootsCoreDiscount"), + ( + "RadrootsCoreDiscountScope", + "discount::RadrootsCoreDiscountScope", + ), + ( + "RadrootsCoreDiscountThreshold", + "discount::RadrootsCoreDiscountThreshold", + ), + ( + "RadrootsCoreDiscountValue", + "discount::RadrootsCoreDiscountValue", + ), + ("RadrootsCoreMoney", "money::RadrootsCoreMoney"), + ( + "RadrootsCoreMoneyInvariantError", + "money::RadrootsCoreMoneyInvariantError", + ), + ("RadrootsCorePercent", "percent::RadrootsCorePercent"), + ( + "RadrootsCorePercentParseError", + "percent::RadrootsCorePercentParseError", + ), + ("RadrootsCoreQuantity", "quantity::RadrootsCoreQuantity"), + ( + "RadrootsCoreQuantityInvariantError", + "quantity::RadrootsCoreQuantityInvariantError", + ), + ( + "RadrootsCoreQuantityPrice", + "quantity_price::RadrootsCoreQuantityPrice", + ), + ( + "RadrootsCoreQuantityPriceError", + "quantity_price::RadrootsCoreQuantityPriceError", + ), + ( + "RadrootsCoreQuantityPriceOps", + "quantity_price::RadrootsCoreQuantityPriceOps", + ), + ("RadrootsCoreUnit", "unit::RadrootsCoreUnit"), + ( + "RadrootsCoreUnitConvertError", + "unit::RadrootsCoreUnitConvertError", + ), + ( + "RadrootsCoreUnitDimension", + "unit::RadrootsCoreUnitDimension", + ), + ( + "RadrootsCoreUnitParseError", + "unit::RadrootsCoreUnitParseError", + ), +]; + +pub(super) fn validate(workspace_root: &Path) -> Result<(), String> { + let architecture = read_toml(workspace_root, ARCHITECTURE_RELATIVE)?; + let core_spec = architecture + .get("package") + .and_then(toml::Value::as_array) + .and_then(|packages| { + packages.iter().find(|package| { + package.get("name").and_then(toml::Value::as_str) == Some("radroots-core") + }) + }) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{ARCHITECTURE_RELATIVE} is missing radroots-core"))?; + + let manifest = read_toml(workspace_root, CORE_MANIFEST_RELATIVE)?; + validate_manifest(core_spec, &manifest)?; + validate_crate_root(workspace_root, core_spec) +} + +fn validate_manifest(core_spec: &toml::value::Table, manifest: &toml::Value) -> Result<(), String> { + let manifest = manifest + .as_table() + .ok_or_else(|| format!("{CORE_MANIFEST_RELATIVE} must be a TOML table"))?; + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{CORE_MANIFEST_RELATIVE} is missing [package]"))?; + if package.get("name").and_then(toml::Value::as_str) != Some("radroots-core") { + return Err(format!( + "{CORE_MANIFEST_RELATIVE} package.name must be radroots-core" + )); + } + let library = manifest + .get("lib") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{CORE_MANIFEST_RELATIVE} is missing [lib]"))?; + if library.get("name").and_then(toml::Value::as_str) != Some("radroots_core") + || library.keys().map(String::as_str).collect::<BTreeSet<_>>() != BTreeSet::from(["name"]) + { + return Err(format!( + "{CORE_MANIFEST_RELATIVE} must use the conventional radroots_core library target" + )); + } + + let features = manifest + .get("features") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{CORE_MANIFEST_RELATIVE} is missing [features]"))?; + let expected_features = spec_strings(core_spec, "features")?; + let actual_features = features + .keys() + .filter(|name| name.as_str() != "default") + .cloned() + .collect::<BTreeSet<_>>(); + if actual_features != expected_features { + return Err(format!( + "radroots-core public features must be exactly {expected_features:?}, found {actual_features:?}" + )); + } + let expected_default = spec_strings(core_spec, "default_features")?; + let actual_default = value_strings( + features.get("default"), + &format!("{CORE_MANIFEST_RELATIVE} features.default"), + )?; + if actual_default != expected_default { + return Err(format!( + "radroots-core default features must be exactly {expected_default:?}, found {actual_default:?}" + )); + } + let std_enables = value_strings( + features.get("std"), + &format!("{CORE_MANIFEST_RELATIVE} features.std"), + )?; + if !std_enables.is_empty() { + return Err("radroots-core std must remain an empty additive marker".to_owned()); + } + let serde_enables = value_strings( + features.get("serde"), + &format!("{CORE_MANIFEST_RELATIVE} features.serde"), + )?; + let expected_serde = BTreeSet::from(["dep:serde".to_owned(), "rust_decimal/serde".to_owned()]); + if serde_enables != expected_serde { + return Err(format!( + "radroots-core serde feature must enable {expected_serde:?}, found {serde_enables:?}" + )); + } + + validate_dependency_names( + manifest, + "dependencies", + BTreeSet::from(CORE_RUNTIME_DEPENDENCIES.map(str::to_owned)), + )?; + validate_dependency_names( + manifest, + "dev-dependencies", + BTreeSet::from(CORE_DEV_DEPENDENCIES.map(str::to_owned)), + )?; + reject_nonempty_dependency_section(manifest, "build-dependencies")?; + if manifest + .get("target") + .and_then(toml::Value::as_table) + .is_some_and(|targets| { + targets.values().any(|target| { + target.as_table().is_some_and(|table| { + ["dependencies", "dev-dependencies", "build-dependencies"] + .iter() + .any(|section| { + table + .get(*section) + .and_then(toml::Value::as_table) + .is_some_and(|dependencies| !dependencies.is_empty()) + }) + }) + }) + }) + { + return Err("radroots-core must not declare target-specific dependencies".to_owned()); + } + + let dependencies = manifest + .get("dependencies") + .and_then(toml::Value::as_table) + .expect("validated dependencies"); + validate_dependency_shape(dependencies, "rust_decimal", false, false, &[])?; + validate_dependency_shape(dependencies, "serde", true, false, &["alloc", "derive"]) +} + +fn validate_crate_root( + workspace_root: &Path, + core_spec: &toml::value::Table, +) -> Result<(), String> { + let path = workspace_root.join(CORE_LIB_RELATIVE); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + let file = + syn::parse_file(&raw).map_err(|error| format!("parse {CORE_LIB_RELATIVE}: {error}"))?; + let mut modules = BTreeSet::new(); + let mut exports = BTreeMap::new(); + for item in &file.items { + match item { + Item::Mod(item) if matches!(item.vis, Visibility::Public(_)) => { + modules.insert(item.ident.to_string()); + } + Item::Use(item) if matches!(item.vis, Visibility::Public(_)) => { + collect_use_exports(&item.tree, &mut Vec::new(), &mut exports)?; + } + Item::Macro(item) + if item + .attrs + .iter() + .any(|attribute| attribute.path().is_ident("macro_export")) => + { + return Err(format!( + "{CORE_LIB_RELATIVE} must not declare a root macro export" + )); + } + item if is_public(item) => { + return Err(format!( + "{CORE_LIB_RELATIVE} contains an unsupported public root item: {}", + public_item_kind(item) + )); + } + _ => {} + } + } + + let expected_modules = spec_strings(core_spec, "modules")?; + if modules != expected_modules { + return Err(format!( + "radroots-core public modules must be exactly {expected_modules:?}, found {modules:?}" + )); + } + let specified_exports = spec_strings(core_spec, "root_exports")?; + let canonical_exports = CORE_ROOT_EXPORTS + .iter() + .map(|(name, _)| (*name).to_owned()) + .collect::<BTreeSet<_>>(); + if specified_exports != canonical_exports { + return Err(format!( + "radroots-core canonical root exports must be exactly {canonical_exports:?}, found {specified_exports:?}" + )); + } + let expected_exports = CORE_ROOT_EXPORTS + .into_iter() + .chain(TEMPORARY_ROOT_EXPORTS) + .map(|(name, source)| (name.to_owned(), source.to_owned())) + .collect::<BTreeMap<_, _>>(); + if exports != expected_exports { + return Err(format!( + "radroots-core root exports must be the canonical contract plus the Step 034 compatibility set; expected {expected_exports:?}, found {exports:?}" + )); + } + Ok(()) +} + +fn validate_dependency_names( + manifest: &toml::value::Table, + section: &str, + expected: BTreeSet<String>, +) -> Result<(), String> { + let actual = manifest + .get(section) + .and_then(toml::Value::as_table) + .map(|dependencies| dependencies.keys().cloned().collect::<BTreeSet<_>>()) + .unwrap_or_default(); + if actual != expected { + return Err(format!( + "radroots-core {section} must be exactly {expected:?}, found {actual:?}" + )); + } + Ok(()) +} + +fn reject_nonempty_dependency_section( + manifest: &toml::value::Table, + section: &str, +) -> Result<(), String> { + if manifest + .get(section) + .and_then(toml::Value::as_table) + .is_some_and(|dependencies| !dependencies.is_empty()) + { + return Err(format!("radroots-core must not declare {section}")); + } + Ok(()) +} + +fn validate_dependency_shape( + dependencies: &toml::value::Table, + name: &str, + optional: bool, + default_features: bool, + expected_features: &[&str], +) -> Result<(), String> { + let dependency = dependencies + .get(name) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("radroots-core dependency {name} must use a dependency table"))?; + if dependency.get("workspace").and_then(toml::Value::as_bool) != Some(true) + || dependency + .get("optional") + .and_then(toml::Value::as_bool) + .unwrap_or(false) + != optional + || dependency + .get("default-features") + .and_then(toml::Value::as_bool) + .unwrap_or(true) + != default_features + { + return Err(format!( + "radroots-core dependency {name} has a noncanonical workspace/optional/default-features shape" + )); + } + let mut expected_keys = BTreeSet::from(["workspace", "default-features"]); + if optional { + expected_keys.insert("optional"); + } + if !expected_features.is_empty() { + expected_keys.insert("features"); + } + let actual_keys = dependency + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + if actual_keys != expected_keys { + return Err(format!( + "radroots-core dependency {name} keys must be exactly {expected_keys:?}, found {actual_keys:?}" + )); + } + let actual_features = match dependency.get("features") { + Some(features) => value_strings( + Some(features), + &format!("{CORE_MANIFEST_RELATIVE} dependencies.{name}.features"), + )?, + None => BTreeSet::new(), + }; + let expected_features = expected_features + .iter() + .map(|feature| (*feature).to_owned()) + .collect::<BTreeSet<_>>(); + if actual_features != expected_features { + return Err(format!( + "radroots-core dependency {name} features must be {expected_features:?}, found {actual_features:?}" + )); + } + Ok(()) +} + +fn spec_strings(package: &toml::value::Table, field: &str) -> Result<BTreeSet<String>, String> { + value_strings( + package.get(field), + &format!("{ARCHITECTURE_RELATIVE} radroots-core.{field}"), + ) +} + +fn value_strings(value: Option<&toml::Value>, label: &str) -> Result<BTreeSet<String>, String> { + value + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{label} must be an array"))? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_owned) + .ok_or_else(|| format!("{label} must contain strings")) + }) + .collect() +} + +fn collect_use_exports( + tree: &UseTree, + prefix: &mut Vec<String>, + exports: &mut BTreeMap<String, String>, +) -> Result<(), String> { + match tree { + UseTree::Path(path) => { + prefix.push(path.ident.to_string()); + collect_use_exports(&path.tree, prefix, exports)?; + prefix.pop(); + } + UseTree::Name(name) => { + let source_name = name.ident.to_string(); + let export_name = if source_name == "self" { + prefix.last().cloned().ok_or_else(|| { + format!("{CORE_LIB_RELATIVE} contains an invalid root self re-export") + })? + } else { + source_name.clone() + }; + let source = if source_name == "self" { + prefix.join("::") + } else { + qualified_source(prefix, &source_name) + }; + insert_export(exports, export_name, source)?; + } + UseTree::Rename(rename) if rename.rename.to_string() != "_" => { + insert_export( + exports, + rename.rename.to_string(), + qualified_source(prefix, &rename.ident.to_string()), + )?; + } + UseTree::Rename(_) => {} + UseTree::Glob(_) => { + return Err(format!( + "{CORE_LIB_RELATIVE} must not use public glob re-exports" + )); + } + UseTree::Group(group) => { + for item in &group.items { + collect_use_exports(item, prefix, exports)?; + } + } + } + Ok(()) +} + +fn qualified_source(prefix: &[String], name: &str) -> String { + if prefix.is_empty() { + name.to_owned() + } else { + format!("{}::{name}", prefix.join("::")) + } +} + +fn insert_export( + exports: &mut BTreeMap<String, String>, + name: String, + source: String, +) -> Result<(), String> { + if let Some(previous) = exports.insert(name.clone(), source.clone()) { + return Err(format!( + "{CORE_LIB_RELATIVE} exports {name} more than once from {previous} and {source}" + )); + } + Ok(()) +} + +fn is_public(item: &Item) -> bool { + match item { + Item::Const(item) => matches!(item.vis, Visibility::Public(_)), + Item::Enum(item) => matches!(item.vis, Visibility::Public(_)), + Item::ExternCrate(item) => matches!(item.vis, Visibility::Public(_)), + Item::Fn(item) => matches!(item.vis, Visibility::Public(_)), + Item::Static(item) => matches!(item.vis, Visibility::Public(_)), + Item::Struct(item) => matches!(item.vis, Visibility::Public(_)), + Item::Trait(item) => matches!(item.vis, Visibility::Public(_)), + Item::TraitAlias(item) => matches!(item.vis, Visibility::Public(_)), + Item::Type(item) => matches!(item.vis, Visibility::Public(_)), + Item::Union(item) => matches!(item.vis, Visibility::Public(_)), + _ => false, + } +} + +fn public_item_kind(item: &Item) -> &'static str { + match item { + Item::Const(_) => "const", + Item::Enum(_) => "enum", + Item::ExternCrate(_) => "extern crate", + Item::Fn(_) => "function", + Item::Static(_) => "static", + Item::Struct(_) => "struct", + Item::Trait(_) => "trait", + Item::TraitAlias(_) => "trait alias", + Item::Type(_) => "type alias", + Item::Union(_) => "union", + _ => "item", + } +} + +fn read_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> { + let path = workspace_path(workspace_root, relative); + let raw = + fs::read_to_string(&path).map_err(|error| format!("read {}: {error}", path.display()))?; + raw.parse::<toml::Value>() + .map_err(|error| format!("parse {}: {error}", path.display())) +} + +fn workspace_path(workspace_root: &Path, relative: &str) -> PathBuf { + workspace_root.join(relative) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::validate; + + const SPEC: &str = r#" +[[package]] +name = "radroots-core" +features = ["std", "serde"] +default_features = ["std", "serde"] +modules = ["currency", "decimal", "money", "percent", "pricing", "quantity", "unit"] +root_exports = ["Currency", "Decimal", "Money", "Percent", "Quantity", "QuantityPrice", "Unit", "Error"] +"#; + + const MANIFEST: &str = r#" +[package] +name = "radroots-core" + +[lib] +name = "radroots_core" + +[features] +default = ["std", "serde"] +std = [] +serde = ["dep:serde", "rust_decimal/serde"] + +[dependencies] +rust_decimal = { workspace = true, default-features = false } +serde = { workspace = true, default-features = false, features = ["alloc", "derive"], optional = true } + +[dev-dependencies] +dto_bindgen = { workspace = true } +rust_decimal = { workspace = true } +serde_json = { workspace = true } +"#; + + fn fixture() -> tempfile::TempDir { + let root = tempdir().expect("temporary workspace"); + fs::create_dir_all(root.path().join("docs/specs")).expect("spec directory"); + fs::create_dir_all(root.path().join("crates/core/src")).expect("core directory"); + fs::write( + root.path() + .join("docs/specs/radroots_crates_release_v1.toml"), + SPEC, + ) + .expect("architecture spec"); + fs::write(root.path().join("crates/core/Cargo.toml"), MANIFEST).expect("core manifest"); + let exports = super::CORE_ROOT_EXPORTS + .into_iter() + .chain(super::TEMPORARY_ROOT_EXPORTS) + .map(|(_, source)| format!("pub use {source};")) + .collect::<Vec<_>>() + .join(" "); + fs::write( + root.path().join("crates/core/src/lib.rs"), + format!( + "pub mod currency; pub mod decimal; pub mod money; pub mod percent; pub mod pricing; pub mod quantity; pub mod unit; {exports}" + ), + ) + .expect("core root"); + root + } + + #[test] + fn accepts_exact_core_contract() { + let root = fixture(); + validate(root.path()).expect("exact core contract"); + } + + #[test] + fn rejects_public_codegen_feature_and_runtime_dependency() { + let root = fixture(); + let manifest_path = root.path().join("crates/core/Cargo.toml"); + let manifest = fs::read_to_string(&manifest_path).expect("manifest"); + fs::write( + &manifest_path, + manifest.replace("std = []", "std = []\ndto-bindgen = [\"dep:dto_bindgen\"]"), + ) + .expect("mutated manifest"); + let error = validate(root.path()).expect_err("codegen feature must fail"); + assert!(error.contains("public features must be exactly"), "{error}"); + + fs::write( + &manifest_path, + MANIFEST.replace( + "[dev-dependencies]", + "tokio = { workspace = true }\n\n[dev-dependencies]", + ), + ) + .expect("runtime dependency manifest"); + let error = validate(root.path()).expect_err("runtime dependency must fail"); + assert!(error.contains("dependencies must be exactly"), "{error}"); + + fs::write( + &manifest_path, + MANIFEST.replace( + "rust_decimal = { workspace = true, default-features = false }", + "rust_decimal = { workspace = true, default-features = false, features = [] }", + ), + ) + .expect("noncanonical dependency shape"); + let error = validate(root.path()).expect_err("extra dependency keys must fail"); + assert!(error.contains("keys must be exactly"), "{error}"); + } + + #[test] + fn rejects_extra_public_module_or_export() { + let root = fixture(); + let lib_path = root.path().join("crates/core/src/lib.rs"); + let source = fs::read_to_string(&lib_path).expect("core root"); + fs::write(&lib_path, format!("{source} pub mod serde_ext;")).expect("extra module source"); + let error = validate(root.path()).expect_err("extra module must fail"); + assert!(error.contains("public modules must be exactly"), "{error}"); + + fs::write(&lib_path, format!("{source} pub use fixture::Clock;")) + .expect("extra export source"); + let error = validate(root.path()).expect_err("extra export must fail"); + assert!(error.contains("root exports must be"), "{error}"); + } + + #[test] + fn rejects_rebound_duplicate_and_glob_exports() { + let root = fixture(); + let lib_path = root.path().join("crates/core/src/lib.rs"); + let source = fs::read_to_string(&lib_path).expect("core root"); + fs::write( + &lib_path, + source.replace("pub use currency::Currency;", "pub use money::Currency;"), + ) + .expect("rebound export source"); + let error = validate(root.path()).expect_err("rebound export must fail"); + assert!(error.contains("root exports must be"), "{error}"); + + fs::write(&lib_path, format!("{source} pub use currency::*;")).expect("glob export source"); + let error = validate(root.path()).expect_err("glob export must fail"); + assert!(error.contains("must not use public glob"), "{error}"); + + fs::write(&lib_path, format!("{source} pub use currency::Currency;")) + .expect("duplicate export source"); + let error = validate(root.path()).expect_err("duplicate export must fail"); + assert!(error.contains("exports Currency more than once"), "{error}"); + } +}