anapao 0.2.0

Library for deterministic simulation tests and reproducible stochastic workflows
Documentation
// SPDX-FileCopyrightText: 2026 Bruno Meilick
// SPDX-License-Identifier: LicenseRef-Anapao-FreeUse-NoCopy-NoDerivatives
//
// All rights reserved.
//
// This file is part of Anapao and is proprietary software.
// Unauthorized copying, modification, or distribution is prohibited.

//! Anapao — library-only deterministic simulation testing utility.
//!
//! Compile a declarative [`ScenarioSpec`], run seeded single simulations or Monte
//! Carlo batches, evaluate typed [`Expectation`]s, and persist CI-friendly artifact
//! packs. Prefer [`Simulator`] as the public entrypoint; its compiled execution
//! plan is intentionally opaque.
//!
//! ## Concepts
//! - `ScenarioSpec`: declarative simulation graph (nodes, edges, end conditions, metrics).
//! - `Scenario`: immutable, semantically checked scenario domain value.
//! - `ScenarioBuilder`: conventional checked Rust authoring with duplicate rejection.
//! - `CompiledScenario`: opaque execution plan produced from either scenario representation.
//! - `RunConfig`: deterministic single-run controls (`seed`, `max_steps`, diagnostic capture).
//! - `BatchConfig`: deterministic Monte Carlo controls (`runs`, `base_seed`, execution mode,
//!   aggregate metric sampling).
//! - `BatchRunTemplate`: seed-agnostic per-run defaults used by `BatchConfig`.
//! - `Expectation`: typed assertions evaluated against run or batch reports.
//! - Artifacts: manifested CI-friendly outputs (`events.jsonl`, `series.csv`, `summary.csv`, ...).
//!
//! Terminal node and metric maps are always retained. [`CaptureConfig`] controls diagnostic
//! snapshots, transfer records, variable snapshots, and single-run series; [`AggregationConfig`]
//! controls batch metric series only. Event sinks are live streams and do not depend on either
//! retention policy. Final assertions therefore work with no series, while step and series
//! assertions require explicitly retained evidence.
//!
//! ## Stable Documents and Checked Authoring
//!
//! [`ScenarioSpec`] remains the serde wire contract. Deserialize it first, then cross the
//! semantic boundary explicitly with [`Scenario::try_from`]. Checked values are not a second
//! serde format.
//!
//! ```rust
//! use anapao::{Scenario, ScenarioSpec};
//!
//! let document = serde_json::to_string(&anapao::testkit::fixture_scenario()).unwrap();
//! let dto: ScenarioSpec = serde_json::from_str(&document).unwrap();
//! let checked = Scenario::try_from(dto).unwrap();
//!
//! assert_eq!(checked.id().as_str(), "scenario-testkit");
//! ```
//!
//! For conventional Rust authoring, use the checked builder and family constructors. This
//! complete flow creates several node families, resource and state connections, run controls,
//! compiles the checked value, and executes it.
//!
//! ```rust
//! use std::num::NonZeroU64;
//! use anapao::types::{
//!     EdgeId, EndConditionSpec, MetricKey, NodeId, ResourceConnection, RunConfig,
//!     ScenarioBuilder, ScenarioEdge, ScenarioId, ScenarioNode, StateConnection,
//!     StateConnectionRole, StateTarget, TransferSpec,
//! };
//! use anapao::Simulator;
//!
//! let source = NodeId::fixture("source");
//! let pool = NodeId::fixture("pool");
//! let sink = NodeId::fixture("sink");
//! let authored = ScenarioBuilder::new(ScenarioId::fixture("checked-authoring"))
//!     .with_title("Checked authoring")
//!     .with_description("resource and state flow")
//!     .with_tag("docs")
//!     .with_node(ScenarioNode::source(source.clone()).with_initial_value(2.0))?
//!     .with_node(ScenarioNode::pool(pool.clone(), Default::default()).with_label("buffer"))?
//!     .with_node(ScenarioNode::sink(sink.clone()))?
//!     .with_edge(ScenarioEdge::resource(
//!         EdgeId::fixture("source-pool"), source.clone(), pool.clone(),
//!         TransferSpec::Fixed { amount: 1.0 },
//!         ResourceConnection::default().with_token_size(NonZeroU64::new(1).unwrap()),
//!     ))?
//!     .with_edge(ScenarioEdge::resource(
//!         EdgeId::fixture("pool-sink"), pool.clone(), sink,
//!         TransferSpec::Remaining, ResourceConnection::default(),
//!     ))?
//!     .with_edge(ScenarioEdge::state(
//!         EdgeId::fixture("source-pool-state"), source, pool,
//!         TransferSpec::Remaining,
//!         StateConnection::new(StateConnectionRole::Modifier, "+1", StateTarget::Node),
//!     ))?
//!     .with_end_condition(EndConditionSpec::MaxSteps { steps: 2 })
//!     .with_tracked_metric(MetricKey::fixture("sink"))
//!     .with_metadata("owner", "docs")
//!     .build()?;
//!
//! let compiled = Simulator::compile_checked(authored)?;
//! assert_eq!(compiled.source_spec().title.as_deref(), Some("Checked authoring"));
//! let report = Simulator::run(&compiled, &RunConfig::for_seed(39)).unwrap();
//! assert!(report.completed);
//! # Ok::<(), anapao::error::SetupError>(())
//! ```
//!
//! Existing DTO authoring remains supported: pass a [`ScenarioSpec`] to
//! [`Simulator::compile`]. Both compile routes converge on the same opaque plan.
//!
//! ## Declarative checked authoring
//!
//! [`scenario!`] is the single declarative checked-authoring macro. It produces the same checked
//! [`Scenario`] as [`ScenarioBuilder`], rather than a serde document or a second validation path.
//! It returns `Result<Scenario, SetupError>`: use `?` when setup errors should propagate, or
//! match `Err` when a caller needs to handle them. The exact queue-flow intake form is:
//!
//! ```rust
//! let scenario = anapao::scenario! {
//!     id: "queue-flow";
//!
//!     nodes {
//!         source: Source { initial: 64.0 };
//!         delay: Delay { steps: 2 };
//!         sink: Pool;
//!     }
//!     edges {
//!         source_delay: source -> delay => fixed(1.0);
//!         delay_sink: delay -> sink => remaining;
//!     }
//! }?;
//! # let _ = scenario;
//! # Ok::<(), anapao::error::SetupError>(())
//! ```
//!
//! The macro takes scenario fields in this canonical order: `id`, optional `title`,
//! `description`, `tags`, `variables`, and `metadata`; required `nodes` and `edges`; then
//! optional `track` and repeated `end` statements. Sections are semicolon-separated. A native
//! node config/mode and `config: ...` are exclusive. Node and edge symbols map to their exact
//! spelling in distinct namespaces; tracked metrics are node-backed, and state targets may use
//! forward edge symbols. It evaluates each expression once, uses `$crate` hygiene (including when
//! this dependency is renamed), and adds no panic path; builder validation supplies semantic
//! errors. These grammar, mapping, evaluation, result/error, and root/prelude paths are public
//! 0.2 compatibility promises.
//!
//! ## Deterministic Single Run
//! ```rust
//! use anapao::{Simulator, testkit};
//! use anapao::types::MetricKey;
//!
//! let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
//! let report = Simulator::run(&compiled, &testkit::deterministic_run_config()).unwrap();
//!
//! assert!(report.completed);
//! assert_eq!(report.steps_executed, 3);
//! assert_eq!(report.final_metrics.get(&MetricKey::fixture("sink")), Some(&3.0));
//! ```
//!
//! ## Deterministic Batch (Monte Carlo)
//! ```rust
//! use anapao::{Simulator, testkit};
//! use anapao::types::MetricKey;
//!
//! let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
//! let batch = Simulator::run_batch(&compiled, &testkit::deterministic_batch_config()).unwrap();
//!
//! assert_eq!(batch.completed_runs, batch.requested_runs);
//! assert!(batch.runs.windows(2).all(|window| window[0].run_index < window[1].run_index));
//! assert!(batch.aggregate_series.contains_key(&MetricKey::fixture("sink")));
//! ```
//!
//! ## Assertions Plus Event Stream
//! ```rust
//! use anapao::{Simulator, testkit};
//! use anapao::assertions::{Expectation, MetricSelector};
//! use anapao::events::VecEventSink;
//! use anapao::types::MetricKey;
//!
//! let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
//! let expectations = vec![Expectation::Equals {
//!     metric: MetricKey::fixture("sink"),
//!     selector: MetricSelector::Final,
//!     expected: 3.0,
//! }];
//!
//! let mut sink = VecEventSink::new();
//! let (_report, assertion_report) = Simulator::run_with_assertions_and_sink(
//!     &compiled,
//!     &testkit::deterministic_run_config(),
//!     &expectations,
//!     &mut sink,
//! )
//! .unwrap();
//!
//! assert!(assertion_report.is_success());
//! assert!(sink
//!     .events()
//!     .iter()
//!     .any(|event| event.event_name() == "assertion_checkpoint"));
//! ```
//!
//! ## Full Playbook (Setup -> Run -> Assert -> Artifacts)
//! ```no_run
//! use anapao::{Simulator, testkit};
//! use anapao::artifact::write_run_artifacts_with_assertions;
//! use anapao::assertions::{Expectation, MetricSelector};
//! use anapao::events::VecEventSink;
//! use anapao::types::MetricKey;
//!
//! // 1) Setup scenario + compile.
//! let scenario = testkit::fixture_scenario();
//! let compiled = Simulator::compile(scenario).unwrap();
//!
//! // 2) Setup run config + expectations.
//! let run_config = testkit::deterministic_run_config();
//! let expectations = vec![Expectation::Equals {
//!     metric: MetricKey::fixture("sink"),
//!     selector: MetricSelector::Final,
//!     expected: 3.0,
//! }];
//!
//! // 3) Run simulation and evaluate assertions.
//! let mut sink = VecEventSink::new();
//! let (run_report, assertion_report) = Simulator::run_with_assertions_and_sink(
//!     &compiled,
//!     &run_config,
//!     &expectations,
//!     &mut sink,
//! )
//! .unwrap();
//! assert!(assertion_report.is_success());
//!
//! // 4) Persist artifact pack for CI/debugging.
//! let output_dir = std::env::temp_dir().join("anapao-doc-playbook");
//! let manifest = write_run_artifacts_with_assertions(
//!     &output_dir,
//!     &run_report,
//!     sink.events(),
//!     Some(&assertion_report),
//! )
//! .unwrap();
//! assert!(manifest.artifacts.contains_key("manifest"));
//! ```

#![forbid(unsafe_code)]

pub mod artifact;
pub mod assertions;
mod batch;
mod engine;
pub mod error;
pub mod events;
pub mod expr;
mod plan;
pub mod prelude;
pub mod rng;
mod scenario_macro;
pub mod simulator;
pub mod stats;
pub mod stochastic;
pub mod testkit;
pub mod types;
mod validation;

#[cfg(feature = "analysis-polars")]
pub mod analysis;

pub use assertions::{AssertionReport, Expectation, MetricSelector};
pub use events::{EventSink, VecEventSink};
pub use plan::CompiledScenario;
pub use simulator::Simulator;
pub use types::{
    AggregationConfig, BatchConfig, BatchReport, BatchRunTemplate, CaptureConfig, CaptureSchedule,
    EndConditionSpec, ExecutionMode, MetricKey, RunConfig, RunReport, Scenario, ScenarioBuilder,
    ScenarioSpec, Selection, TransferSpec,
};

#[cfg(doctest)]
#[doc = include_str!("../README.md")]
mod readme_doctest {}