Files
Aura/docs/plans/0009-run-metrics-and-manifest.md
T
Brummel cb66708d26 plan: 0009 run metrics + manifest
Six-task plan for the cycle-0009 report surface (issue #6): module scaffold +
RunMetrics/RunManifest/RunReport types (Task 1), the pure summarize reduction
(Task 2, RED-first), the f64_field recorded-stream adapter (Task 3, RED-first),
RunReport::to_json canonical zero-dep JSON (Task 4, RED-first), an end-to-end
determinism test reusing the cycle-0007 two-sink harness (Task 5), and the full
workspace gates (Task 6). Pure-additive in a new aura-engine report module; the
re-export line grows per task so the crate compiles at every boundary.

refs #6
2026-06-04 18:51:04 +02:00

25 KiB

Run metrics + run manifest — Implementation Plan

Parent spec: docs/specs/0009-run-metrics-and-manifest.md

For agentic workers: REQUIRED SUB-SKILL: use the implement skill to run this plan. Steps use - [ ] checkboxes for tracking.

Goal: Deliver a new report module in aura-engine that reduces a run's recorded pip-equity + exposure streams into summary metrics and pairs them with a caller-supplied run manifest, rendered as canonical zero-dependency JSON.

Architecture: A pure, post-run reduction (summarize) over recorded (Timestamp, f64) streams plus three plain data types (RunMetrics, RunManifest, RunReport) and a Vec<Scalar>-row adapter (f64_field), all in crates/aura-engine/src/report.rs, exported from lib.rs. No engine / Harness / node-contract change; the surface is pure-additive and uses only aura-core (Scalar, Timestamp). The end-to-end test reuses the cycle-0007 harness shape under #[cfg(test)] (where aura-std is a dev-dependency).

Tech Stack: Rust 2024, aura-core (Scalar/Timestamp), aura-engine (Harness + the cycle-0006 recording-sink mechanism), aura-std nodes (Sma/Sub/Exposure/SimBroker) in the test only.


Files this plan creates or modifies:

  • Create: crates/aura-engine/src/report.rs — the report module: types, summarize, f64_field, RunReport::to_json, helpers, and all tests.
  • Modify: crates/aura-engine/src/lib.rs:16-26 — wire mod report; + re-exports and amend the module-doc "Still to come" list.
  • Test: crates/aura-engine/src/report.rs (#[cfg(test)] mod tests) — unit tests for summarize / f64_field / to_json plus one end-to-end determinism test.

Resolved (recon open question): the end-to-end test builds SimBroker::new(0.0001), so its manifest broker label reads "sim-optimal(pip_size=0.0001)" (faithful to the actual broker). The spec's north-star snippet used pip_size=1.0 illustratively; the to_json snapshot unit test (Task 4) keeps the spec's canonical example values verbatim via a hand-built RunReport, independent of any run.


Task 1: Module scaffold — the three data types + lib wiring

Files:

  • Create: crates/aura-engine/src/report.rs

  • Modify: crates/aura-engine/src/lib.rs:16-26

  • Step 1: Create report.rs with the module doc and the three data types

Create crates/aura-engine/src/report.rs with exactly this content:

//! Run summary metrics + the reproducible run manifest (C18 / C12): the
//! `(manifest, metrics)` pair a run produces "from day one". The metrics are a
//! **post-run pure reduction** over a run's recorded streams — a node cannot
//! reduce end-of-run (C8 caps a node at one record per `eval`, with no terminal
//! `eval`), so the World drains its recording sinks after [`Harness::run`](crate::Harness::run)
//! and folds them here. Output is canonical, hand-rolled JSON (C14): the schema
//! is tiny, closed, and flat, so the deliberately zero-dependency workspace
//! stays zero-dependency.

use aura_core::{Scalar, Timestamp};

/// Summary metrics reduced from a run's recorded streams — the `-> metrics`
/// half of C12's atomic sim unit. Pure function of the recorded streams.
#[derive(Clone, Debug, PartialEq)]
pub struct RunMetrics {
    /// Final cumulative pip equity — the last value of the (cumulative)
    /// pip-equity curve. `0.0` if the curve is empty.
    pub total_pips: f64,
    /// Largest peak-to-trough drop on the cumulative pip curve:
    /// `max_t (running_peak(t) - equity(t))`, always `>= 0.0` (`0.0` if the
    /// curve is monotonic non-decreasing or empty).
    pub max_drawdown: f64,
    /// Count of adjacent recorded exposure samples whose sign differs (a zero
    /// exposure normalizes to sign `0`, so flat is distinct from long/short).
    /// A turnover proxy: it counts long<->short reversals *and* transitions
    /// into/out of flat — the plain sign-change count over the exposure series.
    pub exposure_sign_flips: u64,
}

/// The reproducible run descriptor (C18). **Caller-supplied**: the engine
/// cannot introspect a git commit, an RNG seed, or a broker label — the World
/// that bootstraps and runs the harness fills these in.
#[derive(Clone, Debug, PartialEq)]
pub struct RunManifest {
    /// Node/engine identity: the git commit of the frozen artifact (C18 —
    /// commit = identity; the frozen bot *is* a commit).
    pub commit: String,
    /// The bound tuning params as ordered `name -> value` pairs — the precursor
    /// to the eventual typed param-space (deferred; see spec Non-goals).
    pub params: Vec<(String, f64)>,
    /// The data-window: inclusive `(from, to)` epoch-ns bounds (C12).
    pub window: (Timestamp, Timestamp),
    /// The RNG seed (C12 seed-as-input). `0` for a seed-free synthetic run.
    pub seed: u64,
    /// The broker profile label, e.g. `"sim-optimal(pip_size=0.0001)"`.
    pub broker: String,
}

/// A run's full structured result: the descriptor plus the metrics it
/// reproduces. The durable run record of C18 ("stores manifests + metrics,
/// re-derives full results on demand").
#[derive(Clone, Debug, PartialEq)]
pub struct RunReport {
    pub manifest: RunManifest,
    pub metrics: RunMetrics,
}
  • Step 2: Wire the module and re-exports in lib.rs, amend the doc

In crates/aura-engine/src/lib.rs, replace the "Still to come" doc paragraph (lines 16-20), which currently reads:

//! Still to come (subsequent cycles): the `Source` trait + data-server ingestion
//! and source-native time normalization (C3/C11), the broker-independent
//! position-event output and downstream broker nodes (C10), and the atomic sim
//! unit `(topology + params + data-window + seed) -> metrics` that the sweep /
//! optimize / walk-forward / Monte-Carlo axes orchestrate.

with:

//! Delivered in cycle 0009 — the run report surface:
//!
//! - [`RunMetrics`] / [`RunManifest`] / [`RunReport`] — the `(manifest, metrics)`
//!   pair C18 mandates per run, with [`RunReport::to_json`] for the structured
//!   C14 face;
//! - [`summarize`] — the post-run pure reduction over a run's recorded
//!   pip-equity + exposure streams into [`RunMetrics`]; [`f64_field`] bridges a
//!   recording sink's `Vec<Scalar>` rows to it.
//!
//! Still to come (subsequent cycles): the `Source` trait + data-server ingestion
//! and source-native time normalization (C3/C11), the broker-independent
//! position-event output and downstream broker nodes (C10), and the param
//! injection + orchestration axes of the atomic sim unit
//! (`(topology + params + data-window + seed)`, swept by optimize / walk-forward
//! / Monte-Carlo) — its `-> metrics` reduction now ships via [`summarize`].

Then replace the module/exports block (lines 24-26), which currently reads:

mod harness;

pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target};

with (re-export the types onlysummarize / f64_field are added to this line by Task 2 and Task 3 as each function lands, so the crate compiles at every task boundary):

mod harness;
mod report;

pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
pub use report::{RunManifest, RunMetrics, RunReport};
  • Step 3: Build gate

Run: cargo build -p aura-engine Expected: PASS — 0 errors, 0 warnings (the three types compile and are exported; no function is named in the pub use yet, so the crate compiles cleanly).


Task 2: summarize + the reduction (RED-first)

Files:

  • Modify: crates/aura-engine/src/report.rs

  • Modify: crates/aura-engine/src/lib.rs (extend the re-export)

  • Step 1: Write the failing tests

Append to crates/aura-engine/src/report.rs a test module:

#[cfg(test)]
mod tests {
    use super::*;

    fn samples(values: &[f64]) -> Vec<(Timestamp, f64)> {
        values
            .iter()
            .enumerate()
            .map(|(i, &v)| (Timestamp(i as i64 + 1), v))
            .collect()
    }

    #[test]
    fn summarize_total_pips_is_last_cumulative_value() {
        let equity = samples(&[0.0, 5.0, 4.0, 12.0]);
        let m = summarize(&equity, &[]);
        assert_eq!(m.total_pips, 12.0);
    }

    #[test]
    fn summarize_is_zero_on_empty_streams() {
        let m = summarize(&[], &[]);
        assert_eq!(m.total_pips, 0.0);
        assert_eq!(m.max_drawdown, 0.0);
        assert_eq!(m.exposure_sign_flips, 0);
    }

    #[test]
    fn summarize_max_drawdown_is_worst_peak_to_trough() {
        // peak 10 then trough 5 (drop 5), recovers to 8; worst drop is 5,
        // not the final drop (10 -> 8 = 2).
        let equity = samples(&[0.0, 10.0, 5.0, 8.0]);
        let m = summarize(&equity, &[]);
        assert_eq!(m.max_drawdown, 5.0);
    }

    #[test]
    fn summarize_max_drawdown_zero_on_monotonic_curve() {
        let equity = samples(&[0.0, 1.0, 2.0, 3.0]);
        let m = summarize(&equity, &[]);
        assert_eq!(m.max_drawdown, 0.0);
    }

    #[test]
    fn summarize_sign_flips_counts_signum_changes() {
        // signum series: + + - 0 -  ->  flips at +->-, -->0, 0->- = 3.
        let exposure = samples(&[0.5, 0.5, -0.5, 0.0, -0.5]);
        let m = summarize(&[], &exposure);
        assert_eq!(m.exposure_sign_flips, 3);
    }

    #[test]
    fn summarize_sign_flips_zero_on_constant_sign() {
        let exposure = samples(&[0.2, 0.5, 1.0, 0.7]);
        let m = summarize(&[], &exposure);
        assert_eq!(m.exposure_sign_flips, 0);
    }
}
  • Step 2: Run the tests to verify they fail

Run: cargo test -p aura-engine summarize Expected: FAIL — compile error cannot find function summarize in this scope (the function does not exist yet).

  • Step 3: Write the reduction

In crates/aura-engine/src/report.rs, after the RunReport struct and before the #[cfg(test)] module, add:

/// Reduce a run's recorded pip-equity + exposure streams into summary metrics.
/// Pure — identical inputs yield identical metrics (C1/C12). Timestamps are
/// carried in the input to match exactly what a sink records; the reduction
/// itself is value-only (it does not read the timestamps).
pub fn summarize(
    equity: &[(Timestamp, f64)],
    exposure: &[(Timestamp, f64)],
) -> RunMetrics {
    // total pips: the last cumulative equity value (0.0 if empty).
    let total_pips = equity.last().map(|&(_, v)| v).unwrap_or(0.0);

    // max drawdown: the largest running-peak-minus-value, always >= 0.0.
    let mut peak = f64::NEG_INFINITY;
    let mut max_drawdown = 0.0_f64;
    for &(_, v) in equity {
        if v > peak {
            peak = v;
        }
        let dd = peak - v;
        if dd > max_drawdown {
            max_drawdown = dd;
        }
    }

    // exposure sign-flips: adjacent samples whose normalized sign differs.
    let mut exposure_sign_flips = 0u64;
    let mut prev: Option<f64> = None;
    for &(_, v) in exposure {
        let s = sign0(v);
        if let Some(p) = prev {
            if s != p {
                exposure_sign_flips += 1;
            }
        }
        prev = Some(s);
    }

    RunMetrics { total_pips, max_drawdown, exposure_sign_flips }
}

/// Three-way sign: `-1.0` / `0.0` / `+1.0`. Unlike `f64::signum` (which returns
/// `+1.0` for `+0.0`), a zero exposure maps to `0.0` so flat is distinct from
/// long/short in the sign-flip count.
fn sign0(v: f64) -> f64 {
    if v > 0.0 {
        1.0
    } else if v < 0.0 {
        -1.0
    } else {
        0.0
    }
}

Then extend the re-export in crates/aura-engine/src/lib.rs:

pub use report::{summarize, RunManifest, RunMetrics, RunReport};
  • Step 4: Run the tests to verify they pass

Run: cargo test -p aura-engine summarize Expected: PASS — 6 tests run, 0 failed (all six summarize_* tests match the summarize filter).


Task 3: f64_field adapter (RED-first)

Files:

  • Modify: crates/aura-engine/src/report.rs

  • Modify: crates/aura-engine/src/lib.rs (extend the re-export)

  • Step 1: Write the failing tests

Inside the existing #[cfg(test)] mod tests in report.rs, add:

    #[test]
    fn f64_field_projects_the_named_field() {
        let rows = vec![
            (Timestamp(1), vec![Scalar::F64(1.5), Scalar::I64(9)]),
            (Timestamp(2), vec![Scalar::F64(2.5), Scalar::I64(8)]),
        ];
        assert_eq!(
            f64_field(&rows, 0),
            vec![(Timestamp(1), 1.5), (Timestamp(2), 2.5)],
        );
    }

    #[test]
    #[should_panic(expected = "not an f64 scalar")]
    fn f64_field_panics_on_kind_mismatch() {
        let rows = vec![(Timestamp(1), vec![Scalar::I64(7)])];
        let _ = f64_field(&rows, 0);
    }
  • Step 2: Run the tests to verify they fail

Run: cargo test -p aura-engine f64_field Expected: FAIL — compile error cannot find function f64_field in this scope.

  • Step 3: Write the adapter

In crates/aura-engine/src/report.rs, after summarize/sign0 and before the test module, add:

/// Bridge a recording sink's recorded `(ts, row)` stream to [`summarize`]:
/// extract one `f64` field of each row into `(ts, f64)` samples. Panics if a
/// row has no such field or the field is not an `f64` scalar — a wiring bug (a
/// sink's declared kinds are fixed at bootstrap, so a correctly-wired
/// equity/exposure sink always yields `f64` at field 0), surfaced like the
/// engine's other "checked at wiring" contract violations rather than silently
/// dropped.
pub fn f64_field(rows: &[(Timestamp, Vec<Scalar>)], field: usize) -> Vec<(Timestamp, f64)> {
    rows.iter()
        .map(|(ts, row)| {
            let Some(&scalar) = row.get(field) else {
                panic!("f64_field: row has no field {field} (row width {})", row.len());
            };
            let Scalar::F64(v) = scalar else {
                panic!("f64_field: field {field} is not an f64 scalar: {scalar:?}");
            };
            (*ts, v)
        })
        .collect()
}

Then extend the re-export in crates/aura-engine/src/lib.rs:

pub use report::{f64_field, summarize, RunManifest, RunMetrics, RunReport};
  • Step 4: Run the tests to verify they pass

Run: cargo test -p aura-engine f64_field Expected: PASS — 2 tests run, 0 failed.


Task 4: RunReport::to_json canonical JSON (RED-first)

Files:

  • Modify: crates/aura-engine/src/report.rs

  • Step 1: Write the failing test

Inside the #[cfg(test)] mod tests in report.rs, add:

    #[test]
    fn to_json_renders_the_canonical_form() {
        let report = RunReport {
            manifest: RunManifest {
                commit: "abc123".to_string(),
                params: vec![
                    ("sma_fast".to_string(), 2.0),
                    ("sma_slow".to_string(), 4.0),
                    ("exposure_scale".to_string(), 1.0),
                ],
                window: (Timestamp(1), Timestamp(6)),
                seed: 0,
                broker: "sim-optimal(pip_size=1.0)".to_string(),
            },
            metrics: RunMetrics {
                total_pips: 12.0,
                max_drawdown: 1.0,
                exposure_sign_flips: 1,
            },
        };
        assert_eq!(
            report.to_json(),
            r#"{"manifest":{"commit":"abc123","params":{"sma_fast":2,"sma_slow":4,"exposure_scale":1},"window":[1,6],"seed":0,"broker":"sim-optimal(pip_size=1.0)"},"metrics":{"total_pips":12,"max_drawdown":1,"exposure_sign_flips":1}}"#,
        );
    }
  • Step 2: Run the test to verify it fails

Run: cargo test -p aura-engine to_json Expected: FAIL — compile error no method named to_json found for ... RunReport.

  • Step 3: Write to_json + the string-escape helper

In crates/aura-engine/src/report.rs, after the RunReport struct definition, add an impl block and a free helper (place the helper next to sign0):

impl RunReport {
    /// Render canonical, machine-readable JSON (C14). Hand-rolled — the schema
    /// is tiny, closed, and flat, so the deliberately zero-dependency workspace
    /// stays so. Field order is fixed; `f64` uses the round-trippable `{}`
    /// shortest form (finite values only — pip equity and exposure are finite
    /// by construction); `params` renders as a JSON object in insertion order.
    pub fn to_json(&self) -> String {
        let m = &self.manifest;
        let mut params = String::from("{");
        for (i, (name, value)) in m.params.iter().enumerate() {
            if i > 0 {
                params.push(',');
            }
            params.push_str(&json_str(name));
            params.push(':');
            params.push_str(&value.to_string());
        }
        params.push('}');
        format!(
            "{{\"manifest\":{{\"commit\":{commit},\"params\":{params},\"window\":[{from},{to}],\"seed\":{seed},\"broker\":{broker}}},\"metrics\":{{\"total_pips\":{pips},\"max_drawdown\":{dd},\"exposure_sign_flips\":{flips}}}}}",
            commit = json_str(&m.commit),
            from = m.window.0.0,
            to = m.window.1.0,
            seed = m.seed,
            broker = json_str(&m.broker),
            pips = self.metrics.total_pips,
            dd = self.metrics.max_drawdown,
            flips = self.metrics.exposure_sign_flips,
        )
    }
}

/// Minimal JSON string rendering: wrap in quotes, escape `"` and `\`. Our
/// caller-supplied labels (commit hash, param names, broker label) contain
/// neither control characters nor other JSON-significant bytes, so these two
/// escapes are sufficient.
fn json_str(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('"');
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            _ => out.push(c),
        }
    }
    out.push('"');
    out
}
  • Step 4: Run the test to verify it passes

Run: cargo test -p aura-engine to_json Expected: PASS — 1 test run, 0 failed.


Task 5: End-to-end determinism test (the C18 / acceptance demonstrator)

Files:

  • Modify: crates/aura-engine/src/report.rs (test module only)

  • Step 1: Add the harness fixtures + the determinism test

Inside the #[cfg(test)] mod tests in report.rs, add the fixture imports at the top of the module (just below use super::*;):

    use crate::{Edge, Harness, SourceSpec, Target};
    use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, ScalarKind};
    use aura_std::{Exposure, SimBroker, Sma, Sub};
    use std::sync::mpsc;

    /// Build an f64 source stream from (timestamp, value) points (mirrors the
    /// harness.rs test helper; the e2e test needs its own copy — the harness
    /// test module's is private to that module).
    fn f64_stream(points: &[(i64, f64)]) -> Vec<(Timestamp, Scalar)> {
        points.iter().map(|&(t, v)| (Timestamp(t), Scalar::F64(v))).collect()
    }

    /// A recording sink that sends `(now, row)` out of the graph each fired
    /// cycle and returns `None` (pure consumer, C8). Re-declared here because
    /// the identically-shaped fixture in `harness.rs` is private to that test
    /// module.
    struct Recorder {
        kinds: Vec<ScalarKind>,
        firing: Firing,
        tx: mpsc::Sender<(Timestamp, Vec<Scalar>)>,
    }
    impl Recorder {
        fn new(
            kinds: &[ScalarKind],
            firing: Firing,
            tx: mpsc::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]> {
            // this test records only f64 streams (one f64 column per sink)
            let mut row = Vec::with_capacity(self.kinds.len());
            for i in 0..self.kinds.len() {
                let w = ctx.f64_in(i);
                if w.is_empty() {
                    return None;
                }
                row.push(Scalar::F64(w[0]));
            }
            let _ = self.tx.send((ctx.now(), row));
            None
        }
    }

    /// Bootstrap the cycle-0007 signal-quality harness with TWO sinks: one on
    /// the SimBroker equity output (node 4 -> node 5) and one on the Exposure
    /// output (node 3 -> node 6). Returns the harness plus the two receivers.
    #[allow(clippy::type_complexity)]
    fn build_two_sink_harness() -> (
        Harness,
        mpsc::Receiver<(Timestamp, Vec<Scalar>)>,
        mpsc::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
                Box::new(Sma::new(4)),                                       // 1
                Box::new(Sub::new()),                                        // 2
                Box::new(Exposure::new(0.5)),                                // 3
                Box::new(SimBroker::new(0.0001)),                            // 4
                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
                ],
            }],
            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 },
                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 signal-quality DAG");
        (h, rx_eq, rx_ex)
    }

    fn run_once() -> RunReport {
        let (mut h, rx_eq, rx_ex) = build_two_sink_harness();
        h.run(vec![f64_stream(&[
            (1, 1.0000),
            (2, 1.0010),
            (3, 1.0025),
            (4, 1.0020),
            (5, 1.0040),
        ])]);
        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: "test-commit".to_string(),
                params: vec![
                    ("sma_fast".to_string(), 2.0),
                    ("sma_slow".to_string(), 4.0),
                    ("exposure_scale".to_string(), 0.5),
                ],
                window: (Timestamp(1), Timestamp(5)),
                seed: 0,
                broker: "sim-optimal(pip_size=0.0001)".to_string(),
            },
            metrics,
        }
    }

    #[test]
    fn report_is_deterministic_end_to_end() {
        let r1 = run_once();
        let r2 = run_once();
        // a run actually emitted metrics over a non-empty pip curve
        assert!(r1.metrics.total_pips.is_finite());
        // same manifest -> same metrics (C1/C12): two runs are bit-identical
        assert_eq!(r1.metrics, r2.metrics);
        assert_eq!(r1.to_json(), r2.to_json());
    }
  • Step 2: Run the test

Run: cargo test -p aura-engine report_is_deterministic_end_to_end Expected: PASS — 1 test run, 0 failed (it composes the already-built surface; it is a property/integration test, not a RED driver for new production code).


Task 6: Full workspace gates

Files: none (verification only).

  • Step 1: Full test suite

Run: cargo test --workspace Expected: PASS — all tests across the workspace, 0 failed. The aura-engine suite now includes the cycle-0009 report tests (9 unit + 1 e2e) on top of the existing harness/node tests.

  • Step 2: Clippy

Run: cargo clippy --workspace --all-targets -- -D warnings Expected: clean — 0 warnings.

  • Step 3: Doc build

Run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps Expected: clean — 0 warnings (intra-doc links [\RunMetrics`], [`RunReport::to_json`], [`summarize`], [`f64_field`], `Harness::run`` all resolve).