27ac4dc537
Iteration-1 §B of the construction service (#157), on the §A shared predicates
(ea1ca32): a declarative, replayable by-identifier op-script that builds a
runnable blueprint through validated ops, reusing the engine's existing gates at
two cadences — no second validator (C24).
- construction.rs: `Op` (Source/Input/Add/Feed/Connect/Expose, 1:1 with the
document, dotted by-identifier ports), `OpError` (by-identifier causes),
`GraphSession` (per-op-fallible accumulator) and the free `replay` driver
(stop-at-first-failing-op, naming it by `(op_index, OpError)`).
- Eager (per-op): name resolution + edge_kind_check + the >1-cover
DoubleWiredPort arm + try_bind — all calling the §A shared predicates.
- Holistic (finish): the 0-cover totality arm, param-namespace injectivity, and
root-role boundness — the SAME unchanged engine gates
(check_param_namespace_injective / validate_wiring), now at end-of-document.
- Build-free introspection: `unwired()` (a partial document's still-open slots).
- aura-std std_vocabulary_types(): the enumerable companion to the closed
std_vocabulary match (a `fn(&str)->Option<…>` resolver cannot be listed),
lockstep-tested in the list->resolver direction; the reverse fails safe (#160).
- aura-engine exports replay/GraphSession/Op/OpError + re-exports BindOpError.
Acceptance (engine level): a sink-free price->fast/slow SMA->Sub->Bias signal
root built through the ops compiles to a FlatGraph byte-identical (edges +
sources) to its GraphBuilder twin (C1); an invalid op is rejected at the op,
named; vocabulary + unwired slots answer without a build. Pinned by
construction.rs unit tests + a construction_e2e.rs integration test over the
public seam the iteration-2 CLI will consume.
Notable judgement calls folded in from the implement loop: feed is all-or-nothing
(two-phase resolve-then-commit, no partial wiring on a mid-fan-out fault); the
node identifier is stamped onto the builder (`named(&id)`) so two same-type nodes
get distinct param paths and do not trip the injectivity gate prematurely; a
module-level `#![allow(dead_code)]` covers the public surface until the
iteration-2 CLI consumes it. Full suite green; clippy --all-targets -D warnings
clean.
Iteration 2 (the JSON op-list encoding + `aura graph build`/`introspect` CLI)
remains; #157 stays open. refs #157
152 lines
6.7 KiB
Rust
152 lines
6.7 KiB
Rust
//! 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,
|
|
})
|
|
}
|
|
|
|
/// The enumerable companion to [`std_vocabulary`]: the closed set of zero-arg
|
|
/// type identities the resolver answers. A `fn(&str)->Option<…>` resolver cannot
|
|
/// be listed, so build-free vocabulary introspection (#157) reads this. Kept in
|
|
/// lockstep with the `std_vocabulary` match arms by maintainer discipline — a
|
|
/// type added there must be added here. The reverse (an arm with no entry here)
|
|
/// is not test-enforced and cannot be, from a `fn` resolver; it fails safe (the
|
|
/// type stays load-resolvable, only unlisted; #160).
|
|
pub fn std_vocabulary_types() -> &'static [&'static str] {
|
|
&[
|
|
"Add", "And", "Bias", "CarryCost", "ConstantCost", "Delay", "EMA",
|
|
"EqConst", "FixedStop", "Gt", "Latch", "LongOnly", "Mul",
|
|
"PositionManagement", "Resample", "RollingMax", "RollingMin", "Sizer",
|
|
"SMA", "Sqrt", "Sub", "VolSlippageCost",
|
|
]
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
#[test]
|
|
fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
|
|
use super::std_vocabulary_types;
|
|
// every listed type resolves to a builder carrying that exact label
|
|
for &type_id in std_vocabulary_types() {
|
|
let builder = std_vocabulary(type_id)
|
|
.unwrap_or_else(|| panic!("{type_id} listed but unresolvable"));
|
|
assert_eq!(
|
|
builder.label(),
|
|
type_id,
|
|
"listed type must round-trip to its label"
|
|
);
|
|
}
|
|
// the list is the WHOLE resolvable set: a couple of known non-members stay out
|
|
assert!(!std_vocabulary_types().contains(&"LinComb")); // construction-arg node
|
|
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
|
|
assert!(!std_vocabulary_types().contains(&"nope"));
|
|
// count guard: pins the list at exactly 22 entries, so a dropped or added
|
|
// list entry trips this; with the round-trip loop above, the list is
|
|
// exactly the 22 listed resolvable keys. The reverse drift — a resolver
|
|
// arm with no list entry — is not catchable from a `fn(&str)` resolver
|
|
// (not enumerable); it fails safe (load-resolvable, only unlisted; #160).
|
|
assert_eq!(std_vocabulary_types().len(), 22);
|
|
}
|
|
}
|