plan: 0105 std-vocabulary roster macro

Two tasks: (1) the private std_vocabulary_roster! macro + single 22-key
invocation replacing the two hand-written fns in aura-std/vocabulary.rs
(byte-identical signatures/keys/order; docs generated with the fns), plus
the two unit tests reshaped to iterate the generated list instead of
carrying an inline roster copy (oracle: PrimitiveBuilder::label(); count
pin kept as deliberate friction); (2) full regression — suite green
UNCHANGED at 884 (no test added or removed).

Recon-verified isolation: lib.rs re-export untouched (private in-module
macro, same pub fns), no other roster copy anywhere in the workspace, no
doc/ledger reference stales, first macro_rules in the crate, no
missing_docs gate.

refs #180, refs #160
This commit is contained in:
2026-07-02 21:40:47 +02:00
parent 103a82dc2e
commit e0c745a9b0
@@ -0,0 +1,221 @@
# Std-vocabulary roster macro — Implementation Plan
> **Parent spec:** `docs/specs/0105-std-vocabulary-roster-macro.md`
>
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to
> run this plan. Steps use `- [ ]` checkboxes for tracking.
**Goal:** Collapse the three hand-kept copies of the std-vocabulary roster
(match arms, `std_vocabulary_types()` list, test inline list) into one
declarative-macro source — byte-preserving (same signatures, same 22 ids,
same order), every consumer untouched.
**Architecture:** One private `macro_rules! std_vocabulary_roster` in
`crates/aura-std/src/vocabulary.rs`, invoked once with the
`"TypeId" => Type` pairs, expanding into both public fns (docs included).
The two unit tests stop carrying a roster copy: the round-trip test
iterates the generated list (oracle: `PrimitiveBuilder::label()`), the
shape test keeps the count pin and non-member checks.
**Tech Stack:** aura-std only (`vocabulary.rs`). `lib.rs`'s re-export
(`crates/aura-std/src/lib.rs:86`) resolves identically — NO edit there. No
new dependencies; the macro is the crate's first `macro_rules!`
(collision-free, recon-verified); no `missing_docs` gate fires (none is
configured, and the macro supplies `///` on both fns anyway).
**Files this plan creates or modifies:**
- Modify: `crates/aura-std/src/vocabulary.rs:27-72` — the two hand-written
fns become the macro + single roster invocation.
- Modify: `crates/aura-std/src/vocabulary.rs:74-151` — the two tests
reshaped (no inline roster copy).
Everything else is untouched by design: `crates/aura-std/src/lib.rs`, all
consumer call sites (aura-cli `project.rs`/`main.rs`/`graph_construct.rs`,
aura-engine loader + e2e), and every existing pin
(`graph_introspect_vocabulary_lists_the_node_types`,
`unknown_node_type_fails_named`, the `tests/project_load.rs` suite,
`signal_serializes_to_canonical_golden`).
---
### Task 1: The roster macro and the reshaped tests
**Files:**
- Modify: `crates/aura-std/src/vocabulary.rs:27-72` (fns → macro)
- Modify: `crates/aura-std/src/vocabulary.rs:74-151` (tests)
Note for the reviewers: this is a **behaviour-preserving refactor** — there
is no new RED test by design; the oracle is the existing suite staying
green unchanged (the two reshaped tests plus the untouched consumer pins).
The module doc (`:1-18`) and the `use crate::{...}` import block
(`:20-25`) stay byte-identical.
- [ ] **Step 1: Replace the two hand-written fns with the macro + invocation**
In `crates/aura-std/src/vocabulary.rs`, replace everything from the line
```rust
/// Resolve a serialized node type identity (the `PrimitiveBuilder::label`, e.g.
```
(line 27, the doc comment above `std_vocabulary`) through the closing brace
of `std_vocabulary_types` (line 72, inclusive — i.e. both fns and their doc
comments) with:
```rust
/// 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 bump in the tests). 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,
}
```
(The roster preserves the current 22 keys and their exact current order —
byte-identical `std_vocabulary_types()` output.)
- [ ] **Step 2: Reshape the two tests**
In the same file, replace the whole `#[cfg(test)] mod tests { ... }` block
(currently lines 74-151) with:
```rust
#[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);
}
}
```
- [ ] **Step 3: Run the vocabulary unit tests**
Run: `cargo test -p aura-std std_vocabulary`
Expected: PASS — 2 passed
(`std_vocabulary_resolves_known_and_rejects_unknown`,
`std_vocabulary_types_lists_exactly_the_resolvable_keys`), 0 failed.
- [ ] **Step 4: Verify the consumers see identical behaviour**
Run: `cargo test -p aura-cli --test graph_construct`
Expected: PASS — 20 passed, 0 failed (in particular
`graph_introspect_vocabulary_lists_the_node_types` green unchanged).
---
### Task 2: Full regression
**Files:** none (verification only)
- [ ] **Step 1: Build**
Run: `cargo build --workspace`
Expected: clean, 0 errors.
- [ ] **Step 2: Full suite**
Run: `cargo test --workspace`
Expected: 0 failed across all targets; total pass count unchanged vs the
cycle-0104 close (884 passed — this cycle adds and removes no test, it only
reshapes the two vocabulary tests in place).
- [ ] **Step 3: Lint**
Run: `cargo clippy --workspace --all-targets -- -D warnings`
Expected: clean, 0 warnings.
- [ ] **Step 4: Doc build**
Run: `cargo doc --workspace --no-deps 2>&1`
Expected: finishes with 0 warnings (the macro-generated fns carry their
`///` docs).