Files
Aura/crates/aura-vocabulary/src/lib.rs
T
claude a8b1ba45c5 feat(aura-engine, aura-vocabulary, aura-cli): the args channel end to end
The data plane reaches non-scalar structural config (closes #271, the
final Data-authorability-boundary item). Op::Add gains args
(string pairs, applied via try_args after resolve and before the bind
loop — no pending builder ever enters a graph; an arg-bearing type
without args refuses as MissingArg). OpError::BadArg carries the
precise ArgOpError; the CLI renders it as prose naming the arg, its
kind, and the kind's closed hint (exit 1, #175 content-fault class).

Serde: PrimitiveData.args (skip-if-empty — args-free documents stay
byte-identical, golden-pinned, so every existing content id is stable
per C18) with a data-driven format version: the writer emits 1 for
args-free documents and 2 when any primitive recursively carries args;
the loader accepts 1..=2. The old v2-refusal pin flips deliberately to
v3 (named contract change). Identity JSON keeps args — they are
structural, and the Rust configured() path and the args op-script
bridge to the same identity id (pinned).

Roster: Session / LinComb / CostSum enter (33 -> 36, both count pins;
the LinComb-rejection assertion flips to the resolved side). introspect
--node shows arg rows (name, kind, hint) plus the pending note;
OP_REFERENCE's add row teaches the args form. Registry comment
de-staled (LinComb is now roster-reachable).

Sequencing note: this commit lands op seam and roster together so no
intermediate tree state exposes a pending builder.
2026-07-24 21:43:02 +02:00

156 lines
6.9 KiB
Rust

//! The closed, compiled-in dispatch from a serialized node **type identity** to
//! its node-crate 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 the node crates); a loader
//! takes it as an injected resolver, and a project `cdylib` later supplies its
//! own (C16) through the same seam.
//!
//! Scope (#155, extended #271): the roster resolves any type whose factory is
//! `Type::builder()` with zero RUST-side arguments — this now includes
//! arg-bearing types (`Session`, `LinComb`, `CostSum`), whose `builder()` is
//! itself zero-arg and returns a *pending* `PrimitiveBuilder`, configured
//! through the typed `args` channel (`try_args`) at the `add` op, before any
//! bind. `SimBroker(pip_size)` stays out of scope: it is a plain scalar Rust
//! parameter, not a structural construction arg — a different (unaddressed)
//! gap. The recording sinks (`Recorder`/`GatedRecorder`/`SeriesReducer`, which
//! capture an `mpsc::Sender`) stay deliberately absent — an absent `type_id`
//! yields `None`, so the loader fails cleanly (`LoadError::UnknownNodeType`)
//! rather than guessing.
use aura_backtest::PositionManagement;
use aura_market::{Resample, Session, SessionFrankfurt};
use aura_std::{
Abs, Add, And, Const, CumSum, Delay, Div, Ema, EqConst, Gt, Latch, LinComb, Max, Min, Mul,
RollingMax, RollingMin, Scale, Select, Sign, Sma, Sqrt, Sub, When,
};
use aura_strategy::{
Bias, CarryCost, ConstantCost, CostSum, FixedStop, LongOnly, Sizer, VolSlippageCost,
};
use aura_core::PrimitiveBuilder;
/// The single roster source (#160): one `"TypeId" => Type` line per zero-arg
/// node, expanded into BOTH the `std_vocabulary` match and the
/// `std_vocabulary_types` list — the two surfaces cannot drift apart, and
/// adding a zero-arg node to the vocabulary is exactly one line here (plus the
/// conscious count-pin bumps: the in-crate shape test and aura-cli's
/// cross-boundary `--vocabulary` count e2e). What this cannot guard: a new
/// zero-arg node never rostered at all — that fails safe (clean
/// `LoadError::UnknownNodeType` on load, merely absent from `--vocabulary`;
/// #160).
macro_rules! std_vocabulary_roster {
($($type_id:literal => $ty:ty),+ $(,)?) => {
/// 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 node
/// vocabulary.
pub fn std_vocabulary(type_id: &str) -> Option<PrimitiveBuilder> {
Some(match type_id {
$($type_id => <$ty>::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. Generated from the same roster as the match arms
/// (#160) — the two surfaces agree by construction.
pub fn std_vocabulary_types() -> &'static [&'static str] {
&[$($type_id),+]
}
};
}
std_vocabulary_roster! {
"Abs" => Abs,
"Add" => Add,
"And" => And,
"Bias" => Bias,
"CarryCost" => CarryCost,
"Const" => Const,
"ConstantCost" => ConstantCost,
"CostSum" => CostSum,
"CumSum" => CumSum,
"Delay" => Delay,
"Div" => Div,
"EMA" => Ema,
"EqConst" => EqConst,
"FixedStop" => FixedStop,
"Gt" => Gt,
"Latch" => Latch,
"LinComb" => LinComb,
"LongOnly" => LongOnly,
"Max" => Max,
"Min" => Min,
"Mul" => Mul,
"PositionManagement" => PositionManagement,
"Resample" => Resample,
"RollingMax" => RollingMax,
"RollingMin" => RollingMin,
"Scale" => Scale,
"Select" => Select,
"Session" => Session,
"SessionFrankfurt" => SessionFrankfurt,
"Sign" => Sign,
"Sizer" => Sizer,
"SMA" => Sma,
"Sqrt" => Sqrt,
"Sub" => Sub,
"VolSlippageCost" => VolSlippageCost,
"When" => When,
}
#[cfg(test)]
mod tests {
use super::{std_vocabulary, std_vocabulary_types};
/// Every rostered key resolves to a builder that carries that exact type
/// label back, and only the rostered keys do. The builder's own `label()`
/// is the independent oracle: a typo'd roster key fails the lookup-label
/// agreement even though the match arms and the list share the same
/// (mistyped) literal; an unresolvable id and a sink stay absent so the
/// loader fails cleanly rather than guessing. `LinComb` — an arg-bearing
/// type, #271 — now resolves to a PENDING builder (its own `.label()`
/// still round-trips; it just is not yet constructible without
/// `try_args`).
#[test]
fn std_vocabulary_resolves_known_and_rejects_unknown() {
for &type_id in std_vocabulary_types() {
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 and a sink are absent; LinComb is now resolved (#271)
assert!(std_vocabulary("nope").is_none());
assert!(std_vocabulary("LinComb").is_some());
assert!(std_vocabulary("Recorder").is_none());
}
/// List <-> resolver agreement holds BY CONSTRUCTION since the #160 roster
/// macro (one source expands into both surfaces); what stays pinned here is
/// the roster's SHAPE. The count pin is deliberate friction: any roster
/// line added or dropped trips it, making a vocabulary change a conscious
/// act. The residual #160 drift — a new zero-arg node never rostered at
/// all — is not catchable here (no enumeration of zero-arg builders
/// exists); it fails safe (clean `UnknownNodeType` on load, merely absent
/// from `--vocabulary`). #271: `Session`/`LinComb`/`CostSum` moved IN
/// (arg-bearing types now have zero-arg `builder()` factories); the count
/// pin moves 33 -> 36.
#[test]
fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
// known non-members stay out of the list
assert!(std_vocabulary_types().contains(&"LinComb")); // now arg-bearing-reachable (#271)
assert!(!std_vocabulary_types().contains(&"Recorder")); // sink
assert!(!std_vocabulary_types().contains(&"nope"));
// count guard: pins the roster at exactly 36 entries
assert_eq!(std_vocabulary_types().len(), 36);
}
}