bd32c4fcb3
Architect drift review (range 7b429f9..57f401f): drift_found (low/medium only), resolved inline as close-fixes: - crates/aura-std/src/vocabulary.rs — the roster-site doc now names BOTH count pins a node addition trips (the in-crate shape test and aura-cli's cross-boundary --vocabulary count e2e); the singular phrasing understated the cross-crate lockstep the extra e2e introduced. - docs/design/INDEX.md — the C24 enforcement-shift passage gains the 0105 delivery note: resolver-vs-list drift closed by construction via the roster macro; the un-rostered-node residual stays fail-safe. Noted, no action: spec/plan predicted 884 (no new test) but delivery added one e2e (885) — consciously superseded, disclosed in the iter commit body; the artifacts are ephemeral and removed below. What holds (architect): byte-preserving refactor confirmed against the diff (22 keys/order exact, signatures and module doc untouched, every consumer unchanged); invariant 9 / C24 preserved (compile-time expansion, no registry); the new e2e count pin is genuinely roster-tied. Regression gates (all green): cargo build clean; cargo test --workspace 885 passed / 0 failed (884 baseline + 1 new e2e); clippy --all-targets -D warnings clean; cargo doc --no-deps 0 warnings. Ephemeral cycle artifacts removed per project convention (git rm): docs/specs/0105-std-vocabulary-roster-macro.md, docs/plans/0105-std-vocabulary-roster-macro.md. refs #180
130 lines
5.9 KiB
Rust
130 lines
5.9 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;
|
|
|
|
/// 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 `aura-std`
|
|
/// 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! {
|
|
"Add" => Add,
|
|
"And" => And,
|
|
"Bias" => Bias,
|
|
"CarryCost" => CarryCost,
|
|
"ConstantCost" => ConstantCost,
|
|
"Delay" => Delay,
|
|
"EMA" => Ema,
|
|
"EqConst" => EqConst,
|
|
"FixedStop" => FixedStop,
|
|
"Gt" => Gt,
|
|
"Latch" => Latch,
|
|
"LongOnly" => LongOnly,
|
|
"Mul" => Mul,
|
|
"PositionManagement" => PositionManagement,
|
|
"Resample" => Resample,
|
|
"RollingMax" => RollingMax,
|
|
"RollingMin" => RollingMin,
|
|
"Sizer" => Sizer,
|
|
"SMA" => Sma,
|
|
"Sqrt" => Sqrt,
|
|
"Sub" => Sub,
|
|
"VolSlippageCost" => VolSlippageCost,
|
|
}
|
|
|
|
#[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; construction-arg nodes and sinks stay absent so the
|
|
/// loader fails cleanly rather than guessing.
|
|
#[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, 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());
|
|
}
|
|
|
|
/// 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`).
|
|
#[test]
|
|
fn std_vocabulary_types_lists_exactly_the_resolvable_keys() {
|
|
// known non-members stay out of the list
|
|
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 roster at exactly 22 entries
|
|
assert_eq!(std_vocabulary_types().len(), 22);
|
|
}
|
|
}
|