Guard std_vocabulary against silently dropping a new zero-arg node #160

Closed
opened 2026-06-29 15:00:45 +02:00 by Brummel · 3 comments
Owner

The closed match in crates/aura-std/src/vocabulary.rs (std_vocabulary)
maps the 22 zero-argument aura-std builder factories to their type identities
(cycle 0087, commit d5602ec). It is hand-maintained: a new zero-arg node added
to aura-std does 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 it
does not fail when the roster grows.

A guard that fails at test time when an aura-std zero-arg node lacks a
vocabulary 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).

  • depends on: nothing (standalone hardening)
  • context: deferred at cycle-0087 close; fails safe today (UnknownNodeType),
    so non-blocking. Droppable.
The closed `match` in `crates/aura-std/src/vocabulary.rs` (`std_vocabulary`) maps the 22 zero-argument `aura-std` builder factories to their type identities (cycle 0087, commit `d5602ec`). It is hand-maintained: a new zero-arg node added to `aura-std` does 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 it does not fail when the roster grows. A guard that fails at test time when an `aura-std` zero-arg node lacks a vocabulary 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`). - depends on: nothing (standalone hardening) - context: deferred at cycle-0087 close; fails safe today (`UnknownNodeType`), so non-blocking. Droppable.
Brummel added the idea label 2026-06-29 15:00:45 +02:00
Author
Owner

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) — an
enumerable companion list of the 22 zero-arg type identities — because the
construction service's build-free introspection (aura graph introspect --vocabulary) cannot enumerate a fn(&str)->Option<…> resolver. This is a third
hand-maintained copy of the roster (the std_vocabulary match arms + the existing
test's inline list + the new std_vocabulary_types body), and it escalates this
issue's consequence
: a new zero-arg node missing from std_vocabulary_types now
makes aura graph introspect --vocabulary silently under-report the vocabulary to
a 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
fn resolver, exactly as this issue notes. The guard this issue calls for should now
cover 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
UnknownNodeType on load; an unlisted-but-resolvable node is merely absent from
--vocabulary, never wrong). Surfaced by the cycle-0088 architect drift review.

## 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`) — an enumerable companion list of the 22 zero-arg type identities — because the construction service's build-free introspection (`aura graph introspect --vocabulary`) cannot enumerate a `fn(&str)->Option<…>` resolver. This is a **third** hand-maintained copy of the roster (the `std_vocabulary` match arms + the existing test's inline list + the new `std_vocabulary_types` body), and it **escalates this issue's consequence**: a new zero-arg node missing from `std_vocabulary_types` now makes `aura graph introspect --vocabulary` **silently under-report** the vocabulary to a 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 `fn` resolver, exactly as this issue notes. The guard this issue calls for should now cover 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 `UnknownNodeType` on load; an unlisted-but-resolvable node is merely absent from `--vocabulary`, never wrong). Surfaced by the cycle-0088 architect drift review.
Author
Owner

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_vocabulary is the
load-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 injected
resolver 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 (still
behaviour-preserving, fails safe), but the rationale is no longer "consistency
nicety".

## 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_vocabulary` is the load-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 injected resolver 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` (still behaviour-preserving, fails safe), but the rationale is no longer "consistency nicety".
Brummel added this to the Project environment — the project-as-crate authoring loop milestone 2026-07-02 14:17:30 +02:00
Author
Owner

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.

  • Fork: macro shape → one macro_rules! invocation carrying the "TypeId" => Type roster and expanding into BOTH std_vocabulary and std_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.
  • Fork: the third copy (the test's inline 22-key list) → replaced by iterating the generated 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).
  • Fork: residual drift ("a new zero-arg node never rostered at all") → accepted as out of scope, documented at the roster site.
    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::UnknownNodeType on 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.
  • Fork: public surface → both fn signatures, the 22 keys, and their order stay byte-identical.
    Basis: derived — behaviour-preserving by design; every consumer (loader resolvers, graph introspect --vocabulary via the project env, the 0102 merged-vocabulary charter checks) is untouched.

Status: design settled — ready for spec production.

## 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. - **Fork: macro shape** → one `macro_rules!` invocation carrying the `"TypeId" => Type` roster and expanding into BOTH `std_vocabulary` and `std_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. - **Fork: the third copy (the test's inline 22-key list)** → replaced by iterating the generated `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). - **Fork: residual drift ("a new zero-arg node never rostered at all")** → accepted as out of scope, documented at the roster site. 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::UnknownNodeType` on 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. - **Fork: public surface** → both fn signatures, the 22 keys, and their order stay byte-identical. Basis: derived — behaviour-preserving by design; every consumer (loader resolvers, `graph introspect --vocabulary` via the project env, the 0102 merged-vocabulary charter checks) is untouched. Status: design settled — ready for spec production.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#160