Files
Aura/docs/plans/0010-aura-run-cli.md
T
Brummel 93195aa04b plan: 0010 aura run CLI
Placeholder-free, verbatim-code plan for cycle 0010 (#8). Two deliverables in
dependency order, four tasks:

1. aura-std::Recorder — a four-kind recording sink (pure consumer, output:
   vec![], holds an mpsc::Sender), mirroring the existing #[cfg(test)] fixture;
   wired into lib.rs alphabetically; two unit tests (f64 capture after warm-up,
   None-until-all-columns-warm).
2. aura-cli run subcommand — synthetic_prices / sample_harness / run_sample /
   main over the raw Harness::bootstrap API (no builder DSL), + aura-std/aura-core
   path deps; a unit test pinning determinism and the hand-computed metrics.
3. tests/cli_run.rs — integration test driving the built binary (run -> exit 0 +
   single-line JSON; no args -> exit 2 + usage stderr).
4. Workspace gates (test / clippy -D warnings / doc -D warnings).

The chosen synthetic stream (7 ticks, rises then reverses) is traced tick-by-tick
in the plan: equity [0,0,0,0,-0.08,-0.17,-0.13] -> total_pips -0.13, max_drawdown
0.17, exposure_sign_flips 1. The integer flip count is pinned exactly; the two
f64 metrics within 1e-9 (dust ~1e-15); determinism pinned exactly.

refs #8
2026-06-04 20:39:50 +02:00

24 KiB

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<Scalar>)>, 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.rsrun → 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:

//! `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<ScalarKind>,
    firing: Firing,
    tx: Sender<(Timestamp, Vec<Scalar>)>,
}

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<Scalar>)>) -> 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<Scalar>)> = 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<Scalar>)> = 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:

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:

[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:

//! `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<Scalar>)>,
    Receiver<(Timestamp, Vec<Scalar>)>,
) {
    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<Scalar>)> = rx_eq.try_iter().collect();
    let ex_rows: Vec<(Timestamp, Vec<Scalar>)> = 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:

//! 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.