Iteration within the C24 topology-as-data milestone (#156). Codifies the blueprint format's additive forward-compatibility under one discipline: Tier-1 (additive-optional) = serde-default silent-ignore, no version bump, C1 default-equals-prior-behaviour; Tier-2 (must-understand: new node type, edge semantics, structural-axis kind) = bump format_version so an old reader refuses cleanly rather than building a different graph. Most of #156 already shipped with #155 (cycle 0087): the format_version envelope, omit-defaults canonical form, and both Tier-2 load-path refusals (LoadError::UnsupportedVersion / UnknownNodeType) are present and green. The genuine remainder is one unproven property — forward-tolerance of an unknown optional field on the current loader — plus codifying the discipline in the LoadError doc and the C24 ledger entry. Not a new mechanism. Derived fork recorded on #156: the per-section required-flag mechanism (PNG critical-vs-ancillary) is NOT built — the version envelope already gives the must-understand refusal at version granularity, there is no current Tier-2 section to validate a finer scheme against, and C1 holds either way. Boss-signed on a grounding-check PASS (all current-behaviour claims ratified by named green tests; the new Tier-1 pin correctly absent — this cycle's deliverable). refs #156
10 KiB
Blueprint format forward-compatibility: the two-tier discipline — Design Spec
Date: 2026-06-30 Status: Draft — awaiting user spec review Authors: orchestrator + Claude
Goal
Close #156: guarantee the blueprint data format (C24) extends additively — later additions land without breaking existing serializations — under a single, codified two-tier discipline:
- Tier-1 (additive-optional, ignore / default-safe). A new optional field or
section is tolerated by an older reader: serde ignores unknown fields (no
deny_unknown_fieldsanywhere — verifiedrg = 0),skip_serializing_ifomits absent options, and noformat_versionbump occurs. Constraint (C1): a new optional field's default equals prior behaviour, so an old blueprint loaded by a new engine reproduces the same graph. - Tier-2 (load-bearing, must-understand). A new node type, edge semantics, or
structural-axis kind must bump
format_version, so an old reader refuses cleanly (LoadError::UnsupportedVersion) rather than silently building a different graph. An out-of-vocabulary node likewise refuses (LoadError::UnknownNodeType).
Most of #156 already shipped with #155 (cycle 0087). This cycle is a small
verification-plus-codification iteration: it adds the one missing proof —
forward-tolerance of an unknown optional field on the current loader — and writes
the discipline durably into the LoadError doc and the C24 ledger entry. It is
not a new mechanism.
What #155 already delivers (verified, green — the grounding evidence)
- The
format_versionenvelope:BLUEPRINT_FORMAT_VERSION = 1,BlueprintDoc { format_version, blueprint }(blueprint_serde.rs:17-24). - Canonical omit-defaults form:
#[serde(default, skip_serializing_if = ...)]onCompositeData::{edges,input_roles,output}andPrimitiveData::{name,bound}. Pinned bysignal_serializes_to_canonical_golden. - Both Tier-2 refusals at the load path, each with a green test:
LoadError::UnsupportedVersion { found, supported }—blueprint_serde.rs:162-167, testunsupported_version_fails_named(format_version: 2→ refused).LoadError::UnknownNodeType(String)—blueprint_serde.rs:128, testunknown_node_type_fails_named("NoSuchNode"→ refused).
There is no CLI load-a-blueprint-JSON command today; the version-refusal has
no CLI surface, so the engine-level typed LoadError is the Tier-2 contract.
The CLI build path (aura graph build) already exit-codes an unknown node type
via OpError (cycle 0088). This cycle adds no CLI surface.
Architecture
The forward-compat guarantee is a property of the existing serde projection, not a new component. Tier-1 tolerance is already true (serde's default unknown-field handling + the omit-defaults discipline); it is simply unproven. The deliverable is therefore:
- one executable proof (the Tier-1 forward-tolerance test), and
- a documentation codification turning the currently-deferred "must-understand horizon … is #156" note into the settled two-tier rule.
No struct, signature, or runtime behaviour changes. The derived design fork (the per-section required-flag / PNG critical-vs-ancillary mechanism) is not built — recorded on #156 with its rationale: the version envelope already gives the must-understand refusal at version granularity, there is no current Tier-2 section to validate a finer scheme against, and C1 holds either way.
Concrete code shapes
The deliverable: the Tier-1 forward-tolerance test (new — the additive property as executable evidence)
Added to the blueprint_serde.rs test module (it already has run_recording,
sink_free_sma_cross_signal, and std_vocabulary in scope):
#[test]
fn unknown_optional_field_is_tolerated_byte_identically() {
// Forward-compat (C24 / #156, Tier-1): a future writer that has additively
// extended the format emits optional keys the CURRENT loader does not know.
// The loader must silently ignore them and reconstruct the IDENTICAL graph —
// never refuse, never let an unknown key leak into the built blueprint. serde
// ignores unknown fields by default (no deny_unknown_fields). This pins the
// additive property the canonical-golden + omit-defaults discipline claims but
// no existing test exercises.
let canonical =
blueprint_to_json(&crate::test_fixtures::sink_free_sma_cross_signal()).expect("serializes");
// Inject unknown optional keys at two additive shapes a new writer might emit:
// a doc-level section ("metadata") and a per-primitive key ("annotations").
let with_unknown = canonical
.replacen(
r#"{"format_version":1,"#,
r#"{"format_version":1,"metadata":{"author":"future"},"#,
1,
)
.replacen(r#"{"type":"Sub"}"#, r#"{"type":"Sub","annotations":["future"]}"#, 1);
assert_ne!(with_unknown, canonical, "probe must actually inject unknown keys");
// tolerated, not refused
let loaded = blueprint_from_json(&with_unknown, &|t| aura_std::std_vocabulary(t))
.expect("an unknown optional field is tolerated, not refused");
// the unknown keys did not leak into the graph: re-serialization equals the
// canonical form WITHOUT them (byte-identical reconstruction).
assert_eq!(blueprint_to_json(&loaded).expect("re-serializes"), canonical);
// and the loaded graph runs bit-identically to the plain canonical one (C1).
let plain = blueprint_from_json(&canonical, &|t| aura_std::std_vocabulary(t)).expect("loads");
assert_eq!(
run_recording(loaded),
run_recording(plain),
"run diverged under an unknown optional field",
);
}
The codification: LoadError::UnsupportedVersion doc (before → after)
Before (blueprint_serde.rs:110-113):
/// `format_version` the loader does not understand. The full must-understand
/// horizon (multiple versions, unknown-section policy) is #156; #155 supports
/// exactly one version and refuses any other, cleanly.
UnsupportedVersion { found: u32, supported: u32 },
After (the discipline is now settled, not deferred):
/// `format_version` the loader does not understand — the **Tier-2
/// (must-understand)** refusal. The format extends under a two-tier discipline
/// (#156): a **Tier-1** additive-optional change (a new optional field /
/// section) does NOT bump the version — an older reader tolerates it (serde
/// ignores unknown fields; see `unknown_optional_field_is_tolerated_byte_identically`)
/// and, by C1, a new optional field defaults to prior behaviour. A **Tier-2**
/// load-bearing change (a new node type, edge semantics, or structural-axis
/// kind) MUST bump `BLUEPRINT_FORMAT_VERSION`, so an old reader refuses here
/// rather than silently building a different graph (C1 / C18). The
/// per-section required-flag scheme is deferred until a real sub-version Tier-2
/// addition needs finer granularity than a version bump.
UnsupportedVersion { found: u32, supported: u32 },
(The BLUEPRINT_FORMAT_VERSION doc at :15-17 already states "Bumped only by a
load-bearing (Tier-2) change; additive optional fields do not bump it (#156)" and
needs no change — it is consistent with the settled discipline.)
The codification: C24 ledger Status block (before → after, shape only)
The C24 entry's **Remaining** list (INDEX.md:1823-1827) names "the
forward-compat must-understand gate (#156)". This cycle moves #156 out of
Remaining into a shipped note stating the two-tier discipline and the proof test.
Exact prose is the planner's; the shape: drop #156 from Remaining, add one
sentence to Status that the forward-compat discipline is codified (Tier-1
serde-default tolerance proven; Tier-2 = version/vocabulary refusal, already
green) — leaving #158 and #159 in Remaining.
Components
crates/aura-engine/src/blueprint_serde.rs— add one test (unknown_optional_field_is_tolerated_byte_identically); reword theLoadError::UnsupportedVersiondoc comment. No code/behaviour change.docs/design/INDEX.md— C24 Status/Remaining update (codification).
Data flow
Unchanged. blueprint_from_json → serde_json::from_str::<BlueprintDoc> (ignores
unknown fields) → version check → reconstruct (resolves each type_id through
the injected vocabulary). The test exercises exactly this path with an
unknown-key-bearing input.
Error handling
Unchanged and already complete: LoadError::{Json, UnsupportedVersion, UnknownNodeType} — typed, named, never a panic, never a silent wrong graph. The
two Tier-2 arms are the must-understand refusals; the new test asserts the Tier-1
path does not error.
Testing strategy
- New (Tier-1, the gap):
unknown_optional_field_is_tolerated_byte_identically— an unknown optional key at doc and primitive level is ignored; the reconstructed graph re-serializes byte-identically to the canonical form and runs bit-identically (C1). - Existing (Tier-2, cited as grounding evidence, unchanged):
unsupported_version_fails_named,unknown_node_type_fails_named. - Full workspace gate:
cargo test --workspace; lintcargo clippy --workspace --all-targets -- -D warnings.
Acceptance criteria
- (a) Tier-1 additive forward-tolerance — a blueprint JSON carrying an unknown optional field/section loads on the current loader, the unknown key does not leak into the graph (byte-identical re-serialization), and the run is bit-identical. Pinned by the new test.
- (b) Tier-2 must-understand refusal — a too-new
format_versionand an out-of-vocabulary node each yield a clean, namedLoadError(never a panic, never a silent graph change). Already green (unsupported_version_fails_named,unknown_node_type_fails_named); unchanged by this cycle. - The two-tier discipline is codified in the
LoadErrordoc and the C24 ledger entry; the deferred per-section required-flag mechanism is recorded as out-of-scope on #156. cargo test --workspacegreen;cargo clippy --workspace --all-targets -- -D warningsclean.