diff --git a/docs/plans/0090-blueprint-forward-compat.md b/docs/plans/0090-blueprint-forward-compat.md new file mode 100644 index 0000000..1fab5d3 --- /dev/null +++ b/docs/plans/0090-blueprint-forward-compat.md @@ -0,0 +1,195 @@ +# Blueprint format forward-compatibility: the two-tier discipline — Implementation Plan + +> **Parent spec:** `docs/specs/0090-blueprint-forward-compat.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run +> this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Pin the blueprint format's Tier-1 additive forward-tolerance with one +characterization test and codify the two-tier discipline in the `LoadError` doc +and the C24 ledger entry (#156). + +**Architecture:** No struct, signature, or runtime behaviour changes. Tier-1 +tolerance is already true (serde ignores unknown fields — no `deny_unknown_fields` +anywhere; omit-defaults via `skip_serializing_if`); this iteration proves it and +documents the discipline. Both Tier-2 refusals already ship green +(`LoadError::UnsupportedVersion` / `UnknownNodeType`). + +**Tech Stack:** `crates/aura-engine/src/blueprint_serde.rs` (test + doc), +`docs/design/INDEX.md` (C24 ledger). + +--- + +**Files this plan creates or modifies:** + +- Test: `crates/aura-engine/src/blueprint_serde.rs:171-283` — append the Tier-1 + forward-tolerance characterization test to the `#[cfg(test)] mod tests` block. +- Modify: `crates/aura-engine/src/blueprint_serde.rs:110-112` — reword the + `LoadError::UnsupportedVersion` doc comment to state the settled two-tier + discipline. +- Modify: `docs/design/INDEX.md:1822-1827` — add one codification sentence to the + C24 Status block and drop #156 from the Remaining list (keep #158/#159). + +All three changes are one cohesive, independently-reviewable unit → a single task. + +--- + +### Task 1: Tier-1 forward-tolerance pin + two-tier codification + +**Files:** +- Test: `crates/aura-engine/src/blueprint_serde.rs:171-283` +- Modify: `crates/aura-engine/src/blueprint_serde.rs:110-112` +- Modify: `docs/design/INDEX.md:1822-1827` + +- [ ] **Step 1: Append the Tier-1 forward-tolerance test** + +In `crates/aura-engine/src/blueprint_serde.rs`, inside the `#[cfg(test)] mod tests` +block, insert the following test immediately **after** the closing `}` of +`unsupported_version_fails_named` (currently line 282) and **before** the module's +closing `}` (currently line 283). All referenced helpers are in scope: +`run_recording` (:188), `crate::test_fixtures::sink_free_sma_cross_signal` (used at +:212/:225/:237/:258), the `&|t| aura_std::std_vocabulary(t)` closure form (used at +:214/:227/:250/:273/:280), `blueprint_to_json`/`blueprint_from_json` (via `use +super::*;` at :173). + +```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", + ); + } +``` + +- [ ] **Step 2: Run the new test to verify it passes (characterization pin)** + +This test pins ALREADY-EXISTING behaviour, so it goes GREEN on first run (it is a +forward-tolerance characterization pin, NOT a RED test). + +Run: `cargo test -p aura-engine --lib unknown_optional_field_is_tolerated_byte_identically` +Expected: PASS — `test result: ok. 1 passed` (the filter matches exactly this one +new test by name). + +- [ ] **Step 3: Reword the `LoadError::UnsupportedVersion` doc comment** + +In `crates/aura-engine/src/blueprint_serde.rs`, replace the exact current doc +comment (lines 110-112, verbatim): + +```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. +``` + +with: + +```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 replacement includes the unchanged `UnsupportedVersion { ... }` variant line +as the trailing anchor so the edit lands at the right spot. The +`BLUEPRINT_FORMAT_VERSION` doc at :15-16 is left unchanged — it already states the +rule.) + +- [ ] **Step 4: Add the C24 codification sentence to the ledger Status block** + +In `docs/design/INDEX.md`, the C24 Status block currently ends (line 1822) with +`...the no-second-validator lockstep preserved), only translating their result.` +immediately followed by `**Remaining**` on line 1823. Replace the exact current +text: + +``` +no-second-validator lockstep preserved), only translating their result. +**Remaining** +``` + +with: + +``` +no-second-validator lockstep preserved), only translating their result. +**Cycle 0090 (#156) codifies the forward-compat two-tier discipline.** Tier-1 +(additive-optional) is serde-default silent-ignore with no `format_version` bump +(a new optional field defaults to prior behaviour, C1) — now proven by +`unknown_optional_field_is_tolerated_byte_identically` (an unknown optional key +loads byte-identically and runs bit-identically). Tier-2 (must-understand: a new +node type, edge semantics, or structural-axis kind) bumps `format_version` so an +old reader refuses cleanly (`LoadError::UnsupportedVersion` / `UnknownNodeType`, +already green). The per-section required-flag scheme is deferred (no current +Tier-2 section to validate it; recorded on #156). +**Remaining** +``` + +- [ ] **Step 5: Drop #156 from the C24 Remaining list** + +In `docs/design/INDEX.md`, the Remaining block (line 1824) currently reads: + +``` +(each its own milestone issue): the forward-compat *must-understand* gate (#156); +``` + +Replace that exact line with: + +``` +(each its own milestone issue): +``` + +This leaves the #158 / #159 entries on lines 1825-1827 intact. Do NOT touch the +other four `#156` occurrences (`blueprint_serde.rs:15-16`, `blueprint_serde.rs:111` +in the reworded doc, `aura-std/src/vocabulary.rs:18`, `INDEX.md:1831`) — they are +semantically distinct and stay. + +- [ ] **Step 6: Full workspace gate** + +Run: `cargo test --workspace` +Expected: PASS — all suites green, including the new +`unknown_optional_field_is_tolerated_byte_identically`. No regressions. + +- [ ] **Step 7: Lint gate** + +Run: `cargo clippy --workspace --all-targets -- -D warnings` +Expected: clean (no warnings).