Guard std_vocabulary against silently dropping a new zero-arg node #160
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The closed
matchincrates/aura-std/src/vocabulary.rs(std_vocabulary)maps the 22 zero-argument
aura-stdbuilder factories to their type identities(cycle 0087, commit
d5602ec). It is hand-maintained: a new zero-arg node addedto
aura-stddoes not automatically gain a match arm.Consequence (verified by reading the match + its test): a blueprint that
references the new node's type identity fails to load with
LoadError::UnknownNodeType— clean and named, never a silent wrong graph(C1-safe) — but the node is silently non-round-trippable until someone
notices. The existing test
std_vocabulary_resolves_known_and_rejects_unknown(
crates/aura-std/src/vocabulary.rs) enumerates only the 22 current keys, so itdoes not fail when the roster grows.
A guard that fails at test time when an
aura-stdzero-arg node lacks avocabulary arm would catch this drift. There is no introspective "all zero-arg
builders" enumeration today, so such a guard needs either a maintained roster or
a compile-time registration mechanism that stays within the closed compiled-in
set constraint (domain invariant 9 / C24 — the vocabulary must not become a
dynamic/marketplace registry).
Surfaced by the cycle-0087 architect drift review (classified
debt-med).UnknownNodeType),so non-blocking. Droppable.
Cycle 0088 (#157) escalation: the vocabulary roster now has a user-facing reader
Cycle 0088 added
std_vocabulary_types()(crates/aura-std/src/vocabulary.rs) — anenumerable companion list of the 22 zero-arg type identities — because the
construction service's build-free introspection (
aura graph introspect --vocabulary) cannot enumerate afn(&str)->Option<…>resolver. This is a thirdhand-maintained copy of the roster (the
std_vocabularymatch arms + the existingtest's inline list + the new
std_vocabulary_typesbody), and it escalates thisissue's consequence: a new zero-arg node missing from
std_vocabulary_typesnowmakes
aura graph introspect --vocabularysilently under-report the vocabulary toa data-level author — a user-facing gap, not only the non-round-trippable load gap this
issue originally described.
The cycle-0088 lockstep test (
std_vocabulary_types_lists_exactly_the_resolvable_keys,same file) guards only the list → resolver direction (every listed type resolves;
count == 22); the reverse (a resolver arm with no list entry) stays unguardable from a
fnresolver, exactly as this issue notes. The guard this issue calls for should nowcover the list copy too — a single roster all three copies derive from would close both
the round-trip gap and the introspection gap at once. Still fails safe today (clean
UnknownNodeTypeon load; an unlisted-but-resolvable node is merely absent from--vocabulary, never wrong). Surfaced by the cycle-0088 architect drift review.Post-C24 reframe: this guards the data-plane face of invariant 9
A cycle-0091 design analysis (adversarially verified) reframes this issue's
weight. After C24 lifted blueprints onto the data plane,
std_vocabularyis theload-time gate a serialized blueprint's type-id strings pass through. Pre-C24, a
blueprint referencing a node outside the compiled-in set was a compile error
— topology was in-process Rust, so the reference was structurally impossible.
Post-C24 it is a string refused only by this resolver's closed set
(
UnknownNodeType). So enforcement of the engine/project boundary (invariant 9)shifted from compiler-guaranteed structural impossibility to resolver-seam
discipline — and this guard (that the closed vocabulary stays honest) is the
data-plane face of that discipline, not droppable test hygiene.
Scope note: this issue's OWN drift direction still fails safe — a new zero-arg
node lacking a match arm is non-round-trippable but never a wrong graph (clean
UnknownNodeType). The dangerous direction for invariant 9 — an injectedresolver that resolves TOO MUCH (cross-project type-ids) — is a separate,
deliberately deferred surface for the World / project-as-crate layer (the "what
may an injected project resolver resolve" charter point), recorded in the C24
ledger entry's "Enforcement shift" note (
docs/design/INDEX.md).The mechanism choice stands: a declarative macro as the single source for both
the resolver match arms and the enumerable
std_vocabulary_types()list(collapsing the hand-kept 3-copy lockstep, staying a closed compiled-in set — no
runtime registry, invariant 9). The label stays
idea(stillbehaviour-preserving, fails safe), but the rationale is no longer "consistency
nicety".
Design reconciliation (cycle 0105)
Spec: std-vocabulary roster macro. The mechanism this issue's 2026-06-30 comment records (a declarative macro as the single source for the resolver match arms and the
std_vocabulary_types()list) is settled; the remaining sub-forks are resolved below.macro_rules!invocation carrying the"TypeId" => Typeroster and expanding into BOTHstd_vocabularyandstd_vocabulary_types.Basis: derived — the pair-generating single invocation makes divergence between match arms and list structurally impossible (exactly this issue's failure mode), whereas a callback-style roster macro adds an indirection layer with no second consumer to justify it.
std_vocabulary_types().Basis: derived — with the roster as single source, an independent inline copy re-introduces the very lockstep being removed; the label round-trip against
PrimitiveBuilder::label()remains the independent oracle (a typo'd roster key still fails against the builder's own label, so the test is not self-referential).Basis: derived — no enumeration of zero-arg builders exists to test against (as this issue's body already notes), the failure mode is fail-safe in both directions (clean
LoadError::UnknownNodeTypeon load; merely absent from--vocabulary), and the maintainer surface shrinks from three coordinated edits to one roster line, which is the practical guard. The count pin (== 22) stays as deliberate friction: any roster-line change trips a conscious test update.Basis: derived — behaviour-preserving by design; every consumer (loader resolvers,
graph introspect --vocabularyvia the project env, the 0102 merged-vocabulary charter checks) is untouched.Status: design settled — ready for spec production.