plan: 0090 blueprint forward-compat

Single-task iteration for #156: append the Tier-1 forward-tolerance
characterization test (unknown_optional_field_is_tolerated_byte_identically),
reword the LoadError::UnsupportedVersion doc to the settled two-tier
discipline, and codify it in the C24 ledger (Status sentence + drop #156 from
Remaining). No struct/signature/runtime change. refs #156
This commit is contained in:
2026-06-30 13:16:21 +02:00
parent cb558519b0
commit ead064df50
+195
View File
@@ -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).