anapao 0.2.0

Library for deterministic simulation tests and reproducible stochastic workflows
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# anapao

[![Crates.io Version](https://img.shields.io/crates/v/anapao)](https://crates.io/crates/anapao)
[![CI](https://img.shields.io/github/actions/workflow/status/bnomei/anapao/ci.yml?branch=main)](https://github.com/bnomei/anapao/actions/workflows/ci.yml)
[![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/bnomei/anapao?utm_source=badge)
[![Crates.io Downloads](https://img.shields.io/crates/d/anapao)](https://crates.io/crates/anapao)
[![License](https://img.shields.io/crates/l/anapao)](https://crates.io/crates/anapao)
[![Discord](https://flat.badgen.net/badge/discord/bnomei?color=7289da&icon=discord&label)](https://discordapp.com/users/bnomei)
[![Buymecoffee](https://flat.badgen.net/badge/icon/donate?icon=buymeacoffee&color=FF813F&label)](https://www.buymeacoffee.com/bnomei)

`anapao` is a library-only deterministic Rust testing utility for simulation and stochastic workflows. It is intended to be used from Rust tests and tooling through the crate API, not as a command-line program.
This README is a linear tutorial for new users: you will build one scenario, run it deterministically, add expectations, run Monte Carlo batches, and persist CI-friendly artifacts.

The README and generated crate documentation are self-contained public documentation. Any ignored local `docs/` directory is reserved for private research notes and is not tracked, packaged, shipped, or required to use the crate.

## What You Will Build

By the end, you will have a repeatable testing flow that can:
- load a stable `ScenarioSpec` document or author an immutable checked `Scenario`,
- compile either representation into the same opaque executable model,
- execute seeded deterministic single runs,
- execute deterministic Monte Carlo batches,
- evaluate typed assertions with evidence,
- persist artifact packs (`manifest.json`, `events.jsonl`, `series.csv`, and more).

## Prerequisites

- Rust `1.85+`
- Cargo
- A Rust test project where you want deterministic simulation checks

Add the library dependency:

```toml
[dependencies]
anapao = "0.1.0"
```

The crate does not install or expose a binary target; import `anapao` from your Rust code.

---

## Scenario Representations and the Validation Boundary

Anapao has four deliberately distinct stages:

1. `ScenarioSpec` is the stable serde wire DTO used to load, inspect, edit, and store documents.
2. `Scenario::try_from` checks a DTO and produces an immutable semantic domain value.
3. `ScenarioBuilder` and the `ScenarioNode`/`ScenarioEdge` family constructors author that checked
   domain directly from Rust.
4. `Simulator::compile` (legacy DTO input) or `Simulator::compile_checked` (checked input) produces
   an opaque `CompiledScenario`, which `Simulator::run` executes.

Checked types are not a second serde representation. Deserialize the stable DTO first:

```rust
use anapao::types::{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 programmatic authoring, use the complete checked builder. Its consuming insertion methods
return `Result` because duplicate IDs are rejected:

```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 scenario = 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(scenario)?;
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>(())
```

The common `anapao::prelude` exports the checked scenario entrypoints. Individual family config
types remain available from `anapao::types` when their defaults need customization.

---

## Declarative `scenario!` Authoring

`anapao::scenario!` is the concise route to the same checked `Scenario` authored by
`ScenarioBuilder`; it is not another serde format or validation path. The deliberate macro set
contains exactly one macro: `scenario!`. There are no `expectations!`, assertion, config, or
report macros. Future assertion ergonomics should use normal associated functions, with
`#[track_caller]` where call-site diagnostics benefit from it.

The exact queue-flow intake example is executable unchanged:

```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>(())
```

A complete authoring shape can use every scenario-level section plus native and typed escape
forms. Here, `config`, `transfer`, `connection`, and `condition` pass existing checked values
through unchanged.

```rust
use anapao::types::{
    EndConditionSpec, PoolConfig, ResourceConnection, Scenario, StateConnection,
    TransferSpec, VariableRuntimeConfig,
};

let typed_pool = PoolConfig::default().with_capacity(16);
let typed_transfer = TransferSpec::Remaining;
let typed_state = StateConnection::default();
let typed_end = EndConditionSpec::MaxSteps { steps: 8 };
let typed_variables = VariableRuntimeConfig::default();

let scenario: Scenario = anapao::scenario! {
    id: "complete-macro";
    title: "Complete macro authoring";
    description: "native and typed forms";
    tags ["docs", "checked"];
    variables: typed_variables;
    metadata {"owner" => "docs"}
    nodes {
        source: Source { initial: 4.0, label: "Input" };
        buffer: Pool { config: typed_pool };
        sink: Sink { tags ["output"] };
    }
    edges {
        ingress: source -> buffer => fixed(1.0) resource {
            connection: ResourceConnection::default(), enabled: true
        };
        egress: buffer -> sink => transfer(typed_transfer) state { connection: typed_state };
    }
    track [source, sink];
    end max_steps(4);
    end condition(typed_end);
}?;
# let _ = scenario;
# Ok::<(), anapao::error::SetupError>(())
```

The canonical section order is `id`; optional `title`, `description`, `tags`, `variables`, and
`metadata`; required `nodes` and `edges`; then optional `track` and repeated `end` statements.
Use semicolons between sections and declarations; lists and blocks accept trailing separators.
Native node config/mode fields and `config: ...` are mutually exclusive. Consult the
[`scenario!` rustdoc](https://docs.rs/anapao/latest/anapao/macro.scenario.html) for the complete
grammar and each node, transfer, connection, state-target, and end-condition family.

Node and edge symbols turn into IDs from their exact spelling, while retaining separate node and
edge namespaces. A tracked metric is backed by its node symbol. State targets may name an edge
that is declared later. The macro deliberately delegates unknown references, duplicate symbols,
and graph semantics to `ScenarioBuilder`, preserving its established error diagnostics.

`scenario!` returns `Result<Scenario, SetupError>`. Propagate setup failures with `?`, or handle
them explicitly:

```rust
use anapao::error::SetupError;

let result = anapao::scenario! {
    id: "handled-error";
    nodes { source: Source; }
    edges { flow: source -> missing => remaining; }
};

match result {
    Err(SetupError::InvalidGraphReference { .. }) => {}
    Err(error) => return Err(error),
    Ok(scenario) => drop(scenario),
}
# Ok::<(), SetupError>(())
```

Macro expressions are evaluated once. Expansion uses `$crate` and absolute standard-library paths,
so it is hygienic with caller names, imports, and Cargo dependency renames. It introduces no
panic path; only the checked builder performs semantic validation. In public 0.2, the grammar,
symbol mapping, evaluation count, result/error types, and root (`anapao::scenario!`) and wildcard
prelude (`use anapao::prelude::*; scenario!`) paths are SemVer promises. Keep
`ScenarioBuilder` for direct checked Rust authoring, and keep the `ScenarioSpec` load followed by
`Scenario::try_from` route for stable serde documents.

---

## Step 1: Create `ScenarioSpec`

`ScenarioSpec` is your declarative model: nodes, edges, end conditions, and tracked metrics.

### Snippet S01 — Build a Minimal Scenario

```rust
use anapao::types::{EndConditionSpec, MetricKey, ScenarioSpec, TransferSpec};

let mut scenario = ScenarioSpec::source_sink(TransferSpec::Fixed { amount: 1.0 })
    .with_end_condition(EndConditionSpec::MaxSteps { steps: 3 });
scenario.tracked_metrics.insert(MetricKey::fixture("sink"));

assert_eq!(scenario.nodes.len(), 2);
assert_eq!(scenario.edges.len(), 1);
```

What you learned:
- how to bootstrap a minimum source->sink scenario with a convenience constructor,
- how end conditions and tracked metrics are attached.

---

## Step 2: Compile with `Simulator::compile`

Compilation validates and transforms your scenario into deterministic execution indexes.

### Snippet S02 — Compile a Scenario

```rust
use anapao::types::{EndConditionSpec, ScenarioSpec, TransferSpec};
use anapao::Simulator;

let scenario = ScenarioSpec::source_sink(TransferSpec::Fixed { amount: 1.0 })
    .with_end_condition(EndConditionSpec::MaxSteps { steps: 3 });

let compiled = Simulator::compile(scenario).unwrap();
assert_eq!(compiled.scenario_id().as_str(), "scenario-source-sink");
```

What you learned:
- compilation is explicit and deterministic,
- you should compile once and reuse the compiled form for runs.

### 0.2 API Migration

`CompiledScenario` is now an opaque, immutable execution product. Use the root-level
`Simulator` facade instead of the old raw compiler/engine/batch paths:

```rust
let scenario = anapao::testkit::fixture_scenario();
let run_config = anapao::testkit::deterministic_run_config();

// Before: anapao::validation::compile_scenario(&scenario)
// After:
let compiled = anapao::Simulator::compile(scenario).unwrap();

// Before: anapao::engine::run_single(&compiled, &run_config)
// After:
let report = anapao::Simulator::run(&compiled, &run_config).unwrap();

// Before: compiled.scenario.id / compiled.node_order / compiled.edge_order
// After:  compiled.scenario_id() / compiled.node_ids() / compiled.edge_ids()
```

For checked conversion in generic code, use `let compiled: anapao::CompiledScenario =
scenario.try_into()?;`. Read inspection data through `scenario_id()`, `source_spec()`,
`node_ids()`, `edge_ids()`, `node_count()`, and `edge_count()`; raw execution modules are private.

The legacy DTO route remains supported and its `with_node`/`with_edge` helpers keep
last-write-wins replacement semantics. The checked `ScenarioBuilder` instead returns a stable
error for duplicate node or edge IDs and retains the first definition.

Version 0.2 intentionally rejects semantic combinations that older execution paths could repair
or reinterpret:

- a node or edge map key that differs from the embedded `id`;
- an explicit node-family tag paired with another family's config payload;
- a resource/state connection tag paired with an active payload for the other connection kind;
- a node state target carrying a target connection ID; and
- a resource-connection, state-connection, or formula target missing its required target ID.

These checks happen after serde parsing. Raw JSON lexical duplicate keys are not detected at this
boundary, and no stored-data backfill or second checked serde format is introduced.

---

## Step 3: Configure `RunConfig`

`RunConfig` controls deterministic single-run execution (`seed`, `max_steps`, capture policy).

### Snippet S03 — Create a Deterministic RunConfig

```rust
use anapao::types::{CaptureConfig, CaptureSchedule, RunConfig};

let run = RunConfig::for_seed(42).with_max_steps(250).with_capture(
    CaptureConfig::default().with_schedule(CaptureSchedule::Every {
        stride: std::num::NonZeroU64::new(5).expect("positive stride"),
        include_initial: true,
        include_final: true,
    }),
);

assert_eq!(run.seed, 42);
assert_eq!(run.max_steps, 250);
assert!(matches!(
    run.capture.schedule(),
    CaptureSchedule::Every { stride, .. } if stride.get() == 5
));
```

What you learned:
- seeds pin determinism,
- capture configuration controls diagnostic trace granularity.

### Retention, Events, and Aggregation Are Separate

`CaptureConfig` controls **diagnostic report retention**, not whether the simulation completes.
`CaptureConfig::none()` leaves `RunReport::final_node_values` and `RunReport::final_metrics`
available, while intentionally leaving node snapshots, variable snapshots, transfer records, and
metric series empty. Use `CaptureConfig::final_only()` when final step-aligned diagnostics are
useful without retaining transfers, or `CaptureSchedule::Every` with typed `Selection` values for
periodic/selective diagnostics.

Live streamed events are independent of report retention: `Simulator::run_with_sink` and the
assertion-streaming APIs emit the same ordered simulation events when capture is `none()` as they
do with default capture. Batch aggregate sampling is separate again: `AggregationConfig` controls
only the metric series in `BatchReport`, while every `BatchRunSummary` retains terminal metrics.

Consequently, final-value assertions work with no captured series. Step selectors,
monotonic-series assertions, and series probability assertions require captured or aggregated
series evidence; when it was not requested, they report missing evidence instead of inferring it.

Batch aggregation is separate from per-run diagnostic capture. Configure only the
metric schedule and selection that belong in the `BatchReport`:

```rust
use anapao::types::{AggregationConfig, BatchConfig, CaptureSchedule, ExecutionMode};

let batch = BatchConfig::for_runs(64)
    .with_execution_mode(ExecutionMode::SingleThread)
    .with_aggregation(AggregationConfig::default().with_schedule(CaptureSchedule::Final));

assert!(matches!(batch.run_template.aggregation.schedule(), CaptureSchedule::Final));
```

---

## Step 4: Execute a Deterministic Single Run

Now run one deterministic simulation and assert expected outputs.

### Snippet S04 — Run Once and Verify Outputs

```rust
use anapao::{testkit, Simulator};
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));
```

What you learned:
- deterministic single-run output can be asserted directly in tests.

---

## Step 5: Create an `Expectation` Set

`Expectation` provides typed assertion semantics for run and batch reports.

### Snippet S05 — Declare Expectations

```rust
use anapao::assertions::{Expectation, MetricSelector};
use anapao::types::MetricKey;

let metric = MetricKey::fixture("sink");
let expectations = vec![
    Expectation::Equals {
        metric: metric.clone(),
        selector: MetricSelector::Final,
        expected: 3.0,
    },
    Expectation::Approx {
        metric: metric.clone(),
        selector: MetricSelector::Final,
        expected: 3.0,
        abs_tol: 0.0001,
        rel_tol: 0.0,
    },
    Expectation::Between {
        metric,
        selector: MetricSelector::Final,
        min: 0.0,
        max: 10.0,
    },
];

assert_eq!(expectations.len(), 3);
```

What you learned:
- expectations are data, not ad-hoc assertion code,
- final selectors read always-retained terminal metrics, while specific-step selectors require
  captured series evidence.

---

## Step 6: Run with Assertions and Event Sink

Use the integrated assertion path and capture ordered events for diagnostics.

### Snippet S06 — `run_with_assertions_and_sink` + `VecEventSink`

```rust
use anapao::assertions::{Expectation, MetricSelector};
use anapao::events::VecEventSink;
use anapao::types::MetricKey;
use anapao::{testkit, Simulator};

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"));
```

What you learned:
- assertions and execution can be done in one call,
- event streams provide structured debugging context.

---

## Step 7: Configure `BatchConfig`

`BatchConfig` controls deterministic Monte Carlo execution.

### Snippet S07 — Create BatchConfig

```rust
use anapao::types::{BatchConfig, BatchRunTemplate, ExecutionMode};

let batch = BatchConfig::for_runs(64)
    .with_execution_mode(ExecutionMode::SingleThread)
    .with_base_seed(7)
    .with_run_template(BatchRunTemplate::default())
    .with_max_steps(50);

assert_eq!(batch.runs, 64);
assert_eq!(batch.base_seed, 7);
assert_eq!(batch.run_template.max_steps, 50);
```

What you learned:
- `runs` scales the Monte Carlo sample size,
- `base_seed` + run index derivation preserve reproducibility.

---

## Step 8: Execute a Deterministic Batch Run

Run many deterministic simulations and check aggregate outputs.

### Snippet S08 — Run Batch and Verify Ordering/Aggregates

```rust
use anapao::{testkit, Simulator};
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")));
```

What you learned:
- batch summaries are deterministic and index-ordered.
- `completed_runs` counts reported run summaries; inspect each `run.completed` for semantic completion.

---

## Step 9: Persist Artifacts and Inspect `ManifestRef`

Persist reports for CI diffing and post-run diagnostics.

### Snippet S09 — Full Playbook (Setup -> Run -> Assert -> Artifacts)

```rust,no_run
use anapao::artifact::write_run_artifacts_with_assertions;
use anapao::assertions::{Expectation, MetricSelector};
use anapao::events::VecEventSink;
use anapao::types::MetricKey;
use anapao::{testkit, Simulator};

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 (run_report, assertion_report) = Simulator::run_with_assertions_and_sink(
    &compiled,
    &testkit::deterministic_run_config(),
    &expectations,
    &mut sink,
)
.unwrap();
assert!(run_report.completed);
assert!(assertion_report.is_success());

let output_dir = std::env::temp_dir().join("anapao-readme-playbook");
let manifest = write_run_artifacts_with_assertions(
    &output_dir,
    &run_report,
    sink.events(),
    Some(&assertion_report),
)
.unwrap();

assert!(manifest.artifacts.contains_key("manifest"));
assert!(manifest.artifacts.contains_key("events"));
assert!(manifest.artifacts.contains_key("assertions"));
```

What you learned:
- persisted artifacts become your CI and debugging contract,
- manifest keys are stable assertions for artifact expectations.

Artifact file ownership does not change when diagnostics are disabled. Where a run or batch writer
is invoked, its manifest-owned `variables.csv` and `series.csv` files remain valid header-only CSVs
when no variable snapshots or series were retained. Supplied events still produce `events.jsonl`
and drive the history/replay indexes.

---

## Step 10: Fixture-First Testing with `testkit` (and `rstest`)

Use `testkit` helpers to avoid duplicating setup across tests.

### Snippet S10 — Reusable Fixture-Style Test Pattern

```rust
use anapao::{testkit, Simulator};
use anapao::types::MetricKey;

fn deterministic_fixture_smoke() {
    let compiled = Simulator::compile(testkit::fixture_scenario()).unwrap();
    let report = Simulator::run(&compiled, &testkit::deterministic_run_config()).unwrap();
    assert_eq!(report.final_metrics.get(&MetricKey::fixture("sink")), Some(&3.0));
}

deterministic_fixture_smoke();
```

What you learned:
- fixture helpers keep tests concise and deterministic,
- you can wrap these helpers in your own `rstest` fixture macros for larger matrices.

---

## Common Failure Modes and Debugging Hints

- Missing tracked metric:
  - symptom: expectation fails with missing observed value.
  - fix: ensure metric key is in `scenario.tracked_metrics`.
- Non-terminating scenarios:
  - symptom: run ends at `max_steps` unexpectedly.
  - fix: verify `end_conditions` are configured and reachable.
- Seed confusion:
  - symptom: output differs between runs.
  - fix: pin `RunConfig.seed` for single runs and keep batch `base_seed` stable (batch seeds derive from `base_seed` + run index).
- Sparse traces:
  - symptom: insufficient snapshots for diagnostics.
  - fix: use `CaptureConfig::final_only()` or adjust `RunConfig.capture` with
    `CaptureSchedule::Every`.

## Feature Flags

- `parallel`: enables Rayon-backed batch execution mode (`ExecutionMode::Rayon`).
- `analysis-polars`: enables Polars DataFrame shaping helpers.
- `assertions-extended`: enables extra assertion/snapshot/property helper crates.

CI intentionally validates a targeted feature surface instead of an exhaustive feature
combination matrix. The supported check surface is the default feature set, each
individual optional feature (`parallel`, `analysis-polars`, and
`assertions-extended`), and the combined `--all-features` build.

## Module Surface (Reference)

`anapao` exports:
- `types`
- `error`
- `rng`
- `stochastic`
- `events`
- `stats`
- `artifact`
- `assertions`
- `testkit`
- `analysis` (only with `analysis-polars`)
- `Simulator` (compile/run/batch facade)

## Validation Commands

```bash
cargo test --doc
cargo test --all-targets
cargo test --all-targets --features parallel
cargo test --all-targets --features analysis-polars
cargo test --all-targets --features assertions-extended
cargo test --all-targets --all-features
cargo audit --deny warnings
cargo bench --no-run
```

## Performance Workflow (Manual Compare)

```bash
# capture matching default and parallel Criterion baselines (these runs can take time)
./scripts/bench-criterion save --bench simulation --baseline capture-retention-default
./scripts/bench-criterion save --bench simulation --features parallel --baseline capture-retention-parallel

# compare matrix
./scripts/bench-criterion compare --bench simulation --baseline capture-retention-default
./scripts/bench-criterion compare --bench simulation --features parallel --baseline capture-retention-parallel

# manual non-failing regression summary (+7% threshold)
./scripts/bench-criterion summary --bench simulation --baseline capture-retention-default --threshold 0.07
./scripts/bench-criterion summary --bench simulation --features parallel --baseline capture-retention-parallel --threshold 0.07

# run isolated DHAT capture-retention evidence in separate processes
./scripts/bench-capture-memory save --baseline capture-retention-default
./scripts/bench-capture-memory compare --baseline capture-retention-default

# flamegraphs and csv summaries
./benchmarks/run_profiles.sh
BENCH_FEATURES=parallel ./benchmarks/run_profiles.sh
```

## Dependency and Security Maintenance

CI runs `cargo audit --deny warnings` on every push and pull request to report RustSec advisories and dependency problems from `Cargo.lock`. Treat a failing audit as a release blocker unless the advisory is not reachable for this crate; if an advisory is not actionable immediately, document the reason and the planned follow-up in the pull request.

When updating dependencies:

1. Prefer the smallest compatible version bump that resolves the advisory or maintenance need.
2. Review changelogs for public API, MSRV, feature, and license changes before merging.
3. Keep optional feature dependencies (`parallel`, `analysis-polars`, and `assertions-extended`) checked with the normal CI matrix instead of adding one-off release automation.
4. Regenerate and commit `Cargo.lock`, then run `cargo audit --deny warnings` plus the standard repository validation commands.

## 0.2 Capture Policy Migration

Rust configuration fields are intentionally no longer a stable struct-literal surface. Construct
policies with `CaptureConfig::{none, final_only, default}` and consuming builders such as
`with_schedule`, `with_metrics`, and `with_variables`; configure batch aggregate sampling with
`AggregationConfig` and `BatchConfig::with_aggregation`. `CaptureConfig::disabled()` and batch
`with_capture` adapters are deprecated compatibility spellings, not recommended examples.

Persisted JSON remains migration-friendly: anapao reads the historical five-field capture object
(and historical nested `BatchRunTemplate.capture`) with its old behavior, rejects a zero legacy
stride, and writes only the canonical tagged typed representation. New JSON should use the current
`schedule`, channel selections, and batch `aggregation` fields.

## Local Pre-commit

This repo ships a native `prek.toml` for fast local commit gates.

```bash
prek validate-config
prek run --all-files
prek install
```

The hooks intentionally stay lightweight: `cargo fmt --all -- --check` and `cargo clippy --all-targets --all-features -- -D warnings`.