refactor(aura-engine): Cell into the construction path — param-plane base/frontend split

Cell becomes the carrier of the construction path; Scalar narrows to the author/render boundaries. The validated/enumerated param point carries no redundant kind (it lives once, in the declared param-space); at the author edge the kind is a checksum — two independent sources, the typed value vs. the slot — so the self-describing Scalar stays there. The name->slot binding is dynamic (C23), so the check is necessarily a runtime one and the value must self-describe for it.

Base/frontend split (the AnyColumn push/push_cell pattern one level up): compile_with_cells / bootstrap_with_cells are the kind-check-free base; compile_with_params / bootstrap_with_params are the frontend that adds only the per-value kind checksum, strips to cells (new Scalar::cell(), the partner of Scalar::from_cell), and delegates. lower_items loses its per-primitive kind-check; PrimitiveBuilder::build and the std node builders (sma/ema/exposure/lincomb) read cells.

Boundary: construction -> Cell (PrimitiveBuilder::build, lower_items, GridSpace.axes/points, SweepPoint.params, the sweep closure). Author edges stay Scalar (GridSpace::new, bind, compile_with_params/bootstrap_with_params). walkforward chosen_params stays a self-describing Scalar report record (Option A — WalkForwardResult carries no space); the cell winner is reconstructed once at the WindowRun site via from_cell.

injective-check moved ahead of the arity-check in the frontend to preserve the pre-split error order (DuplicateParamPath before ParamArity). The lossy i64->f64 param projection (scalar_as_f64 / scalar_as_param_f64) is deliberately untouched — a separate follow-up.

Behaviour-preserving (C1): build --all-targets / test / clippy -D warnings / doc all clean across the workspace.
This commit is contained in:
2026-06-16 16:22:17 +02:00
parent 3598cb9dde
commit 43716be10e
10 changed files with 159 additions and 80 deletions
+53 -27
View File
@@ -15,7 +15,7 @@
//! C23) and no external dependency (C16).
use aura_core::{
FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind,
Cell, FieldSpec, Firing, Node, NodeSchema, ParamSpec, PortSpec, PrimitiveBuilder, Scalar, ScalarKind,
};
use crate::harness::{BootstrapError, Edge, FlatGraph, Harness, SourceSpec, Target};
@@ -197,11 +197,13 @@ impl Composite {
out
}
/// Compile this composite as the ROOT graph under an injected param vector:
/// validate structurally pre-build (via `signature()`, no node built), require
/// every root role bound, then lower (build each primitive, gather its signature,
/// inline composites, rewrite edges, lower bound roles to flat sources).
pub fn compile_with_params(self, params: &[Scalar]) -> Result<FlatGraph, CompileError> {
/// Compile this composite as the ROOT graph from a VALIDATED cell point — the
/// construction base (the cell side of the param-plane split). Structural
/// validation runs here (wiring, root roles, arity), but NOT the per-value kind
/// check: the cells are trusted to match the declared slot kinds, having been
/// checked at the authoring edge (`compile_with_params`, `GridSpace::new`, or
/// `bind`). Lowers by reading each cell as its declared kind.
pub fn compile_with_cells(self, point: &[Cell]) -> Result<FlatGraph, CompileError> {
// structural validation, all pre-build (no node constructed):
let space = self.param_space();
check_param_namespace_injective(&space)?;
@@ -212,8 +214,8 @@ impl Composite {
}
}
if params.len() != space.len() {
return Err(CompileError::ParamArity { expected: space.len(), got: params.len() });
if point.len() != space.len() {
return Err(CompileError::ParamArity { expected: space.len(), got: point.len() });
}
let mut flat_nodes: Vec<Box<dyn Node>> = Vec::new();
@@ -223,7 +225,7 @@ impl Composite {
let lowerings = lower_items(
self.nodes,
params,
point,
&mut cursor,
&mut flat_nodes,
&mut flat_signatures,
@@ -250,6 +252,30 @@ impl Composite {
Ok(FlatGraph { nodes: flat_nodes, signatures: flat_signatures, sources: flat_sources, edges: flat_edges })
}
/// Compile from a self-describing param vector — the authoring-edge frontend
/// over [`Self::compile_with_cells`]. Its sole added responsibility is the per-value
/// kind checksum (each value's `kind()` vs. the declared slot kind, C7's
/// authoring boundary); it then strips the validated values to tag-free cells
/// and delegates to the base. Arity is checked here too, so the checksum's zip
/// cannot silently skip a trailing slot.
pub fn compile_with_params(self, params: &[Scalar]) -> Result<FlatGraph, CompileError> {
let space = self.param_space();
// injective before arity, preserving the pre-split error order: a duplicate
// param path is reported regardless of how many values were supplied (e.g.
// `compile()` passes none). The base re-checks it for the direct cell path.
check_param_namespace_injective(&space)?;
if params.len() != space.len() {
return Err(CompileError::ParamArity { expected: space.len(), got: params.len() });
}
for (slot, (v, spec)) in params.iter().zip(&space).enumerate() {
if v.kind() != spec.kind {
return Err(CompileError::ParamKindMismatch { slot, expected: spec.kind, got: v.kind() });
}
}
let cells: Vec<Cell> = params.iter().map(|s| s.cell()).collect();
self.compile_with_cells(&cells)
}
/// No-param compile (errors `ParamArity` if any param is declared).
pub fn compile(self) -> Result<FlatGraph, CompileError> {
self.compile_with_params(&[])
@@ -261,6 +287,14 @@ impl Composite {
Harness::bootstrap(flat).map_err(CompileError::Bootstrap)
}
/// Bootstrap from a VALIDATED cell point — the cell-side base of
/// [`Self::bootstrap_with_params`]. The sweep path uses this directly: its enumerated
/// point is already kind-checked by `GridSpace::new`, so it skips the frontend.
pub fn bootstrap_with_cells(self, point: &[Cell]) -> Result<Harness, CompileError> {
let flat = self.compile_with_cells(point)?;
Harness::bootstrap(flat).map_err(CompileError::Bootstrap)
}
/// No-param bootstrap.
pub fn bootstrap(self) -> Result<Harness, CompileError> {
self.bootstrap_with_params(vec![])
@@ -348,7 +382,7 @@ impl SweepBinder {
/// run the disjoint sweep.
pub fn sweep<F>(self, run_one: F) -> Result<SweepFamily, BindError>
where
F: Fn(&[Scalar]) -> RunReport + Sync,
F: Fn(&[Cell]) -> RunReport + Sync,
{
let space = self.bp.param_space();
check_param_namespace_injective(&space).map_err(BindError::Compile)?;
@@ -642,7 +676,7 @@ enum ItemLowering {
/// input item, in order.
fn lower_items(
items: Vec<BlueprintNode>,
params: &[Scalar],
point: &[Cell],
cursor: &mut usize,
flat_nodes: &mut Vec<Box<dyn Node>>,
flat_signatures: &mut Vec<NodeSchema>,
@@ -653,17 +687,9 @@ fn lower_items(
match item {
BlueprintNode::Primitive(builder) => {
let n = builder.params().len();
let slice = &params[*cursor..*cursor + n]; // in range: arity checked up front
for (i, spec) in builder.params().iter().enumerate() {
let got = slice[i].kind();
if got != spec.kind {
return Err(CompileError::ParamKindMismatch {
slot: *cursor + i,
expected: spec.kind,
got,
});
}
}
// cells are trusted: kind-checked at the authoring edge (the
// `compile_with_params` frontend); here read each by its declared kind.
let slice = &point[*cursor..*cursor + n]; // in range: arity checked up front
let index = flat_nodes.len();
flat_signatures.push(builder.schema().clone());
flat_nodes.push(builder.build(slice));
@@ -673,7 +699,7 @@ fn lower_items(
BlueprintNode::Composite(c) => {
lowerings.push(inline_composite(
c,
params,
point,
cursor,
flat_nodes,
flat_signatures,
@@ -689,7 +715,7 @@ fn lower_items(
/// edges, then resolve its output port and per-role flat targets.
fn inline_composite(
c: Composite,
params: &[Scalar],
point: &[Cell],
cursor: &mut usize,
flat_nodes: &mut Vec<Box<dyn Node>>,
flat_signatures: &mut Vec<NodeSchema>,
@@ -699,13 +725,13 @@ fn inline_composite(
// (C23 — the boundary does not reach the flat graph), so it is not destructured.
// Node names join the same non-load-bearing debug-symbol class: they qualify the
// param-space path at construction but are dropped at lowering — the injected
// scalar `params: &[Scalar]` arg drives `lower_items` below; the flat graph stays
// cell `point: &[Cell]` arg drives `lower_items` below; the flat graph stays
// wired by raw index.
let Composite { name: _, nodes, edges, input_roles, output } = c;
let item_count = nodes.len();
// recursively lower interior items, then rewrite interior edges through them
let interior = lower_items(nodes, params, cursor, flat_nodes, flat_signatures, flat_edges)?;
let interior = lower_items(nodes, point, cursor, flat_nodes, flat_signatures, flat_edges)?;
for e in &edges {
for fe in rewrite_edge(e, &interior, flat_signatures)? {
flat_edges.push(fe);
@@ -888,7 +914,7 @@ mod tests {
.axis("sma_cross.fast.length", [2.0, 3.0]) // F64 values for the I64 slot
.axis("sma_cross.slow.length", [4])
.axis("exposure.scale", [0.5])
.sweep(|_: &[Scalar]| -> RunReport { panic!("axis pre-validation must reject before running") });
.sweep(|_: &[Cell]| -> RunReport { panic!("axis pre-validation must reject before running") });
assert_eq!(
result,
Err(BindError::KindMismatch {
+24 -20
View File
@@ -4,7 +4,7 @@
//! per-point run-to-metrics closure is the author's (harness-specific sink glue
//! the engine cannot generically own — C8/C18).
use aura_core::{zip_params, ParamSpec, Scalar, ScalarKind};
use aura_core::{zip_params, Cell, ParamSpec, Scalar, ScalarKind};
use crate::RunReport;
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -14,7 +14,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug)]
pub struct GridSpace {
space: Vec<ParamSpec>,
axes: Vec<Vec<Scalar>>,
axes: Vec<Vec<Cell>>,
}
impl GridSpace {
@@ -41,6 +41,10 @@ impl GridSpace {
}
}
}
// author edge: the values were just kind-checked above; strip the tag and
// store the enumerated grid as tag-free cells (the kind now lives once, in
// `space`).
let axes = axes.iter().map(|ax| ax.iter().map(|s| s.cell()).collect()).collect();
Ok(Self { space: space.to_vec(), axes })
}
@@ -58,7 +62,7 @@ impl GridSpace {
/// The cartesian product, in odometer order: the **last** axis varies
/// fastest. Deterministic — the same grid yields the same point sequence.
pub fn points(&self) -> Vec<Vec<Scalar>> {
pub fn points(&self) -> Vec<Vec<Cell>> {
let mut out = Vec::with_capacity(self.len());
let mut idx = vec![0usize; self.axes.len()];
loop {
@@ -104,7 +108,7 @@ pub enum SweepError {
/// registry indexes (C18).
#[derive(Clone, Debug, PartialEq)]
pub struct SweepPoint {
pub params: Vec<Scalar>,
pub params: Vec<Cell>,
pub report: RunReport,
}
@@ -131,7 +135,7 @@ impl SweepFamily {
/// via `std::thread::scope`.
pub fn sweep<F>(space: &GridSpace, run_one: F) -> SweepFamily
where
F: Fn(&[Scalar]) -> RunReport + Sync,
F: Fn(&[Cell]) -> RunReport + Sync,
{
let nthreads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
sweep_with_threads(space, nthreads, run_one)
@@ -184,7 +188,7 @@ where
/// onto their points in enumeration (odometer) order.
fn sweep_with_threads<F>(space: &GridSpace, nthreads: usize, run_one: F) -> SweepFamily
where
F: Fn(&[Scalar]) -> RunReport + Sync,
F: Fn(&[Cell]) -> RunReport + Sync,
{
let points = space.points();
let reports = run_indexed(points.len(), nthreads, |i| run_one(&points[i]));
@@ -203,7 +207,7 @@ mod tests {
use super::*;
use crate::test_fixtures::{composite_sma_cross_harness, synthetic_prices};
use crate::{f64_field, summarize, RunManifest, VecSource};
use aura_core::{ParamSpec, Scalar, ScalarKind, Timestamp};
use aura_core::{Cell, ParamSpec, Scalar, ScalarKind, Timestamp};
fn i64_space(n: usize) -> Vec<ParamSpec> {
(0..n)
@@ -225,10 +229,10 @@ mod tests {
assert_eq!(
grid.points(),
vec![
vec![Scalar::i64(2), Scalar::i64(4)],
vec![Scalar::i64(2), Scalar::i64(5)],
vec![Scalar::i64(3), Scalar::i64(4)],
vec![Scalar::i64(3), Scalar::i64(5)],
vec![Cell::from_i64(2), Cell::from_i64(4)],
vec![Cell::from_i64(2), Cell::from_i64(5)],
vec![Cell::from_i64(3), Cell::from_i64(4)],
vec![Cell::from_i64(3), Cell::from_i64(5)],
],
);
}
@@ -287,11 +291,11 @@ mod tests {
/// AND a direct reference for the "sweep == N independent runs" comparison.
/// The manifest is a minimal fixed fixture — the metrics are the run's, and
/// determinism makes `run_point` reproduce a point's report exactly.
fn run_point(point: &[Scalar]) -> RunReport {
fn run_point(point: &[Cell]) -> RunReport {
let (bp, rx_eq, rx_ex) = composite_sma_cross_harness();
let mut h = bp
.bootstrap_with_params(point.to_vec())
.expect("grid points are kind-checked against param_space");
.bootstrap_with_cells(point)
.expect("enumerated grid points are pre-validated by GridSpace::new");
h.run(vec![Box::new(VecSource::new(synthetic_prices()))]);
let equity = f64_field(&rx_eq.try_iter().collect::<Vec<_>>(), 0);
let exposure = f64_field(&rx_ex.try_iter().collect::<Vec<_>>(), 0);
@@ -325,14 +329,15 @@ mod tests {
let grid = sma_cross_grid();
let family = sweep(&grid, run_point);
assert_eq!(family.points.len(), 4);
// params carried in enumeration (odometer) order, self-describing
// params carried in enumeration (odometer) order, as tag-free cells (the
// kind lives once, in `family.space`)
assert_eq!(
family.points[0].params,
vec![Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)],
vec![Cell::from_i64(2), Cell::from_i64(4), Cell::from_f64(0.5)],
);
assert_eq!(
family.points[3].params,
vec![Scalar::i64(3), Scalar::i64(5), Scalar::f64(0.5)],
vec![Cell::from_i64(3), Cell::from_i64(5), Cell::from_f64(0.5)],
);
// each point's metrics equal a direct, independent run of the same point:
// the sweep adds enumeration + execution, never a metrics change (C1).
@@ -356,9 +361,8 @@ mod tests {
// odometer-first point is [I64(2), I64(4), F64(0.5)]
let expected: Vec<(String, Scalar)> = space
.iter()
.cloned()
.zip(family.points[0].params.clone())
.map(|(ps, v)| (ps.name, v))
.zip(&family.points[0].params)
.map(|(ps, c)| (ps.name.clone(), Scalar::from_cell(ps.kind, *c)))
.collect();
assert_eq!(family.named_params(0), expected);
assert_eq!(family.named_params(0)[0].1, Scalar::i64(2));