Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

specification-core Guide

specification-core is an idiomatic Rust implementation of the Specification Pattern. Release 0.1.0 provides a synchronous core, explicit dynamic and thread-sharing boundaries, typed decisions, immutable evaluation context, deterministic time rules, a runtime-neutral async layer, and opt-in serde and macro crates.

This book is the reader-facing guide. The generated API reference contains complete signatures and item-level rustdoc. The same crates are also available on docs.rs.

Start here

What is in 0.1.0

The released API includes boolean composition, AllOf/AnyOf, BoxedSpecification, SharedSpecification, FirstMatch, EvaluationContext, MaxCount, Flag, Cooldown, AsyncSpecification, RuleDocument, and the #[specification(Name)] predicate macro.

Date-range rules, segment providers, property wrappers, Combine observation, and global context providers are intentionally outside the Rust 0.1.0 scope. Use ordinary closures or application-owned adapters for domain-specific rules.

Documentation surfaces

  • This book explains concepts, workflows, design trade-offs, and Swift migration.
  • rustdoc/docs.rs is the authoritative API reference and compiles focused examples as doctests.
  • Cargo examples and integration tests are the executable end-to-end scenarios linked from the relevant chapters.

Getting Started

This chapter takes a small eligibility rule from a closure to a reusable, typed decision. The complete static version is also available as static_composition.rs.

Add the core crate to an application:

[dependencies]
specification-core = "0.1"

Specification<T> evaluates a borrowed candidate. A closure with the shape Fn(&T) -> bool implements the trait automatically, so a small rule needs no boilerplate type.

use specification_core::Specification;

struct User {
    age: u8,
    verified: bool,
}

let is_adult = |user: &User| user.age >= 18;
let is_verified = |user: &User| user.verified;
let eligible = is_adult.and(is_verified);

let user = User { age: 21, verified: true };
assert!(eligible.is_satisfied_by(&user));

eligible is a concrete And<...> value. The static composition path does not allocate or use dynamic dispatch, and and/or short-circuit from left to right. Rust receives &user because evaluation borrows the candidate rather than taking ownership of it.

use specification_core::Specification;

let is_adult = |age: &u8| *age >= 18;
let is_retired = |age: &u8| *age >= 65;
let working_age = is_adult.and(is_retired.not());

assert!(working_age.is_satisfied_by(&42));
assert!(!working_age.is_satisfied_by(&70));

AllOf and AnyOf apply the same behavior to a borrowed homogeneous slice; their empty identities are true and false, respectively.

Where to go next

The rust,ignore blocks in this book are illustrative because mdBook does not link standalone snippets to the workspace. Every complete workflow is backed by a Cargo example, integration test, or compiled rustdoc example; links to those sources are provided in the relevant chapter.

Core Concepts

The library applies the Specification Pattern: a small rule evaluates a candidate, and larger rules are built by composing those values. Rust’s trait system makes the static path the default; dynamic dispatch, heap allocation, and thread sharing are explicit choices.

Static and dynamic rules

Choose the smallest boundary that matches the problem:

NeedRust typeCost and guarantee
Known rules composed at compile timeAnd, Or, NotNo library allocation or trait-object dispatch
Homogeneous collectionAllOf, AnyOfBorrows a slice and short-circuits
Heterogeneous/runtime-selected rulesBoxedSpecification<T>Owns a Box<dyn Specification<T>>
Rule shared between threadsSharedSpecification<T>Owns an Arc and requires Send + Sync + 'static
use specification_core::{BoxedSpecification, Specification};

let rules: Vec<BoxedSpecification<u64>> = vec![
    BoxedSpecification::new(|value: &u64| *value > 0),
    BoxedSpecification::new(|value: &u64| *value % 2 == 0),
];

assert!(rules[1].is_satisfied_by(&42));

The complete thread-sharing scenario is in dynamic_and_concurrency.rs.

Boolean identities and short-circuiting

and, or, and not preserve left-to-right short-circuit evaluation. An empty AllOf is true, and an empty AnyOf is false. These identities let an application construct a rule from a possibly empty collection without special-case control flow.

Typed decisions

FirstMatch<T, Decision> is the explicit dynamic boundary for ordered routing. It stores heterogeneous rules with one concrete decision type, evaluates them in insertion order, and returns the first matching Option<&Decision>. Use decide_or when a fallback is required. See Decisions and Evaluation Context for a complete route-selection example.

Immutable context and dependencies

EvaluationContext<UserData> is an immutable input snapshot. Application data is typed through UserData; counters, flags, and timestamps have deliberate defaults. Time rules receive a Clock explicitly, so FixedClock can make a boundary deterministic in tests. There is no global context registry.

See Concurrency Contracts for Send + Sync behavior and Decisions and Evaluation Context for the built-in rules.

The runtime-neutral async layer is described in Async Specifications. Optional wire-format and predicate-macro support are described in Serialization and Macros.

Concurrency contracts

The synchronous API keeps thread sharing explicit. A regular Specification<T> does not require Send or Sync, so a local rule may use single-threaded state when that is the correct domain model.

Use SharedSpecification<T> when a rule must cross a thread boundary. Its constructor requires the concrete rule to be Send + Sync + 'static, and its cloneable wrapper uses an Arc internally:

The complete runnable version is dynamic_and_concurrency.rs.

The following snippet is illustrative in mdBook (the book test runner does not link standalone blocks against the workspace crate); the equivalent rustdoc example is compiled by Cargo’s doctest gate.

use specification_core::{SharedSpecification, Specification};

let rule = SharedSpecification::new(|value: &u64| *value % 2 == 0);
let worker_rule = rule.clone();

std::thread::spawn(move || assert!(worker_rule.is_satisfied_by(&2)))
    .join()
    .expect("worker thread should finish");

EvaluationContext<UserData> is immutable after construction. It can be moved or shared whenever UserData satisfies the corresponding Rust auto-traits. There is no global context registry.

Time-dependent rules receive their Clock explicitly. A mutable or shared clock is therefore an application-owned dependency: use an appropriate standard-library synchronization primitive and inject it into the rule. This keeps concurrent behavior visible in the type graph and makes deterministic tests possible with FixedClock.

Async Specifications

AsyncSpecification<T> is runtime-neutral. It returns a Send future with a typed Result<bool, Error> and does not start or own an executor. Tokio, async-std, smol, or a custom executor can await the same rule.

The runnable standard-library example is async_composition.rs.

use std::{convert::Infallible, future::{ready, Future}};
use specification_core::AsyncSpecification;

struct AtLeast(u8);

impl AsyncSpecification<u8> for AtLeast {
    type Error = Infallible;

    fn is_satisfied_by<'a>(
        &'a self,
        age: &'a u8,
    ) -> impl Future<Output = Result<bool, Self::Error>> + Send + 'a {
        ready(Ok(*age >= self.0))
    }
}

let working_age = AtLeast(18).and(AtLeast(65).not());
let result = working_age.is_satisfied_by(&42).await?;

The future borrows both the specification and candidate. Because it is Send, the executor may move the future between worker threads; T must therefore be Sync. A composed async rule requires both operands to use the same error type, usually an application-owned error enum.

and and or evaluate sequentially and short-circuit. An error from the operand that was evaluated is returned immediately; a skipped operand is never polled. not flips only the boolean and preserves the error.

Dynamic async rules

The trait uses return-position impl Future, so it is not directly object-safe. Use BoxedAsyncSpecification<T, Error> when a heterogeneous or runtime-selected collection is required. This is an explicit Send + Sync + 'static boundary and allocates one boxed future per evaluation.

Cancellation remains an executor concern: dropping the future stops observing it, while any external side effects belong to the application implementation.

Serialization and Macros

The optional integration crates keep the core dependency-free. Add only the boundary your application needs:

[dependencies]
specification-core = "0.1"
specification-core-serde = "0.1"
specification-core-macros = "0.1"

Versioned rule documents

specification-core-serde stores a declarative rule graph. It does not and cannot serialize arbitrary closures, captured state, or trait objects. The schema is versioned independently from crate SemVer and unknown versions are rejected.

The complete JSON round-trip is covered by the serde crate tests and the versioned_document.rs example:

use specification_core_serde::{RuleDocument, RuleNode};

let document = RuleDocument::new(RuleNode::AllOf {
    rules: vec![
        RuleNode::Flag { key: "premium".into() },
        RuleNode::Not {
            rule: Box::new(RuleNode::MaxCount {
                key: "attempts".into(),
                maximum: 3,
            }),
        },
    ],
});

The wire shape is tagged and deterministic:

{"schema_version":1,"rule":{"kind":"flag","key":"premium"}}

RuleDocument is data, not an evaluator. Applications must bind the decoded graph to their own typed context and clock policy.

Predicate macro

#[specification(Name)] turns one free function into a named stateless Specification type:

use specification_core::Specification;
use specification_core_macros::specification;

struct User { age: u8 }

#[specification(Adult)]
fn is_adult(user: &User) -> bool {
    user.age >= 18
}

assert!(Adult.is_satisfied_by(&User { age: 21 }));

The macro validates the immutable &Candidate argument and bool return type. It does not add context lookup, allocation, async behavior, or generated composition. The compiled integration example is tests/usage.rs.

Cross-language conformance

The Rust crate is an idiomatic implementation, not a source-level translation of the Swift package. Cross-language compatibility is therefore expressed as observable behavior in the versioned JSON corpus at crates/specification-core/fixtures/conformance/v1/.

Running the corpus

The Rust adapter is an integration test and is included in the normal test gate:

make test

It reads the manifest, executes every listed fixture, and verifies both the outcome and the named evaluation trace. A missing fixture, invalid JSON, or an unknown schema version fails the test.

Adapter contract

An adapter for another implementation must:

  1. Read manifest.json, then each listed JSON fixture.
  2. Require the document’s schema_version to be supported.
  3. Evaluate each case with the supplied candidate and context input.
  4. Compare expected.result or expected.decision, plus the ordered expected.trace.
  5. Report unsupported fixture groups explicitly; it must not silently skip them.

The trace consists of probe names, not internal method calls. It specifies the observable ordering and short-circuit behavior of composition and first-match rules.

Semantics in v1

GroupContract
compositionAND and OR short-circuit left-to-right; NOT negates; empty all-of is true and empty any-of is false.
first_matchRules are ordered; the first satisfied rule wins; no match is None; an explicit fallback is returned only by fallback evaluation.
contextMissing counters are zero, missing flags are false, MaxCount is strict <, and cooldown uses >= at the boundary. Future timestamps do not satisfy cooldown, including zero duration.

All context timestamps and now_ms values are non-negative integer milliseconds in an abstract monotonic epoch. Adapters must not substitute wall clock time.

Intentional differences and limits

Rust evaluates a borrowed candidate (&T); Swift’s protocol input convention does not need to be copied to satisfy a fixture. Rust also has no global default context provider, property wrappers, Combine observation, or Apple-platform date integration. Context and clocks are injected explicitly.

The v1 corpus does not cover async scheduling or typed async errors because those would require a shared executor and error-transport contract. Rust async behavior remains specified by the AsyncSpecification API and its tests.

specification-core-serde uses a separate versioned RuleDocument schema. Conformance fixtures are test data, not a promise that every executable rule can be serialized or reconstructed.

Decisions and Evaluation Context

Use a boolean Specification<T> when the result is only yes/no. Use FirstMatch<T, Decision> when the rule set must select a typed route, tier, or policy.

A complete context scenario

The executable version of this scenario is context_and_decisions.rs.

use std::time::Duration;

use specification_core::{
    Cooldown, DecisionSpecification, EvaluationContext, FirstMatch,
    FixedClock, Flag, MaxCount, Specification,
};

let context = EvaluationContext::new("account-42")
    .with_counter("attempts", 1)
    .with_flag("enabled", true)
    .with_timestamp("last_action", Duration::from_secs(10));

let clock = FixedClock::new(Duration::from_secs(15));

assert!(MaxCount::new("attempts", 3).is_satisfied_by(&context));
assert!(Flag::new("enabled").is_satisfied_by(&context));
assert!(Cooldown::new(
    "last_action",
    Duration::from_secs(5),
    clock,
).is_satisfied_by(&context));

let mut route = FirstMatch::<EvaluationContext<&str>, &'static str>::new();
route.push(Flag::new("enabled"), "enabled-route");

assert_eq!(route.decide(&context), Some(&"enabled-route"));

EvaluationContext<UserData> is immutable after construction. It keeps application data typed, while counters, flags, and timestamps are named inputs. A missing counter reads as zero, a missing flag as false, and a missing timestamp as None.

Boundary rules

  • MaxCount is satisfied only while counter < maximum.
  • Flag is satisfied only when the named flag is true.
  • Cooldown permits a missing timestamp.
  • Cooldown accepts the exact elapsed >= duration boundary.
  • A future timestamp never satisfies cooldown, including zero duration.

Clock is injected instead of read from global state. FixedClock is useful in tests; an application can inject a monotonic production clock with the same trait.

Ordered decisions and fallback

FirstMatch evaluates rules in insertion order and stops at the first match. With no match, decide returns None. decide_or makes a fallback explicit without adding a hidden Always rule:

let fallback = "default-route";
let selected = route.decide_or(&context, &fallback);

The decision is borrowed from the FirstMatch value. This avoids cloning a large decision payload and makes its lifetime visible in the type system.

Indexed decisions for keyed catalogs

For a large catalog with exact string keys, IndexedFirstMatch can skip rules whose declared key does not match the candidate. Its key projection runs once per decide call, and lookup borrows the candidate’s string without allocating. The index merges the matching bucket with unkeyed rules in insertion order, so earlier fallbacks retain priority. This excerpt is illustrative because mdBook runs guide code blocks without the crate dependency; the complete version is a runnable Cargo example linked below:

use specification_core::{DecisionSpecification, IndexedFirstMatch};

struct Artifact<'a> { name: &'a str, is_directory: bool }
let mut classifier = IndexedFirstMatch::new(|item: &Artifact<'_>| item.name);
classifier.push_keyed("target", |item: &Artifact<'_>| item.name == "target" && item.is_directory, "rust-build");
classifier.push_unkeyed(|item: &Artifact<'_>| item.name.ends_with(".egg-info"), "python-metadata");

let item = Artifact { name: "target", is_directory: true };
assert_eq!(classifier.decide(&item), Some(&"rust-build"));
assert!(classifier.may_match("target"));

Register a rule as keyed only when that key is a necessary condition: the predicate must never match a candidate with another projected key. Arbitrary closures cannot be inspected to prove this. Put rules without a sound key in the unkeyed list. Those rules are checked for every candidate and make may_match return true for any key. Duplicate keys are allowed; large buckets and unkeyed rules are scanned in insertion order. The index requires Send + Sync specifications and can be shared across worker threads when the decisions are Sync. See the runnable indexed_decisions.rs.

Static keyed decisions

For a rule catalog known at compile time, the optional specification-core-macros crate provides first_match!. It generates a concrete evaluator that implements DecisionSpecification<Context> and stores the declared specification and decision values in typed fields; its result is borrowed from the evaluator. The projection runs once per call. Generated dispatch uses a match on the borrowed key and checks the matching keyed rules together with every unkeyed rule in declaration order:

use specification_core::{DecisionSpecification, Specification};
use specification_core_macros::first_match;

struct Facts { name: String, directory: bool, parent_cargo: bool, enabled: bool }
struct Enabled;
impl Specification<Facts> for Enabled {
    fn is_satisfied_by(&self, facts: &Facts) -> bool { facts.enabled }
}

let route = first_match! {
    context: Facts,
    decision: u16,
    key: |facts: &Facts| facts.name.as_str(),
    rules: [
        keyed(["target", "target-alias"], |facts: &Facts| {
            (facts.name == "target" || facts.name == "target-alias")
                && facts.directory && facts.parent_cargo
        }, 1),
        unkeyed(Enabled, 2),
    ]
};

This excerpt is illustrative because mdBook code blocks do not depend on the optional macro crate. The crate’s integration suite compiles the DSL against the real Specification and DecisionSpecification APIs: first_match.rs.

Register a rule as keyed only when its specification cannot match a different projected key. The macro cannot establish that contract from an arbitrary closure. Use one or more string literal aliases for a keyed rule; duplicate keys are valid. Unkeyed rules are checked for every key, and priority always follows declaration order. Unknown keys run only the unkeyed rules. Choose IndexedFirstMatch when the catalog must be assembled dynamically; the macro is for rule sets fixed in source.

Benchmarks

The Criterion baseline compares an equivalent static closure specification with an explicit BoxedSpecification dynamic boundary:

cargo bench -p specification-core --bench dispatch

It measures repeated single-predicate evaluation with black_box. It does not measure allocation, construction, context lookup, composition depth, or application throughput. Record the Rust version, target, CPU, power mode, command, and Criterion report when comparing runs. Results are not a performance guarantee or cross-machine comparison.

The indexed decision benchmark compares FirstMatch and IndexedFirstMatch with a 14-rule catalog and a 214-rule catalog. The larger catalog places 200 unique unrelated keys before the same 14 target rules, so candidate keys miss those first 200 rules. The harness checks every generated candidate and the aggregate checksum, including misses, before timing. It warms both paths, alternates their order across 31 rounds, and prints all raw samples as CSV on stdout with median summaries on stderr:

mkdir -p target/benchmark-artifacts
cargo bench -p specification-core --bench indexed_decisions \
  > target/benchmark-artifacts/indexed-decisions.csv \
  2> target/benchmark-artifacts/indexed-decisions-median.txt

Retain both files with the Rust version, target, CPU, power mode, and command. The run describes this workload only; it does not establish a general speedup.

The static_decisions benchmark uses the actual macro crate and core specifications. It compares a handwritten dispatcher with a 14-key catalog and a catalog containing 214 keyed rules: 200 distinct always-false unrelated rules before the same 14 target rules. It checks every result and checksum outside timing, warms both paths, alternates their order across 25 rounds, and prints each raw sample as CSV:

mkdir -p /Volumes/FlashCard/specification-core-rs-static/benchmark-artifacts
CARGO_TARGET_DIR=/Volumes/FlashCard/specification-core-rs-static \
  cargo bench -p specification-core-macros --bench static_decisions \
  > /Volumes/FlashCard/specification-core-rs-static/benchmark-artifacts/static-decisions.csv \
  2> /Volumes/FlashCard/specification-core-rs-static/benchmark-artifacts/static-decisions-median.txt

Record the compiler, target, machine, power mode, and command beside both files. The handwritten dispatcher is a purpose-built comparator; these synthetic measurements do not establish behavior for an application’s real catalog.

CI runs this benchmark on Linux and macOS and validates its samples with python3 scripts/check_indexed_performance.py target/benchmark-artifacts/indexed-decisions.csv. The indexed 214-rule median must fit 2 × indexed 14-rule median + 20 ns + 6 × combined MAD. This is a coarse same-host regression budget, not a significance test. Raw CSV, environment and JSON summaries are job artifacts; deterministic invocation-count tests establish which buckets are skipped.

Migrating from Swift to Rust

The Rust crate preserves the observable Specification Pattern behavior, not the Swift source API. The Swift package is a behavioral reference; ownership, trait bounds, and concurrency guarantees follow Rust conventions.

Core mapping

Swift SpecificationCoreRust specification-corePractical difference
SpecificationSpecification<T>Swift receives T; Rust borrows &T
PredicateSpec / spec {}Fn(&T) -> boolClosures implement the trait directly
AndSpecification / OrSpecification / NotSpecificationAnd / Or / NotBoth short-circuit; Rust uses named methods
AnySpecification<T>BoxedSpecification<T>Explicit owned trait-object boundary
—SharedSpecification<T>Explicit Arc + Send + Sync sharing boundary
DecisionSpecDecisionSpecification<T>Rust returns Option<&Decision>
FirstMatchSpecFirstMatch<T, Decision>Same ordered first-match behavior
EvaluationContextEvaluationContext<UserData>Generic typed data instead of [String: Any]
DefaultContextProvider.sharedapplication-owned snapshotNo global mutable context provider
Date in contextinjected Clock + DurationDeterministic, application-defined epoch
async throwsFuture<Output = Result<bool, Error>>Typed errors and Send futures
@Satisfies, @Maybe, @Decidesexplicit method callsNo hidden context acquisition in property access
@specs, @AutoContext#[specification(Name)]Rust macro is intentionally narrower

Composition

Swift:

let adult = PredicateSpec<User> { $0.age >= 18 }
let verified = PredicateSpec<User> { $0.verified }
let eligible = adult.and(verified)

let result = eligible.isSatisfiedBy(user)

Rust:

use specification_core::Specification;

let adult = |user: &User| user.age >= 18;
let verified = |user: &User| user.verified;
let eligible = adult.and(verified);

let result = eligible.is_satisfied_by(&user);

Swift’s protocol method is by-value at the API boundary. Rust’s borrowed candidate makes ownership explicit and lets a rule inspect a large value without taking it from the caller.

Type erasure and concurrency

AnySpecification is the usual Swift answer for heterogeneous collections. In Rust choose deliberately:

use specification_core::{BoxedSpecification, SharedSpecification};

let local = BoxedSpecification::new(|user: &User| user.age >= 18);
let shared = SharedSpecification::new(|user: &User| user.age >= 18);

BoxedSpecification owns a local Box<dyn Specification<T>>. The shared form owns Arc<dyn Specification<T> + Send + Sync> and rejects non-thread-safe captures at compile time. A regular specification does not need to be Send or Sync, which keeps single-threaded rules flexible.

Context providers and property wrappers

Swift commonly obtains a fresh snapshot through DefaultContextProvider.shared and evaluates it through a property wrapper. Rust keeps both operations visible:

let context = EvaluationContext::new(user)
    .with_flag("enabled", true)
    .with_counter("attempts", 1);

let enabled = Flag::new("enabled")
    .is_satisfied_by(&context);

Mutation and refresh of counters remain application responsibilities. This avoids hidden global state and makes a test’s inputs explicit.

Async and errors

Swift’s throws can carry any error conforming to Error. Rust associates one concrete Error type with each async specification; composing rules therefore requires a shared application error enum. Rust also requires the future to be Send and the candidate to be Sync, making executor/thread assumptions visible in the API.

The Rust async layer does not select Tokio or another runtime. The caller awaits the future using the runtime already chosen by the application.

What is intentionally not identical

Rust 0.1.0 does not port Swift property wrappers, Combine observation, global providers, segments, or the full date-rule convenience surface. Those features are platform or application concerns. Cross-language conformance fixtures cover observable synchronous behavior—composition, first-match, context defaults, and cooldown boundaries—rather than requiring identical types or serialization.

For the historical source inventory and the full adopt/adapt/defer/exclude matrix, see the maintainer-only DocC Migration Map and Swift Reference Audit.

Documentation and Testing

Public behavior is documented and tested together. Focused snippets belong in rustdoc doctests; complete scenarios belong in Cargo examples/ or integration tests; this book holds narrative material that connects several APIs or explains a design.

The book’s standalone snippets are marked rust,ignore when they need the workspace crate. Every such workflow links to a compiled example or test. Run the complete local documentation gate with:

make docs

The detailed conventions are defined in the contributor documentation guide.

Swift DocC Migration Map (Maintainer Reference)

The Swift SpecificationCore DocC corpus is conceptual source material. This historical map records an explicit Rust destination or exclusion for every authored DocC article and tutorial. The reader-facing comparison is Swift to Rust; this page is traceability data and does not promise source-level API compatibility.

For executable-source evidence and adopt/adapt/defer/exclude decisions, see the Swift reference audit.

Overview and Core API Articles

Swift DocC sourceRust destinationTreatment
SpecificationCore.mdcrate rustdoc and this book introductionSplit API overview from conceptual guide
Specification.mdSpecification rustdoc and conceptsRewrite trait semantics
SpecificationOperators.mdcombinator rustdoc and getting-startedAdapt operators to Rust ergonomics
AnySpecification.mddynamic-dispatch rustdoc and conceptsAdapt type erasure to explicit trait objects
DecisionSpec.mddecision rustdoc and conceptsPreserve ordered typed decision rationale
AsyncSpecification.mdasync specification guide and rustdocImplemented as runtime-neutral RPITIT futures with explicit Send bounds
ContextProviding.mdcontext rustdoc and conceptsAdapt provider model to dependency injection

Context and Built-in Specification Articles

Swift DocC sourceRust destinationTreatment
EvaluationContext.mdcontext rustdoc and conceptsReplace string-to-Any storage with typed/generic design
DefaultContextProvider.mdcontext guideExclude global singleton semantics
MockContextProvider.mdtesting guide and test helpersAdapt to Rust test fixtures
PredicateSpec.mdcore rustdocUse closure implementation when appropriate
FirstMatchSpec.mddecision rustdoc and examplePreserve deterministic priority behavior
MaxCountSpec.mdbuilt-in rules guideRewrite against Rust context API
CooldownIntervalSpec.mddecisions/context guideImplemented with an injected Clock and Duration
TimeSinceEventSpec.mdno Rust 0.1.0 equivalentNot ported; application-owned rule
DateRangeSpec.mdno Rust 0.1.0 equivalentNot ported; application-owned rule
DateComparisonSpec.mdno Rust 0.1.0 equivalentNot ported; application-owned rule

Swift-specific Declarative Articles

Swift DocC sourceRust destinationTreatment
Satisfies.mdmigration guideExplain explicit evaluation; no property-wrapper port
AsyncSatisfies.mdasync specification guideRewrite as explicit future evaluation; executor remains caller-owned
Decides.mddecision guideAdapt to ordinary Rust values/functions
Maybe.mddecision guideAdapt optional decisions to Option
SpecsMacro.mdserialization and macros guideImplemented as an explicit predicate macro in a separate crate
AutoContextMacro.mdserialization and macros guideExclude implicit context; retain explicit dependency injection

Tutorials

Swift DocC sourceRust destinationTreatment
Tutorials.tutorialSUMMARY.mdConvert tutorial hierarchy to book navigation
GettingStartedCore.tutorialgetting-started/Convert prose; rewrite each Swift code resource as Rust doctest or example
PropertyWrappersGuide.tutorialmigration/Explain idiomatic explicit evaluation instead of wrappers
MacrosAndAdvanced.tutorialguides/ and macro roadmapRetain concepts; gate all macro claims on implementation

Source-Code Resource Rule

All 27 Swift tutorial code resources are mapped individually during the corresponding chapter implementation. Each resource becomes one of: a Rust doctest, a complete Cargo example, an adapted conceptual snippet, or an explicit exclusion with a reason. The migration issue must preserve this resource-level traceability rather than only the tutorial-level mapping above.

Swift Reference Audit and Rust Porting Matrix (Maintainer Reference)

Historical planning artifact from Phase 0. For the current public migration walkthrough, use Swift to Rust.

Audited source: SoundBlaster/SpecificationCore

Revision: 7909e62da7ca0416dc24c8165bfd0e6d4cf16e57

Audit date: 2026-07-10

Evidence Scope

The audited Swift package has a library target, a macro target, and one test target. Its manifest declares Swift tools 5.10, Apple platform minimums, and dependencies on SwiftSyntax, MacroTesting, and the Swift DocC plugin.

The implementation contains 23 non-DocC Swift files in SpecificationCore, three macro implementation files, and 12 XCTest source files. The test suite includes decision, first-match, date, async, wrapper, macro, smoke, and performance test groups. This inventory is source evidence; it does not confirm the README’s coverage, latency, or concurrency claims.

Build Baseline

Command:

swift test

Toolchain:

Apple Swift version 6.4 (swiftlang-6.4.0.23.5 clang-2100.3.23.5)
Target: arm64-apple-macosx26.0

Result: FAIL before tests run. FirstMatchSpec.Builder.build() at Sources/SpecificationCore/Specs/FirstMatchSpec.swift:206 has an ambiguous call between the erased-pair initializer and generic initializer. The build also warns that Documentation.docc is unhandled by the Swift target.

This failure prevents a current pass/fail statement about the Swift test suite. It does not invalidate source-level behavior that is independently covered by specific XCTest files; it must not be ported as a Rust behavior.

Source Evidence

AreaSource pathsTest evidence
Core compositionCore/Specification.swift, Core/SpecificationOperators.swiftSpecificationCoreTests.swift
Type erasure and collection rulesCore/AnySpecification.swiftAnySpecificationPerformanceTests.swift
DecisionsCore/DecisionSpec.swift, Specs/FirstMatchSpec.swiftDecisionSpecTests.swift, FirstMatchSpecTests.swift
Context and providersContext/EvaluationContext.swift, Context/DefaultContextProvider.swift, Core/ContextProviding.swiftsmoke and wrapper tests
Built-in rulesSpecs/PredicateSpec.swift, MaxCountSpec.swift, time/date specsdate and wrapper tests
AsyncCore/AsyncSpecification.swift, Wrappers/AsyncSatisfies.swiftAsyncFeaturesTests.swift, AsyncSatisfiesWrapperTests.swift
Declarative/macro APIsWrappers/*.swift, Definitions/AutoContextSpecification.swift, SpecificationCoreMacros/*.swiftwrapper and macro tests

Capability Matrix

Swift capabilityClassificationRust destinationRationale
Specification boolean contractAdoptP1-T2Core domain abstraction
Static and, or, not compositionAdoptP1-T2Preserve boolean and short-circuit semantics
Swift &&, `, !` overloadsAdapt
PredicateSpec closure wrapperAdoptP1-T2Implement through a closure-compatible trait or adapter
Constants and collection all/anyAdoptP1-T2Preserve logical identities
AnySpecificationAdaptP1-T3Use explicit Rust trait objects and ownership bounds
DecisionSpec and boolean-result adapterAdoptP1-T3Preserve typed optional decision model
FirstMatchSpec and fallbackAdoptP1-T3Preserve priority and short-circuit behavior
FirstMatchSpec.BuilderAdaptP1-T3Avoid Swift overload ambiguity
EvaluationContext counters, flags, events, segmentsAdaptP1-T4Preserve concepts with typed/generic data
[String: Any] user dataExcludeP1-T4Replace with generic user data or deliberate typed values
Generic/static context providersAdaptP1-T4/P2-T1Keep dependency injection with Rust sharing bounds
Default provider, locks, Combine updatesExcludeP2-T1Avoid global state and Apple-specific observation
Counter and flag rulesAdaptP1-T4Retain semantics after context design is fixed
Cooldown, event, and date rulesDeferP1-T4Require clock/time representation policy
Property wrappersExcludeMigration guideRust uses explicit evaluation and Option
AsyncSpecification and async wrappersDeferP2-T2Requires runtime-neutral future/object-safety design
Swift macros and auto-contextDeferP2-T3Separate procedural macro decision and crate boundary
Domain composite examplesExcludeExamples/docsDemonstrations, not reusable core API
Combine observationExcludeNonePlatform-specific integration
README performance figuresExcludeP3-T2Re-establish with Rust benchmarks

Behavioral Contract for Rust Tests

BehaviorSwift evidenceRust requirement
AND evaluates left first and skips right after falseSpecification.swiftPreserve short-circuiting
OR evaluates left first and skips right after trueSpecification.swiftPreserve short-circuiting
NOT negates its wrapped resultSpecification.swiftPreserve boolean complement
Empty all-of is true; empty any-of is falseAnySpecification.swiftPreserve logical identities
First match wins and later pairs are not evaluatedDecisionSpecTests.swiftPreserve ordered short-circuiting
No match produces no result; fallback produces a resultFirstMatchSpecTests.swiftModel as Option and explicit fallback
Missing counter is zero; missing flag is falseEvaluationContext.swiftDocument and test if this context model is adopted
MaxCountSpec uses strict < maximum_countMaxCountSpec.swiftPreserve strict bound
Missing cooldown/event timestamp permits evaluationtime specsPreserve only after clock API exists
Cooldown threshold uses >=time specsPreserve boundary inclusivity
Date range is inclusive; missing comparison event is falsedate specsPreserve only after time policy is decided
Async evaluation can failAsyncSpecification.swiftPreserve error propagation in P2-T2

Rust specifications receive candidates by reference unless a later API decision requires ownership. This is an intentional adaptation of Swift’s by-value protocol method and is not a behavioral incompatibility.

P1-T2 Boundary

P1-T2 implements only the synchronous boolean core: trait contract, closure predicates, static composition, constants, collection identities, tests, and documentation. It excludes dynamic dispatch, decisions, context, time, providers, async, wrappers, macros, and domain examples.

Provenance and License

The audited Swift repository declares the MIT License, copyright 2025 Egor Merkushev. This Rust repository also uses MIT. The Rust implementation is an independent rewrite; copied substantial code, prose, or assets must retain the applicable copyright and license notice. Swift package dependencies are not inherited by the Rust core merely because they appear in the Swift manifest.

Relationship to Documentation Migration

The DocC migration map tracks authored documentation destinations. This audit tracks executable behavior and source classification. Both documents must be consulted before a Rust feature is declared compatible with Swift.

Architecture Baseline

The workspace contains a synchronous and runtime-neutral async core library, plus optional procedural-macro and versioned-serialization crates. Each integration may depend on the core, but the dependency direction is never reversed.

See the normative architecture baseline and project charter for the current design contract.