From cb558519b0a919f9738a2c71e41a29c7382509ed Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 30 Jun 2026 13:11:39 +0200 Subject: [PATCH] spec: 0090 blueprint forward-compat two-tier discipline (boss-signed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/specs/0090-blueprint-forward-compat.md | 204 ++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 docs/specs/0090-blueprint-forward-compat.md diff --git a/docs/specs/0090-blueprint-forward-compat.md b/docs/specs/0090-blueprint-forward-compat.md new file mode 100644 index 0000000..079afae --- /dev/null +++ b/docs/specs/0090-blueprint-forward-compat.md @@ -0,0 +1,204 @@ +# 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_fields` anywhere — verified `rg = 0`), `skip_serializing_if` + omits absent options, and **no `format_version` bump** 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_version` envelope: `BLUEPRINT_FORMAT_VERSION = 1`, `BlueprintDoc { + format_version, blueprint }` (`blueprint_serde.rs:17-24`). +- Canonical omit-defaults form: `#[serde(default, skip_serializing_if = ...)]` on + `CompositeData::{edges,input_roles,output}` and `PrimitiveData::{name,bound}`. + Pinned by `signal_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`, test `unsupported_version_fails_named` + (`format_version: 2` → refused). + - `LoadError::UnknownNodeType(String)` — `blueprint_serde.rs:128`, test + `unknown_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: + +1. one **executable proof** (the Tier-1 forward-tolerance test), and +2. 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): + +```rust +#[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`): + +```rust + /// `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): + +```rust + /// `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 the + `LoadError::UnsupportedVersion` doc comment. No code/behaviour change. +- `docs/design/INDEX.md` — C24 Status/Remaining update (codification). + +## Data flow + +Unchanged. `blueprint_from_json` → `serde_json::from_str::` (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`; lint `cargo 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_version` and an + out-of-vocabulary node each yield a clean, named `LoadError` (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 `LoadError` doc and the C24 + ledger entry; the deferred per-section required-flag mechanism is recorded as + out-of-scope on #156. +- [ ] `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D + warnings` clean.