feat(engine,registry): ListSpace + campaign-run realization store (0107 tasks 2-3)

aura-engine: ListSpace — explicit point set implementing Space beside
GridSpace/RandomSpace (a gate's survivor subset has no cartesian
structure); GridSpace-identical arity/kind validation reusing the
existing SweepError variants (KindMismatch.value_index carries the
point ordinal for a list); empty point list is valid and sweeps to an
empty family; re-exported at the crate root.

aura-registry: CampaignRunRecord/CellRealization/StageRealization/
StageSelection — the thin campaign-level linking record over untouched
family records (#198 decision 4) — with append_campaign_run (assigns
run via next_campaign_run, a parallel counter beside next_run) and
load_campaign_runs (missing file => empty) on the campaign_runs.jsonl
sibling store; blueprint_path and find_blueprint_by_identity promoted
to pub (the campaign executor resolves the same refs the referential
tier validates).

Gates: engine 290/0, registry 54/0, clippy -D warnings clean.

refs #198
This commit is contained in:
2026-07-03 19:34:08 +02:00
parent 67aff34923
commit b43def7d75
6 changed files with 692 additions and 25 deletions
+2 -1
View File
@@ -75,7 +75,8 @@ pub use report::{
RunReport, SelectionMode,
};
pub use sweep::{
sweep, GridSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily, SweepPoint,
sweep, GridSpace, ListSpace, ParamRange, RandomSpace, Space, SweepError, SweepFamily,
SweepPoint,
};
pub use mc::{
monte_carlo, r_bootstrap, resample_block, McAggregate, McDraw, McFamily, MetricStats,
+163 -14
View File
@@ -1,11 +1,13 @@
//! Param-sweep (C12.1): enumerate a blueprint's param-space — either a cartesian
//! `GridSpace` (a discrete per-slot lattice) or a seeded `RandomSpace` (`N` draws
//! over typed continuous ranges) — and run each point disjointly (C1). Both
//! enumerations implement the `Space` trait that `sweep` is generic over, so
//! either runs through one execution path. This module owns enumeration
//! (`GridSpace` / `RandomSpace` / the `Space` trait), execution (`sweep`), and
//! collection (`SweepFamily`); the per-point run-to-metrics closure is the
//! author's (harness-specific sink glue the engine cannot generically own — C8/C18).
//! Param-sweep (C12.1): enumerate a blueprint's param-space — a cartesian
//! `GridSpace` (a discrete per-slot lattice), a seeded `RandomSpace` (`N` draws
//! over typed continuous ranges), or an explicit `ListSpace` (an arbitrary
//! validated point set, e.g. a gate's survivor subset) — and run each point
//! disjointly (C1). All three enumerations implement the `Space` trait that
//! `sweep` is generic over, so each runs through one execution path. This module
//! owns enumeration (`GridSpace` / `RandomSpace` / `ListSpace` / the `Space`
//! trait), execution (`sweep`), and collection (`SweepFamily`); the 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, Cell, ParamSpec, Scalar, ScalarKind};
use crate::RunReport;
@@ -125,15 +127,19 @@ impl Space for GridSpace {
}
}
/// A structural fault constructing a `GridSpace` or a `RandomSpace` — the shared
/// typed gate before any run (grid faults: `Arity` / `KindMismatch` / `EmptyAxis`;
/// random faults: `NonNumericRange` / `RangeKindMismatch` / `EmptyRange`).
/// A structural fault constructing a `GridSpace`, a `RandomSpace`, or a
/// `ListSpace` — the shared typed gate before any run (grid faults: `Arity` /
/// `KindMismatch` / `EmptyAxis`; random faults: `NonNumericRange` /
/// `RangeKindMismatch` / `EmptyRange`; list faults reuse `Arity` /
/// `KindMismatch`, with `value_index` carrying the point ordinal).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SweepError {
/// The number of axes does not equal the param-space slot count.
/// The number of axes (grid/random) — or of a single point's values (list)
/// — does not equal the param-space slot count.
Arity { expected: usize, got: usize },
/// A grid value's kind does not match its slot's declared kind. `slot` is the
/// flat param-space index; `value_index` is the position within that axis.
/// A grid or list value's kind does not match its slot's declared kind.
/// `slot` is the flat param-space index; `value_index` is the position
/// within that axis (grid) or the point ordinal (list).
KindMismatch { slot: usize, value_index: usize, expected: ScalarKind, got: ScalarKind },
/// A slot was given no values (would collapse the product to zero points).
EmptyAxis { slot: usize },
@@ -282,6 +288,58 @@ impl Space for RandomSpace {
}
}
/// An explicit, validated point set over a blueprint's param-space — the
/// enumeration for arbitrary member subsets (e.g. a gate's survivor set) that
/// no cartesian `GridSpace` can represent. Points are validated against
/// `space` at construction (the `GridSpace::new` contract: arity per point,
/// kind per slot); an empty point list is valid and yields an empty family.
#[derive(Debug)]
pub struct ListSpace {
space: Vec<ParamSpec>,
points: Vec<Vec<Scalar>>,
}
impl ListSpace {
/// Validate `points` against `space` (the blueprint's `param_space()`):
/// every point carries one value per slot (`Arity`), every value the
/// slot's declared kind (`KindMismatch`, whose `value_index` is the point
/// ordinal here). An empty point list is allowed — a legitimately-empty
/// survivor set is representable, it just enumerates zero points.
pub fn new(space: &[ParamSpec], points: Vec<Vec<Scalar>>) -> Result<Self, SweepError> {
for (point_index, point) in points.iter().enumerate() {
if point.len() != space.len() {
return Err(SweepError::Arity { expected: space.len(), got: point.len() });
}
for (slot, (v, ps)) in point.iter().zip(space).enumerate() {
if v.kind() != ps.kind {
return Err(SweepError::KindMismatch {
slot,
value_index: point_index,
expected: ps.kind,
got: v.kind(),
});
}
}
}
Ok(Self { space: space.to_vec(), points })
}
}
impl Space for ListSpace {
/// The points exactly as given, in input order (no enumeration structure —
/// determinism is the caller's declared order, C1), lowered to tag-free
/// cells in the declared kinds (the kind lives once, in `param_specs()`).
fn points(&self) -> Vec<Vec<Cell>> {
self.points
.iter()
.map(|p| p.iter().map(|s| s.cell()).collect())
.collect()
}
fn param_specs(&self) -> &[ParamSpec] {
&self.space
}
}
/// One enumerated point and the full `RunReport` its run produced.
/// Self-describing: `params` is the point's coordinate in `param_space()` order,
/// and `report` carries the run's `(manifest, metrics)` — the unit the run
@@ -810,4 +868,95 @@ mod tests {
.collect();
assert_eq!(family.named_params(0), expected);
}
/// A fake report whose manifest `commit` encodes the input cells — enough to
/// prove the sweep closure ran on exactly the given point, without a harness
/// run. A free `fn` (Sync) so it serves directly as the `sweep` closure.
fn tagged_report(point: &[Cell]) -> RunReport {
RunReport {
manifest: RunManifest {
commit: point.iter().map(|c| c.i64().to_string()).collect::<Vec<_>>().join(","),
params: Vec::new(),
window: (Timestamp(0), Timestamp(0)),
seed: 0,
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: summarize(&[], &[]),
}
}
#[test]
fn list_space_runs_exactly_the_given_points_in_order() {
let space = i64_space(2);
// deliberately non-odometer order: the list enumerates the input order,
// not any canonical order
let ls = ListSpace::new(
&space,
vec![
vec![Scalar::i64(3), Scalar::i64(5)],
vec![Scalar::i64(2), Scalar::i64(4)],
vec![Scalar::i64(3), Scalar::i64(4)],
],
)
.expect("valid explicit point set");
let expected = vec![
vec![Cell::from_i64(3), Cell::from_i64(5)],
vec![Cell::from_i64(2), Cell::from_i64(4)],
vec![Cell::from_i64(3), Cell::from_i64(4)],
];
assert_eq!(ls.points(), expected);
let family = sweep(&ls, tagged_report);
assert_eq!(family.points.len(), 3);
assert_eq!(family.space, space, "family carries the validated param-space");
for (pt, cells) in family.points.iter().zip(&expected) {
assert_eq!(&pt.params, cells, "points carried tag-free, in input order");
let tag: String =
cells.iter().map(|c| c.i64().to_string()).collect::<Vec<_>>().join(",");
assert_eq!(pt.report.manifest.commit, tag, "the closure ran on exactly this point");
}
}
#[test]
fn list_space_refuses_arity_and_kind_mismatch() {
let space = i64_space(2);
// arity: the second point carries 1 value for a 2-slot space
let err = ListSpace::new(
&space,
vec![vec![Scalar::i64(1), Scalar::i64(2)], vec![Scalar::i64(3)]],
)
.unwrap_err();
assert_eq!(err, SweepError::Arity { expected: 2, got: 1 });
// kind: point 1, slot 1 carries an F64 in an I64 slot
let err = ListSpace::new(
&space,
vec![
vec![Scalar::i64(1), Scalar::i64(2)],
vec![Scalar::i64(3), Scalar::f64(0.5)],
],
)
.unwrap_err();
assert_eq!(
err,
SweepError::KindMismatch {
slot: 1,
value_index: 1,
expected: ScalarKind::I64,
got: ScalarKind::F64,
},
);
}
#[test]
fn list_space_allows_empty_point_list() {
let space = i64_space(2);
let ls = ListSpace::new(&space, vec![]).expect("an empty point list is valid");
assert!(ls.points().is_empty(), "zero points enumerated");
let family = sweep(&ls, tagged_report);
assert!(family.points.is_empty(), "an empty ListSpace sweeps to an empty family");
assert_eq!(family.space, space, "the empty family still carries the param-space");
}
}