# `aura run` end-to-end sample-harness CLI — Implementation Plan > **Parent spec:** `docs/specs/0010-aura-run-cli.md` > > **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run > this plan. Steps use `- [ ]` checkboxes for tracking. **Goal:** Ship a reusable `aura-std::Recorder` sink node and wire an `aura run` subcommand that bootstraps a sample SMA-cross→Exposure→SimBroker harness with two recording sinks, runs it deterministically, and prints the cycle-0009 metrics+manifest report as canonical JSON to stdout. **Architecture:** Two deliverables in dependency order. (1) `aura-std::Recorder` — a pure consumer (`output: vec![]`, C8) over `kinds.len()` input columns, holding an `mpsc::Sender<(Timestamp, Vec)>`, sending `(ctx.now(), row)` each fired cycle once every column is warm and returning `None`; it mirrors the existing `#[cfg(test)]` four-kind fixture in `harness.rs`. (2) `aura-cli` gains `synthetic_prices` / `sample_harness` / `run_sample` / `main`: the sample harness is authored in plain Rust over the raw `Harness::bootstrap(nodes, sources, edges)` API (no builder DSL), and `main` hand-parses one subcommand. No `aura-engine` / `Harness` / node-contract change; pure-additive; the workspace stays zero-(external-)dependency. **Tech Stack:** `aura-core` (`Node`/`Ctx`/`Scalar`/`Firing`/`Timestamp`), `aura-engine` (`Harness::bootstrap`/`run`, the `report` surface — `summarize` / `f64_field` / `RunManifest` / `RunReport`), `aura-std` (`Sma`/`Sub`/`Exposure`/ `SimBroker` + the new `Recorder`), `std::sync::mpsc`. --- ## Files this plan creates or modifies - Create: `crates/aura-std/src/recorder.rs` — the shipped `Recorder` sink node (pure consumer, four-kind, holds `mpsc::Sender`). - Modify: `crates/aura-std/src/lib.rs:18-29` — add `mod recorder;` and `pub use recorder::Recorder;` (alphabetical position: between `lincomb` and `sim_broker`). - Modify: `crates/aura-cli/Cargo.toml:12-13` — add `aura-std` and `aura-core` path deps alongside `aura-engine`. - Modify: `crates/aura-cli/src/main.rs:1-8` — replace the stub with `synthetic_prices` / `sample_harness` / `run_sample` / `main` + a unit-test module. - Create: `crates/aura-cli/tests/cli_run.rs` — integration test driving the built binary via `env!("CARGO_BIN_EXE_aura")`. - Test: `crates/aura-std/src/recorder.rs` (inline `#[cfg(test)] mod tests`) — Recorder captures a known f64 stream + returns `None` until all columns warm. - Test: `crates/aura-cli/src/main.rs` (inline `#[cfg(test)] mod tests`) — `run_sample` determinism + pinned metric values. - Test: `crates/aura-cli/tests/cli_run.rs` — `run` → exit 0 + JSON stdout; bad args → exit 2 + usage stderr. --- ## The chosen synthetic stream and its hand-computed metrics (load-bearing) `synthetic_prices` is the 7-tick f64 stream below (rises through t=4, then reverses), chosen so the demo trace is non-trivial — exactly one exposure sign flip and a real drawdown (C22 populated trace): ``` t: 1 2 3 4 5 6 7 price: 1.0000 1.0010 1.0030 1.0060 1.0040 1.0010 0.9990 ``` Tracing the cycle-0007 chain (topo order 0=Sma2, 1=Sma4, 2=Sub, 3=Exposure, 4=SimBroker, 5=equity sink, 6=exposure sink; the whole signal chain propagates within one cycle, the broker lags exposure one cycle by its own state, C2): - `Sma2_t = (p_t+p_{t-1})/2` (from t=2); `Sma4_t = mean(last 4)` (from t=4). - `spread_t = Sma2_t - Sma4_t`; `exposure_t = clamp(spread/0.5, -1, +1) = 2·spread` (in-band, no clamp): `expo = [+0.004, +0.003, -0.002, -0.005]` for t=4..7. - Exposure node produces (and the exposure sink records) only t=4..7 → exposure rows `[+0.004, +0.003, -0.002, -0.005]`; sign sequence `+,+,-,-` → **1 sign flip**. - SimBroker (`pip_size=0.0001`) fires every cycle, integrating `prev_exposure·(price-prev_price)/pip_size`; equity recorded t=1..7 is `[0, 0, 0, 0, -0.08, -0.17, -0.13]`: - t5: `0.004·(1.0040-1.0060)/0.0001 = 0.004·(-20) = -0.08` → cum `-0.08` - t6: `0.003·(1.0010-1.0040)/0.0001 = 0.003·(-30) = -0.09` → cum `-0.17` - t7: `-0.002·(0.9990-1.0010)/0.0001 = -0.002·(-20) = +0.04` → cum `-0.13` - **`total_pips = -0.13`** (last equity), **`max_drawdown = 0.17`** (running peak 0 minus trough -0.17), **`exposure_sign_flips = 1`**. The integer-valued `exposure_sign_flips` is pinned exactly; the two f64 metrics are pinned within `1e-9` (the computation's float dust is ~`1e-15`, so the tolerance is safe by six orders of magnitude while staying a real correctness pin). Determinism is pinned exactly (two runs, identical JSON). --- ## Task 1: `aura-std::Recorder` sink node **Files:** - Create: `crates/aura-std/src/recorder.rs` - Modify: `crates/aura-std/src/lib.rs:18-29` - Test: `crates/aura-std/src/recorder.rs` (inline `#[cfg(test)] mod tests`) - [ ] **Step 1: Create `crates/aura-std/src/recorder.rs` with the node + its failing tests** Write the file with exactly this content: ```rust //! `Recorder` — a reusable recording sink (the glossary *sink* role, C8/C22): //! a pure consumer that, each fired cycle, sends `(ctx.now(), row)` — the newest //! value of each declared input column — to an out-of-graph `mpsc` destination it //! holds. It produces nothing (`output: vec![]`), so it is a leaf in the DAG. The //! `mpsc::Sender` keeps the engine's purity invariant (C7): the node carries no //! `Rc`/`RefCell` interior mutability, only an owned channel handle. Supports all //! four base scalar kinds so any column can be persisted; returns `None` (filters) //! until every input column is warm. use aura_core::{Ctx, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp}; use std::sync::mpsc::Sender; /// A recording sink over `kinds.len()` input columns. Each fired cycle it reads /// the newest value of every column and sends the row to `tx`; it returns `None` /// (records, forwards nothing) and `None` during warm-up until all columns have a /// value. pub struct Recorder { kinds: Vec, firing: Firing, tx: Sender<(Timestamp, Vec)>, } impl Recorder { /// A recorder over one input column per entry in `kinds`, each with the given /// `firing` policy, sending recorded `(timestamp, row)` pairs to `tx`. pub fn new(kinds: &[ScalarKind], firing: Firing, tx: Sender<(Timestamp, Vec)>) -> Self { Self { kinds: kinds.to_vec(), firing, tx } } } impl Node for Recorder { fn schema(&self) -> NodeSchema { NodeSchema { inputs: self .kinds .iter() .map(|&kind| InputSpec { kind, lookback: 1, firing: self.firing }) .collect(), output: vec![], } } fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> { let mut row = Vec::with_capacity(self.kinds.len()); for (i, &kind) in self.kinds.iter().enumerate() { // newest of each column by kind; `?` returns None (warm-up) if cold. let scalar = match kind { ScalarKind::F64 => Scalar::F64(ctx.f64_in(i).get(0)?), ScalarKind::I64 => Scalar::I64(ctx.i64_in(i).get(0)?), ScalarKind::Bool => Scalar::Bool(ctx.bool_in(i).get(0)?), ScalarKind::Timestamp => Scalar::Ts(ctx.ts_in(i).get(0)?), }; row.push(scalar); } let _ = self.tx.send((ctx.now(), row)); None } } #[cfg(test)] mod tests { use super::*; use aura_core::{AnyColumn, Timestamp}; use std::sync::mpsc; #[test] fn recorder_captures_f64_stream_after_warmup() { let (tx, rx) = mpsc::channel(); let mut rec = Recorder::new(&[ScalarKind::F64], Firing::Any, tx); // size the one f64 input column from the schema, as the engine would. let schema = rec.schema(); assert!(schema.output.is_empty(), "a sink declares no output (C8)"); let mut inputs = vec![AnyColumn::with_capacity( schema.inputs[0].kind, schema.inputs[0].lookback, )]; // cold: returns None and records nothing. assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(1))), None); assert!(rx.try_recv().is_err()); // warm: returns None (pure consumer) but records (now, [F64(newest)]). for (t, v) in [(2_i64, 10.0_f64), (3, 20.0), (4, 30.0)] { inputs[0].push(Scalar::F64(v)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(t))), None); } let rows: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!( rows, vec![ (Timestamp(2), vec![Scalar::F64(10.0)]), (Timestamp(3), vec![Scalar::F64(20.0)]), (Timestamp(4), vec![Scalar::F64(30.0)]), ] ); } #[test] fn recorder_is_none_until_all_columns_warm() { let (tx, rx) = mpsc::channel(); let mut rec = Recorder::new(&[ScalarKind::F64, ScalarKind::F64], Firing::Any, tx); let mut inputs = vec![ AnyColumn::with_capacity(ScalarKind::F64, 1), AnyColumn::with_capacity(ScalarKind::F64, 1), ]; // only column 0 present -> None, nothing recorded. inputs[0].push(Scalar::F64(1.0)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(1))), None); assert!(rx.try_recv().is_err()); // both present -> records the full row (still returns None). inputs[1].push(Scalar::F64(2.0)).unwrap(); assert_eq!(rec.eval(Ctx::new(&inputs, Timestamp(2))), None); let rows: Vec<(Timestamp, Vec)> = rx.try_iter().collect(); assert_eq!(rows, vec![(Timestamp(2), vec![Scalar::F64(1.0), Scalar::F64(2.0)])]); } } ``` - [ ] **Step 2: Wire the module into `crates/aura-std/src/lib.rs`** Add `mod recorder;` between `mod lincomb;` (line 20) and `mod sim_broker;` (line 21); add `pub use recorder::Recorder;` between `pub use lincomb::LinComb;` (line 26) and `pub use sim_broker::SimBroker;` (line 27). The `mod` block becomes: ```rust mod add; mod exposure; mod lincomb; mod recorder; mod sim_broker; mod sma; mod sub; pub use add::Add; pub use exposure::Exposure; pub use lincomb::LinComb; pub use recorder::Recorder; pub use sim_broker::SimBroker; pub use sma::Sma; pub use sub::Sub; ``` - [ ] **Step 3: Run the Recorder tests to verify they pass** Run: `cargo test -p aura-std recorder` Expected: PASS — `recorder_captures_f64_stream_after_warmup` and `recorder_is_none_until_all_columns_warm` both green (2 tests run; the filter `recorder` matches exactly these two named tests). - [ ] **Step 4: Verify the crate still lints and docs clean** Run: `cargo clippy -p aura-std --all-targets -- -D warnings` Expected: PASS — no warnings. --- ## Task 2: `aura-cli` `run` subcommand **Files:** - Modify: `crates/aura-cli/Cargo.toml:12-13` - Modify: `crates/aura-cli/src/main.rs:1-8` - Test: `crates/aura-cli/src/main.rs` (inline `#[cfg(test)] mod tests`) - [ ] **Step 1: Add the path deps to `crates/aura-cli/Cargo.toml`** Replace the `[dependencies]` block (lines 12-13) with: ```toml [dependencies] aura-core = { path = "../aura-core" } aura-engine = { path = "../aura-engine" } aura-std = { path = "../aura-std" } ``` - [ ] **Step 2: Replace `crates/aura-cli/src/main.rs` with the run wiring + tests** Write the file with exactly this content: ```rust //! `aura` — the programmatic / CLI face of the engine (the surface the LLM and //! automation drive: author a node, run a sim/sweep, emit structured metrics). //! //! The walking skeleton's closing seam: `aura run` bootstraps a built-in sample //! signal-quality harness (synthetic source → SMA-cross → Exposure → SimBroker → //! recording sinks), runs it deterministically (C1), and prints the run's //! metrics + manifest (#6) as canonical JSON to stdout (the headline C14 move). use aura_core::{Firing, Scalar, ScalarKind, Timestamp}; use aura_engine::{ f64_field, summarize, Edge, Harness, RunManifest, RunReport, SourceSpec, Target, }; use aura_std::{Exposure, Recorder, SimBroker, Sma, Sub}; use std::sync::mpsc::{self, Receiver}; /// The built-in synthetic price stream: rises through t=4 then reverses, so the /// demo trace carries one exposure sign flip and a real drawdown (C22 populated /// trace). Deterministic and fixed (C1). fn synthetic_prices() -> Vec<(Timestamp, Scalar)> { [ (1_i64, 1.0000_f64), (2, 1.0010), (3, 1.0030), (4, 1.0060), (5, 1.0040), (6, 1.0010), (7, 0.9990), ] .iter() .map(|&(t, p)| (Timestamp(t), Scalar::F64(p))) .collect() } /// Bootstrap the sample signal-quality harness with two recording sinks (equity /// tapped on the SimBroker, exposure tapped on the Exposure node). Rust-authored /// wiring (C17/C20) over the raw bootstrap API — no builder DSL this cycle. The /// price taps both SMAs and the broker's price slot (slot 1); exposure feeds the /// broker's slot 0 (slot order is load-bearing — both are f64). fn sample_harness() -> ( Harness, Receiver<(Timestamp, Vec)>, Receiver<(Timestamp, Vec)>, ) { let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); let h = Harness::bootstrap( vec![ Box::new(Sma::new(2)), // 0 fast SMA Box::new(Sma::new(4)), // 1 slow SMA Box::new(Sub::new()), // 2 spread Box::new(Exposure::new(0.5)), // 3 exposure Box::new(SimBroker::new(0.0001)), // 4 sim-optimal broker Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_eq)), // 5 equity sink Box::new(Recorder::new(&[ScalarKind::F64], Firing::Any, tx_ex)), // 6 exposure sink ], vec![SourceSpec { kind: ScalarKind::F64, targets: vec![ Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }, Target { node: 4, slot: 1 }, // price into the broker's price slot ], }], vec![ Edge { from: 0, to: 2, slot: 0, from_field: 0 }, Edge { from: 1, to: 2, slot: 1, from_field: 0 }, Edge { from: 2, to: 3, slot: 0, from_field: 0 }, Edge { from: 3, to: 4, slot: 0, from_field: 0 }, // exposure into broker slot 0 Edge { from: 4, to: 5, slot: 0, from_field: 0 }, // equity -> sink 5 Edge { from: 3, to: 6, slot: 0, from_field: 0 }, // exposure -> sink 6 ], ) .expect("valid sample signal-quality DAG"); (h, rx_eq, rx_ex) } /// Run the sample harness and fold it into a `RunReport` (drain both sinks → /// `f64_field` → `summarize` → pair with a `RunManifest`). Pure and deterministic /// (C1): the same build yields the same report. fn run_sample() -> RunReport { let (mut h, rx_eq, rx_ex) = sample_harness(); let prices = synthetic_prices(); let window = ( prices.first().expect("non-empty stream").0, prices.last().expect("non-empty stream").0, ); h.run(vec![prices]); let eq_rows: Vec<(Timestamp, Vec)> = rx_eq.try_iter().collect(); let ex_rows: Vec<(Timestamp, Vec)> = rx_ex.try_iter().collect(); let equity = f64_field(&eq_rows, 0); let exposure = f64_field(&ex_rows, 0); let metrics = summarize(&equity, &exposure); RunReport { manifest: RunManifest { commit: option_env!("AURA_COMMIT").unwrap_or("unknown").to_string(), params: vec![ ("sma_fast".to_string(), 2.0), ("sma_slow".to_string(), 4.0), ("exposure_scale".to_string(), 0.5), ], window, seed: 0, broker: "sim-optimal(pip_size=0.0001)".to_string(), }, metrics, } } fn main() { let mut args = std::env::args().skip(1); match args.next().as_deref() { Some("run") => println!("{}", run_sample().to_json()), _ => { eprintln!("aura: usage: aura run"); std::process::exit(2); } } } #[cfg(test)] mod tests { use super::*; #[test] fn run_sample_is_deterministic_and_non_trivial() { let r1 = run_sample(); let r2 = run_sample(); // C1 determinism: two runs are bit-identical (metrics + rendered JSON). assert_eq!(r1.metrics, r2.metrics); assert_eq!(r1.to_json(), r2.to_json()); let m = &r1.metrics; // exactly one exposure sign flip in the demo trace (rises then reverses). assert_eq!(m.exposure_sign_flips, 1); // a non-trivial, populated trace: a real drawdown. assert!(m.max_drawdown > 0.0); // hand-computed magnitudes for the chosen stream (float tolerance; the // computation's dust is ~1e-15). assert!( (m.max_drawdown - 0.17).abs() < 1e-9, "max_drawdown = {}", m.max_drawdown ); assert!( (m.total_pips - (-0.13)).abs() < 1e-9, "total_pips = {}", m.total_pips ); // manifest carries the sample's known configuration. let (from, to) = r1.manifest.window; assert_eq!((from.0, to.0), (1, 7)); assert_eq!(r1.manifest.commit, "unknown"); } } ``` - [ ] **Step 3: Run the `run_sample` unit test to verify it passes** Run: `cargo test -p aura-cli --bin aura run_sample` Expected: PASS — `run_sample_is_deterministic_and_non_trivial` green (1 test run; the filter `run_sample` matches that one named test). - [ ] **Step 4: Smoke-run the binary by hand** Run: `cargo run -p aura-cli -- run` Expected: a single-line JSON object on stdout beginning `{"manifest":{"commit":"unknown","params":{"sma_fast":2,"sma_slow":4,"exposure_scale":0.5},"window":[1,7],"seed":0,"broker":"sim-optimal(pip_size=0.0001)"},"metrics":{"total_pips":` … ending with `"exposure_sign_flips":1}}`, exit code 0. Run: `cargo run -p aura-cli 2>&1 >/dev/null` Expected: `aura: usage: aura run` on stderr; exit code 2. --- ## Task 3: `aura-cli` CLI integration test **Files:** - Create: `crates/aura-cli/tests/cli_run.rs` - [ ] **Step 1: Create `crates/aura-cli/tests/cli_run.rs` with the binary-driving tests** Write the file with exactly this content: ```rust //! Integration test: drive the built `aura` binary as a downstream user would, //! asserting the `run` subcommand's stdout/exit contract and the bad-args path. use std::process::Command; /// Path to the freshly-built `aura` binary (Cargo sets this env var for the test /// crate; the binary is named `aura` in `Cargo.toml`). const BIN: &str = env!("CARGO_BIN_EXE_aura"); #[test] fn run_prints_json_and_exits_zero() { let out = Command::new(BIN).arg("run").output().expect("spawn aura run"); assert!(out.status.success(), "exit status: {:?}", out.status); let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); // exactly one line (the JSON object + a trailing newline from println!). assert_eq!(stdout.lines().count(), 1, "stdout was: {stdout:?}"); let line = stdout.trim_end(); // canonical cycle-0009 JSON shape: nested manifest + metrics, stable keys. assert!(line.starts_with("{\"manifest\":{\"commit\":\"unknown\","), "got: {line}"); assert!(line.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "got: {line}"); assert!(line.contains("\"window\":[1,7]"), "got: {line}"); assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}"); // the integer sign-flip count is stable across float renderings. assert!(line.ends_with("\"exposure_sign_flips\":1}}"), "got: {line}"); } #[test] fn no_args_prints_usage_and_exits_two() { let out = Command::new(BIN).output().expect("spawn aura"); assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status); assert!(out.stdout.is_empty(), "stdout should be empty on the usage path"); let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr"); assert!(stderr.contains("usage"), "stderr was: {stderr:?}"); } ``` - [ ] **Step 2: Run the integration test to verify it passes** Run: `cargo test -p aura-cli --test cli_run` Expected: PASS — `run_prints_json_and_exits_zero` and `no_args_prints_usage_and_exits_two` both green (2 tests run; the `--test cli_run` target resolves to the file created in Step 1). --- ## Task 4: Full-workspace gates **Files:** none (verification only). - [ ] **Step 1: Full test suite** Run: `cargo test --workspace` Expected: PASS — all pre-existing tests plus the new Recorder (2), `run_sample` (1), and `cli_run` (2) tests green; 0 failures. - [ ] **Step 2: Lint gate** Run: `cargo clippy --workspace --all-targets -- -D warnings` Expected: PASS — no warnings across the workspace. - [ ] **Step 3: Doc gate** Run: `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` Expected: PASS — docs build with no warnings (the new `Recorder` rustdoc and the `aura-cli` module/fn docs included). --- ## Self-review (planner Step 5) 1. **Spec coverage:** Recorder node (spec §Architecture 1, Components) → Task 1; `synthetic_prices`/`sample_harness`/`run_sample`/`main` (§Architecture 2, Components, Data flow, Error handling) → Task 2; CLI integration test (§Testing strategy) → Task 3; the three gates (§Testing strategy) → Task 4. The user-facing invocation/output (§Concrete code shapes) is exercised by Task 2 Step 4 + Task 3. All spec sections covered. 2. **Placeholder scan:** no "TBD"/"TODO"/"similar to"/"implement later"/"add appropriate" — every code body is verbatim. 3. **Type consistency:** `Recorder` / `Recorder::new(kinds, firing, tx)` / `RunReport` / `RunManifest` / `summarize` / `f64_field` / `Harness::bootstrap` / `Edge`/`Target`/`SourceSpec` / `Sma`/`Sub`/`Exposure`/`SimBroker` match the recon'd signatures and are spelled identically across tasks. `Scalar::Ts` (not `Scalar::Timestamp`) used for the `ScalarKind::Timestamp` arm. The aura-std `pub use` list stays alphabetical. 4. **Step granularity:** each step is one file write / one wiring edit / one command — 2-5 minutes each. 5. **No commit steps:** none present; the orchestrator commits. 6. **Pin/replacement substring contiguity:** the integration test's `line.starts_with("{\"manifest\":{\"commit\":\"unknown\",")` and `line.ends_with("\"exposure_sign_flips\":1}}")` are substrings the cycle-0009 `to_json` produces verbatim (manifest-first nesting, `commit` first field, `exposure_sign_flips` last metric); `"window":[1,7]` is the `(Timestamp(1), Timestamp(7))` rendering (window-as-2-array, documented schema). No soft-wrap splits any pinned substring. 7. **Compile-gate vs. deferred-caller ordering:** no signature change — the work is pure-additive (a new module + a new binary body + a new dep). Task 2 Step 1 adds the `aura-std`/`aura-core` deps *before* Step 2 introduces the `use` statements that need them, so each task compiles at its own boundary; Task 1 (the `Recorder` it imports) precedes Task 2. No deferred caller. 8. **Verification-command filter strings resolve:** `cargo test -p aura-std recorder` matches the two `recorder_*` tests named in Task 1; `cargo test -p aura-cli --bin aura run_sample` matches the `run_sample_*` test named in Task 2; `cargo test -p aura-cli --test cli_run` targets the file created in Task 3. Each filter/target is verified against a real named test/file in this plan, not guessed from a feature word. Task 4 runs the unfiltered workspace suite with an explicit "0 failures / new counts" expectation. 9. **Parse-the-bytes-you-inline gate:** the profile declares no `spec_validation` parser, so the gate is a documented no-op for this plan's non-Rust fenced blocks (one `text` price table, the `toml` dep block, the bash Run commands); the Rust bodies are validated by the `implement` compile gate (Tasks 1-4 build commands). No surface-language (non-Rust) program is inlined that a configured parser would own.