Files
Aura/fieldtests/milestone-topology-as-data/mt_4_forward_compat.rs
T
Brummel 3f75d59598 audit(0090): cycle + C24 milestone close — fieldtest green; ledger + rm ephemeral spec/plan
Closes the cycle-0090 (#156) work and delivers the "Topology-as-data:
blueprint serialization & loader (C24)" milestone.

Cycle-0090 audit (architect drift review, range 7ad7f58..HEAD): drift-clean
on substance. Tier-1 forward-tolerance is genuinely serde-default
(deny_unknown_fields = 0 in every production struct), pinned at envelope +
primitive across both the in-crate and public-seam tests; Tier-2 refusals
real and green; no struct/signature/runtime change; the invariant-5
acyclicity lockstep (construction gate <-> bootstrap Kahn sort) undisturbed;
invariant-9 resolver stays injected, no registry. The one medium item the
architect flagged — the C24 Remaining block carried no deferral signal for
#158/#159 — is fixed in this commit's ledger update.

Milestone fieldtest (fieldtests/milestone-topology-as-data/, the close-gate
evidence): GREEN, 0 behavioural bugs. A downstream consumer ran the full
author -> serialize -> load -> construct -> introspect -> reproduce story from
the public surface alone — round-trip bit-exact on both authoring surfaces
(Rust builder + op-script CLI), build-free introspection, Tier-1/Tier-2
forward-compat by name, fail-fast diagnostics by identifier. Verified the
headline claims myself (the two Rust binaries + the CLI build/diff + the
fail-fast exits), not just the agent's report.

Findings triaged to the forward queue (none blocking): op-script grammar
undocumented + a stale cycle-0088 example (#163); canonical trailing-newline
divergence + Composite Debug/PartialEq ergonomics (#164); CLI discoverability
of build/introspect + no `graph run` (commented on #159); the newline as a
content-addressing prerequisite (commented on #158).

#158 (content-address topology in the manifest) and #159 (retire the
hard-wired harnesses) were moved OUT of this milestone: both are premature
until the World generates runs from blueprint-data (commit still fully
identifies every hard-wired topology), so they belong to the
project-as-crate / World cycle. The C24 ledger Status records the delivery
and the deferral.

Ephemeral cycle-0090 spec + plan removed per the lifecycle convention.
The Gitea milestone container is closed on user ratification (this run
tees up the close, does not click it).
2026-06-30 14:43:28 +02:00

153 lines
6.8 KiB
Rust

// Milestone fieldtest — Topology-as-data (C24), Example 4.
//
// The forward-compatibility discipline the milestone promises (#156 / C24
// two-tier), exercised from the loader's seat, plus cross-surface interop:
//
// 0. Cross-surface interop: the blueprint emitted by the `aura graph build`
// CLI (mt_2_blueprint.cli.json, trailing newline and all) loads cleanly via
// the library blueprint_from_json and runs — proving CLI-authored topology
// is library-consumable.
// 1. Tier-1 (additive-optional tolerance): a future writer adds an UNKNOWN
// optional field — at the envelope level AND inside a node — and an older
// reader must (a) load it, (b) drop it on re-serialize back to the SAME
// canonical form, and (c) run BIT-IDENTICALLY to the clean blueprint.
// 2. Tier-2 (must-understand refusal): a too-new format_version and an
// out-of-vocabulary node type must each fail CLEANLY BY NAME
// (LoadError::UnsupportedVersion / UnknownNodeType) — never a panic, never
// a silently-wrong graph.
//
// Public interface only: blueprint_to_json / blueprint_from_json / LoadError /
// std_vocabulary, learned from `cargo doc` + the C24 ledger. No crates/*/src.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, LoadError,
OutField, Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};
fn author_signal() -> Composite {
let mut gb = GraphBuilder::new("graph");
let price = gb.source_role("price", ScalarKind::F64);
let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2)));
let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4)));
gb.feed(price, [fast.input("series"), slow.input("series")]);
let sub = gb.add(Sub::builder().named("sub"));
gb.connect(fast.output("value"), sub.input("lhs"));
gb.connect(slow.output("value"), sub.input("rhs"));
let bias = gb.add(Bias::builder().named("bias"));
gb.connect(sub.output("value"), bias.input("signal"));
gb.expose(bias.output("bias"), "bias");
gb.build().expect("signal composite should build")
}
fn run_signal(signal: Composite) -> Vec<(i64, f64)> {
let prices: [f64; 12] = [
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
let harness = Composite::new(
"harness",
vec![
BlueprintNode::Composite(signal),
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![] as Vec<OutField>,
);
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
let mut h = harness
.bootstrap_with_params(vec![Scalar::F64(1.0)])
.expect("bootstrap");
h.run(vec![Box::new(VecSource::new(stream))]);
drop(h);
rx.iter()
.map(|(ts, row)| match row[0] {
Scalar::F64(x) => (ts.0, x),
other => panic!("expected f64 bias, got {other:?}"),
})
.collect()
}
fn main() {
let canonical = blueprint_to_json(&author_signal()).expect("serialize");
let baseline_run = run_signal(author_signal());
// --- 0. cross-surface interop: load the CLI `graph build` output ----------
match std::fs::read_to_string("mt_2_blueprint.cli.json") {
Ok(cli_json) => {
let from_cli = blueprint_from_json(&cli_json, &std_vocabulary)
.expect("CLI-emitted blueprint must load via the library loader");
assert_eq!(
run_signal(from_cli),
baseline_run,
"CLI-authored topology must run identically to the Rust-authored twin"
);
let trailing_nl = cli_json.ends_with('\n');
println!(
"0. cross-surface interop: CLI blueprint loaded + ran identically \
(loader tolerated trailing newline = {trailing_nl})"
);
}
Err(_) => println!("0. (skipped: run mt_2 CLI build first to produce mt_2_blueprint.cli.json)"),
}
// --- 1. Tier-1: unknown optional fields are tolerated ---------------------
// envelope-level unknown key
let env_unknown = canonical.replacen('{', r#"{"x_future_envelope":{"any":[1,2]},"#, 1);
// node-level unknown key (inject inside the first primitive object)
let node_unknown = canonical.replacen(
r#"{"type":"SMA","name":"fast""#,
r#"{"x_future_node_flag":true,"type":"SMA","name":"fast""#,
1,
);
for (label, mutated) in [("envelope", &env_unknown), ("node", &node_unknown)] {
let loaded = blueprint_from_json(mutated, &std_vocabulary)
.unwrap_or_else(|e| panic!("Tier-1 {label} unknown field must be tolerated, got {e:?}"));
let reser = blueprint_to_json(&loaded).expect("re-serialize");
assert_eq!(
reser, canonical,
"Tier-1 {label}: re-serialized form must drop the unknown field and match canonical"
);
assert_eq!(
run_signal(loaded),
baseline_run,
"Tier-1 {label}: must run bit-identically to the clean blueprint"
);
println!("1. Tier-1 {label} unknown field: tolerated, dropped on re-emit, ran identical.");
}
// --- 2. Tier-2: must-understand refusals, by name, no panic ---------------
// (Composite has no Debug impl, so we cannot {:?} the Ok arm — match explicitly.)
let too_new = canonical.replacen(r#""format_version":1"#, r#""format_version":999"#, 1);
match blueprint_from_json(&too_new, &std_vocabulary) {
Err(e @ LoadError::UnsupportedVersion { .. }) => {
println!("2a. too-new format_version (999): refused -> {e:?}")
}
Err(other) => panic!("2a. expected UnsupportedVersion, got {other:?}"),
Ok(_) => panic!("2a. expected refusal, but a graph was silently loaded"),
}
let bad_type = canonical.replacen(r#""type":"Sub""#, r#""type":"FOO""#, 1);
match blueprint_from_json(&bad_type, &std_vocabulary) {
Err(e @ LoadError::UnknownNodeType(..)) => {
println!("2b. out-of-vocabulary node type (FOO): refused -> {e:?}")
}
Err(other) => panic!("2b. expected UnknownNodeType, got {other:?}"),
Ok(_) => panic!("2b. expected refusal, but a graph was silently loaded"),
}
println!("PASS: Tier-1 tolerance + Tier-2 by-name refusal (no panic, no silent wrong graph).");
}