feat(0087): blueprint serialization & loader — close the graph-as-data loop
C24 first cut (#155): a blueprint — the param-generic, named graph-as-data a Rust builder produces — is now a first-class serializable data value with a canonical, versioned format, and the engine has the missing load path (data -> blueprint -> FlatGraph), not only the Rust-builder construction path. What ships: - `blueprint_to_json` / `blueprint_from_json` in a new `aura-engine` blueprint_serde module: a faithful serde DTO projection (the build closures dropped; re-derived on load), top-level `format_version` envelope, canonical compact JSON (struct field order, defaults omitted via skip_serializing_if). - A resolver seam: the loader is generic over an injected `Fn(&str) -> Option<PrimitiveBuilder>`; the concrete closed `match` over the aura-std vocabulary lives in aura-std (`std_vocabulary`), never in the engine (the engine stays domain-free: lib deps aura-core + aura-analysis only, never the node-vocabulary crate). This is C24's compiled-in closed-set referenced by type identity, NOT a dynamic/marketplace node registry (domain invariant 9). - serde derives on the serialized plain types (Edge, Target, OutField, Role, BoundParam, ScalarKind); BoundParam additionally re-exported from aura-core's root (an additive fix the plan's file list missed). Load-bearing scope decisions (derived, logged on #155): - Sinks are excluded from the serialized blueprint — a recording sink captures an mpsc::Sender (runtime identity, not param/topology, C19 value-empty); sink addressing is the C18-registry / #101 concern. The round-trip test attaches identical sinks post-load to both twins. - Construction-arg builders (LinComb(arity), CostSum(n), SimBroker(pip), Session(..)) are out of the round-trippable set: their structural args are a C20 structural-axis concern the param-generic format does not yet encode; an absent type_id fails the load cleanly (UnknownNodeType), never a silent wrong graph. #155's vocabulary is the 22 zero-arg aura-std builders. Additively extensible later (#156). Orchestrator hardening on review: the serializer now emits bound params in ascending original-slot order (mirroring the loader), so the canonical form is bind-order-independent — a precondition for content-addressing (#158). Identity today (every #155 node has <=1 param); holds by construction for future multi-param nodes. Acceptance (all green): a serialized blueprint runs bit-identical (C1) to its Rust-built twin; serializer/loader are inverse (canonical idempotence, incl. a recursive nested composite); unknown-type and unsupported-version fail named, never panicking. cargo test --workspace 748 passed; clippy --all-targets -D warnings clean. closes #155
This commit is contained in:
@@ -46,6 +46,7 @@ mod sma;
|
||||
mod sqrt;
|
||||
mod stop_rule;
|
||||
mod sub;
|
||||
mod vocabulary;
|
||||
mod vol_slippage_cost;
|
||||
pub use add::Add;
|
||||
pub use and::And;
|
||||
@@ -82,4 +83,5 @@ pub use sma::Sma;
|
||||
pub use sqrt::Sqrt;
|
||||
pub use stop_rule::FixedStop;
|
||||
pub use sub::Sub;
|
||||
pub use vocabulary::std_vocabulary;
|
||||
pub use vol_slippage_cost::VolSlippageCost;
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//! The closed, compiled-in dispatch from a serialized node **type identity** to
|
||||
//! its `aura-std` builder factory — the load-side counterpart to the type label
|
||||
//! a blueprint serializes (`PrimitiveBuilder::label`). This is **not** a dynamic
|
||||
//! registry (domain invariant 9): it is a `match` the crate that *owns* the
|
||||
//! nodes writes, over its own closed, compiled-in vocabulary (C24 — nodes are
|
||||
//! referenced "by compiled-in type identity", never a by-name marketplace). The
|
||||
//! engine cannot hold this table (it does not depend on `aura-std`); a loader
|
||||
//! takes it as an injected resolver, and a project `cdylib` later supplies its
|
||||
//! own (C16) through the same seam.
|
||||
//!
|
||||
//! Scope (#155): only the **zero-argument** `Type::builder()` factories are
|
||||
//! resolvable. Builders that take structural construction arguments
|
||||
//! (`LinComb(arity)`, `CostSum(n_costs)`, `SimBroker(pip_size)`, `Session(..)`)
|
||||
//! and the recording sinks (`Recorder`/`GatedRecorder`/`SeriesReducer`, which
|
||||
//! capture an `mpsc::Sender`) are deliberately absent — an absent `type_id`
|
||||
//! yields `None`, so the loader fails cleanly (`LoadError::UnknownNodeType`)
|
||||
//! rather than guessing. Serialising structural-axis construction args is a
|
||||
//! later, additive extension (#156/C20).
|
||||
|
||||
use crate::{
|
||||
Add, And, Bias, CarryCost, ConstantCost, Delay, Ema, EqConst, FixedStop, Gt, Latch, LongOnly,
|
||||
Mul, PositionManagement, Resample, RollingMax, RollingMin, Sizer, Sma, Sqrt, Sub,
|
||||
VolSlippageCost,
|
||||
};
|
||||
use aura_core::PrimitiveBuilder;
|
||||
|
||||
/// Resolve a serialized node type identity (the `PrimitiveBuilder::label`, e.g.
|
||||
/// `"SMA"`) to a fresh, unbound builder from the node's own factory. Returns
|
||||
/// `None` for any identity outside the zero-arg `aura-std` vocabulary.
|
||||
pub fn std_vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
|
||||
Some(match type_id {
|
||||
"Add" => Add::builder(),
|
||||
"And" => And::builder(),
|
||||
"Bias" => Bias::builder(),
|
||||
"CarryCost" => CarryCost::builder(),
|
||||
"ConstantCost" => ConstantCost::builder(),
|
||||
"Delay" => Delay::builder(),
|
||||
"EMA" => Ema::builder(),
|
||||
"EqConst" => EqConst::builder(),
|
||||
"FixedStop" => FixedStop::builder(),
|
||||
"Gt" => Gt::builder(),
|
||||
"Latch" => Latch::builder(),
|
||||
"LongOnly" => LongOnly::builder(),
|
||||
"Mul" => Mul::builder(),
|
||||
"PositionManagement" => PositionManagement::builder(),
|
||||
"Resample" => Resample::builder(),
|
||||
"RollingMax" => RollingMax::builder(),
|
||||
"RollingMin" => RollingMin::builder(),
|
||||
"Sizer" => Sizer::builder(),
|
||||
"SMA" => Sma::builder(),
|
||||
"Sqrt" => Sqrt::builder(),
|
||||
"Sub" => Sub::builder(),
|
||||
"VolSlippageCost" => VolSlippageCost::builder(),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::std_vocabulary;
|
||||
|
||||
/// Every key in the closed vocabulary resolves to a builder that carries
|
||||
/// that exact type label back, and only the zero-arg keys do. The label
|
||||
/// round-trip is the property: a typo in any match key (the silent-
|
||||
/// unresolvable failure this resolver guards against) breaks it, as does a
|
||||
/// key mapped to the wrong builder; construction-arg nodes and sinks stay
|
||||
/// absent so the loader fails cleanly rather than guessing.
|
||||
#[test]
|
||||
fn std_vocabulary_resolves_known_and_rejects_unknown() {
|
||||
// every zero-arg key round-trips: lookup -> builder -> same type label.
|
||||
// (this list is the resolver's whole vocabulary; a missing/typo'd arm
|
||||
// fails the lookup, a mis-mapped arm fails the label assertion.)
|
||||
for type_id in [
|
||||
"Add",
|
||||
"And",
|
||||
"Bias",
|
||||
"CarryCost",
|
||||
"ConstantCost",
|
||||
"Delay",
|
||||
"EMA",
|
||||
"EqConst",
|
||||
"FixedStop",
|
||||
"Gt",
|
||||
"Latch",
|
||||
"LongOnly",
|
||||
"Mul",
|
||||
"PositionManagement",
|
||||
"Resample",
|
||||
"RollingMax",
|
||||
"RollingMin",
|
||||
"Sizer",
|
||||
"SMA",
|
||||
"Sqrt",
|
||||
"Sub",
|
||||
"VolSlippageCost",
|
||||
] {
|
||||
let builder =
|
||||
std_vocabulary(type_id).unwrap_or_else(|| panic!("{type_id} is in the vocabulary"));
|
||||
assert_eq!(
|
||||
builder.label(),
|
||||
type_id,
|
||||
"resolver key must round-trip to the same type label"
|
||||
);
|
||||
}
|
||||
// an unknown id, a construction-arg node, and a sink are all absent
|
||||
assert!(std_vocabulary("nope").is_none());
|
||||
assert!(std_vocabulary("LinComb").is_none());
|
||||
assert!(std_vocabulary("Recorder").is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user