Files
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

124 lines
5.5 KiB
Rust

// Milestone fieldtest — Topology-as-data (C24), Example 1.
//
// The headline round-trip the milestone promises: a researcher authors a small
// SMA-cross bias signal with the Rust GraphBuilder, serializes the topology to
// canonical JSON (blueprint_to_json), loads it back through the std vocabulary
// resolver (blueprint_from_json + aura_std::std_vocabulary), and proves
// (a) the canonical JSON round-trips byte-identically (serialize -> load ->
// re-serialize == original), and
// (b) the RELOADED blueprint runs BIT-IDENTICALLY to its Rust-built twin
// (C1: the recorded bias trace matches value-for-value).
//
// Discovery: the GraphBuilder / blueprint_to_json / blueprint_from_json surface
// was learned from `cargo doc` doc-comments + the C24 ledger entry + `aura graph
// introspect --node <T>` (which gives each node's port and bind shapes). No
// crates/*/src was read.
//
// Note on the run path: a recording sink (Recorder) is deliberately OUTSIDE the
// serializable round-trippable set (C24 ledger: it captures an mpsc::Sender —
// runtime identity, not param-generic data). So the *serializable* unit is the
// pure signal sub-composite (price source -> SMAs -> Sub -> Bias -> expose
// bias); to OBSERVE a run we nest that sub-composite under a tiny throwaway
// harness wrapper that adds a Recorder. The same wrapper is applied to both the
// original and the reloaded sub-composite, so the comparison is apples-to-apples.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, OutField,
Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};
/// Author the serializable signal sub-composite via the Rust builder API.
/// SMA(2) fast, SMA(4) slow, Sub = fast - slow, Bias over the spread, exposing
/// the single `bias` field. The two SMA lengths are baked (`.bind`); Bias.scale
/// is left as a tunable param (mirrors the canonical op-script form).
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")
}
/// Run a signal sub-composite by nesting it under a Recorder harness and
/// recording the exposed `bias` each fired cycle. Returns (ts, bias) rows.
fn run_signal(signal: Composite, prices: &[f64]) -> Vec<(i64, f64)> {
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 }], // bias -> recorder col 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();
// bias.scale is the one tunable param left in the space; bind it at bootstrap.
let mut h = harness
.bootstrap_with_params(vec![Scalar::F64(1.0)])
.expect("harness should bootstrap");
h.run(vec![Box::new(VecSource::new(stream))]);
drop(h); // release the recorder's tx clone
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 prices: Vec<f64> = vec![
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
// --- (a) canonical JSON round-trip ---------------------------------------
let orig = author_signal();
let json1 = blueprint_to_json(&orig).expect("serialize original");
std::fs::write("mt_1_rust_blueprint.json", &json1).expect("persist artifact");
let loaded = blueprint_from_json(&json1, &std_vocabulary).expect("load from json");
let json2 = blueprint_to_json(&loaded).expect("re-serialize loaded");
assert_eq!(
json1, json2,
"canonical JSON must round-trip byte-identically (serialize->load->re-serialize)"
);
println!("(a) canonical JSON round-trip: IDENTICAL ({} bytes)", json1.len());
// --- (b) run identity: reloaded twin runs bit-identical to the original ---
let orig_run = run_signal(author_signal(), &prices);
let loaded_run = run_signal(loaded, &prices);
assert_eq!(
orig_run, loaded_run,
"C1: the reloaded blueprint must run bit-identically to its Rust-built twin"
);
println!(
"(b) run identity: {} recorded bias rows, value-for-value IDENTICAL",
orig_run.len()
);
for (ts, b) in &loaded_run {
println!(" ts={ts:>14} bias={b:+.6}");
}
println!("PASS: author -> serialize -> load -> reproduce (C1 round-trip).");
}