Files
Aura/crates/aura-std/src/vocabulary.rs
T
Brummel 27ac4dc537 feat(0088): §B construction op-script surface — GraphSession/Op/replay + introspection
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
2026-06-29 20:28:41 +02:00

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);
}
}