diff --git a/crates/aura-std/src/vocabulary.rs b/crates/aura-std/src/vocabulary.rs index a9be21d..6ea06ec 100644 --- a/crates/aura-std/src/vocabulary.rs +++ b/crates/aura-std/src/vocabulary.rs @@ -28,7 +28,8 @@ use aura_core::PrimitiveBuilder; /// 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 +/// 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). diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index b31e95c..d2e6ab4 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -2053,7 +2053,12 @@ logic — C17), and `std_vocabulary` stays a compiled-in dispatch table, not a r its enforcement **degrades from compiler-guaranteed to resolver-seam discipline**. Two consequences: (1) **#160** (the guard that the closed vocabulary stays honest) is no longer droppable hygiene — it guards the data-plane face of the boundary (its own drift direction -still fails *safe*, `UnknownNodeType`). (2) The **injected per-project resolver** (C16) is +still fails *safe*, `UnknownNodeType`). **Delivered cycle 0105:** the +`std_vocabulary_roster!` macro expands the resolver match and the enumerable +`std_vocabulary_types()` list from one roster, closing the resolver-vs-list drift mode by +construction; the residual (a new zero-arg node never rostered at all) stays fail-safe, +guarded by the one-line maintainer surface plus the count pins (the in-crate shape test and +aura-cli's cross-boundary `--vocabulary` count e2e). (2) The **injected per-project resolver** (C16) is the genuinely new, *deferred* invariant-9 surface: nothing structurally stops a project resolver from accepting arbitrary type-ids, or a project from layering a blueprint *marketplace* (store + type-discovery API) **on top of** the format — one layer *above* the diff --git a/docs/plans/0105-std-vocabulary-roster-macro.md b/docs/plans/0105-std-vocabulary-roster-macro.md deleted file mode 100644 index dd6737d..0000000 --- a/docs/plans/0105-std-vocabulary-roster-macro.md +++ /dev/null @@ -1,221 +0,0 @@ -# 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 { - 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). diff --git a/docs/specs/0105-std-vocabulary-roster-macro.md b/docs/specs/0105-std-vocabulary-roster-macro.md deleted file mode 100644 index a083c5f..0000000 --- a/docs/specs/0105-std-vocabulary-roster-macro.md +++ /dev/null @@ -1,201 +0,0 @@ -# The std-vocabulary roster macro — Design Spec - -**Date:** 2026-07-02 -**Status:** Draft — awaiting sign-off (boss run: grounding-check gate) -**Authors:** orchestrator + Claude -**Cycle:** 0105 — milestone "Project environment — the project-as-crate -authoring loop" (#180); work item #160 (fork resolutions: the cycle-0105 -reconciliation comment on #160) - -## Goal - -Collapse the three hand-kept copies of the std-vocabulary roster — the -`std_vocabulary` match arms, the `std_vocabulary_types()` list, and the -test's inline 22-key list (`crates/aura-std/src/vocabulary.rs`) — into -**one declarative-macro source**, so the resolver and the enumerable list -cannot drift apart (the #160 failure mode: a node resolvable but silently -absent from `aura graph introspect --vocabulary`, or vice versa). -Behaviour is **byte-preserving**: both fn signatures, the 22 type ids, and -their order are unchanged; every consumer (loader resolvers, build-free -introspection, the 0102 merged-vocabulary charter checks) is untouched. -The vocabulary stays a **closed, compiled-in set** (domain invariant 9 / -C24) — a `macro_rules!` expansion is compile-time; no registry, no -distributed-slice machinery, no build script. - -## Architecture - -One new private `macro_rules! std_vocabulary_roster` in -`crates/aura-std/src/vocabulary.rs`, invoked exactly once with the -`"TypeId" => Type` pairs (current 22, current order). The expansion -produces both public fns with their doc comments. The unit tests stop -carrying their own roster copy: the round-trip test iterates the generated -`std_vocabulary_types()` (the independent oracle remains -`PrimitiveBuilder::label()` — a typo'd roster key still fails the -lookup-label agreement, so the test is not self-referential), and the -shape test keeps the count pin (`== 22`, deliberate friction: any roster -change forces a conscious test update) plus the non-member checks. - -**Accepted residual (documented at the roster site, per the #160 -reconciliation):** a new zero-arg node never added to the roster at all -remains unguarded — no enumeration of zero-arg builders exists to test -against — and fails safe in both directions (clean -`LoadError::UnknownNodeType` on load; merely absent from `--vocabulary`). -The practical guard is the shrunk maintainer surface: adding a node to the -vocabulary is exactly one roster line instead of three coordinated edits. - -## Concrete code shapes - -### The maintainer-facing surface (the acceptance-criterion evidence) - -Adding a future zero-arg node `Foo` to the vocabulary is one line in one -place — both surfaces (resolver + enumerable list) gain it atomically: - -```rust -std_vocabulary_roster! { - "Add" => Add, - "And" => And, - // ... - "Foo" => Foo, // <- the whole vocabulary edit (#160) - // ... - "VolSlippageCost" => VolSlippageCost, -} -``` - -(plus the count-pin bump `22 -> 23` in the shape test — the deliberate -tripwire that makes the roster change a conscious act). - -### The macro (secondary, before → after) - -Before: two hand-written fns whose bodies each carry the full roster -(`vocabulary.rs:30-56` match, `:65-72` list), kept in lockstep "by -maintainer discipline" per the current `std_vocabulary_types` doc comment. - -After — the two fns are generated from one roster: - -```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. 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 { - 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 `use crate::{...}` import list and the module doc (`:1-18`) stay; the -module doc's scope note (construction-arg builders and sinks deliberately -absent) continues to hold — the macro changes how the roster is *written*, -not what is in it. - -### The tests (secondary, before → after) - -`std_vocabulary_resolves_known_and_rejects_unknown` drops its inline -22-key array and iterates `super::std_vocabulary_types()`; the label -round-trip assertion and the three negative probes (`"nope"`, -`"LinComb"`, `"Recorder"`) are unchanged. -`std_vocabulary_types_lists_exactly_the_resolvable_keys` keeps the count -pin (`assert_eq!(std_vocabulary_types().len(), 22)`) and the non-member -checks, and drops its per-key round-trip loop (now the first test's job -over the same generated list); its comment records that list ↔ resolver -agreement holds by construction since the roster macro. - -## Components - -| Component | Crate | New/changed | -|---|---|---| -| `std_vocabulary_roster!` macro + single invocation | aura-std (`vocabulary.rs`) | new (private) | -| `std_vocabulary` / `std_vocabulary_types` | aura-std | regenerated, signatures + bytes-out unchanged | -| The two unit tests | aura-std | reshaped (no inline roster copy) | - -Untouched by design: every consumer — `blueprint_from_json` resolver -injection sites, `aura graph introspect --vocabulary` (via -`project::Env::type_ids`), the 0102 project-vocabulary merge and charter -checks, all e2e tests. - -## Data flow - -``` -std_vocabulary_roster! { "TypeId" => Type, ... } (ONE source) - ├─ expands ─► pub fn std_vocabulary(&str) -> Option - └─ expands ─► pub fn std_vocabulary_types() -> &'static [&'static str] -``` - -## Error handling - -No new failure modes: the macro is a compile-time expansion; a malformed -roster line is a compile error. Runtime behaviour (Some/None, the list) -is byte-identical to today. - -## Testing strategy - -1. **Unit (aura-std, reshaped as above):** label round-trip over the - generated list + negative probes; count pin `== 22` + non-member - checks. -2. **Regression:** full workspace suite green **unchanged** — no test - count change beyond the reshapes (no new tests, none removed); in - particular the consumers' pins stay green: - `graph_introspect_vocabulary_lists_the_node_types` (e2e), - `unknown_node_type_fails_named` (engine loader), the - `tests/project_load.rs` merged-vocabulary suite (aura-cli), and the - canonical golden. - -## Acceptance criteria - -1. Adding a zero-arg node to the vocabulary is exactly one roster line - (plus the conscious count-pin bump) — shown above; the three-copy - lockstep is gone from the file. -2. `std_vocabulary` and `std_vocabulary_types` are byte-identical in - behaviour: same signatures, same 22 ids, same order; the full suite is - green unchanged. -3. The vocabulary remains a closed, compiled-in set (invariant 9 / C24): - no registry, no build script, no runtime registration. -4. The accepted residual (an un-rostered new node, fails safe) is - documented at the roster site — the deferral recorded on #160.