Expand description
Anapao — library-only deterministic simulation testing utility.
Compile a declarative ScenarioSpec, run seeded single simulations or Monte
Carlo batches, evaluate typed Expectations, 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 byBatchConfig.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.
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.
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);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:
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;
}
}?;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
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)
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
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)
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"));Re-exports§
pub use assertions::AssertionReport;pub use assertions::Expectation;pub use assertions::MetricSelector;pub use events::EventSink;pub use events::VecEventSink;pub use simulator::Simulator;pub use types::AggregationConfig;pub use types::BatchConfig;pub use types::BatchReport;pub use types::BatchRunTemplate;pub use types::CaptureConfig;pub use types::CaptureSchedule;pub use types::EndConditionSpec;pub use types::ExecutionMode;pub use types::MetricKey;pub use types::RunConfig;pub use types::RunReport;pub use types::Scenario;pub use types::ScenarioBuilder;pub use types::ScenarioSpec;pub use types::Selection;pub use types::TransferSpec;
Modules§
- artifact
- Filesystem writers for CI-friendly run and batch artifact packs.
- assertions
- Typed expectation language and deterministic evaluation against run/batch reports.
- error
- Layered error taxonomy for setup, run, assertion, and artifact failures.
- events
- Typed run events, deterministic ordering keys, and event-sink adapters.
- expr
- Deterministic arithmetic expression parse/eval used by transfers and state formulas.
- prelude
- Common checked-authoring, compile/run, and assertion types for short import paths.
- rng
- Seed derivation and ChaCha8 RNG helpers that pin run-to-run determinism.
- simulator
- Public façade for compile, single-run, batch, assertion, and event-streaming workflows.
- stats
- Deterministic descriptive stats and prediction indicators for metric samples.
- stochastic
- Validated stochastic primitives for variables, gates, and expression helpers.
- testkit
- Shared deterministic fixtures, parity catalog loaders, and rstest entrypoints.
- types
- Shared domain model for scenarios, run/batch configs, reports, and artifacts.