audit: cycle 0105 tidy (drift-clean after two doc-mirror alignments)
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
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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).
|
||||
@@ -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<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 `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<PrimitiveBuilder>
|
||||
└─ 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.
|
||||
Reference in New Issue
Block a user