feat(0092): run-from-blueprint infrastructure — restructure + topology_hash + shared seam (Tasks 1-3)

Cycle-1 infrastructure for #165 (World/C21). Behaviour-preserving + workspace
green; the CLI arm + tests (Tasks 4-5) follow.

Task 1 — restructured stage1_r_graph into stage1_signal() (the serializable
signal leg: SMA-cross -> Bias, a `price` input-role + a `bias` output) +
wrap_stage1r(signal, ...) (broker/sinks/exec/cost around a nested signal), with
stage1_r_graph() = wrap_stage1r(stage1_signal(...)). DEVIATION from the plan's
"callers untouched" claim, verified behaviour-preserving: nesting the signal
prefixes its param-space names (stage1_signal.fast.length), which broke the
sweep's bare .axis("fast.length"); fixed exactly as the #137 stop-knob machinery
does — FAST_LENGTH_SUFFIX/SLOW_LENGTH_SUFFIX suffix-resolution + stage1_r_friendly_name
arms mapping the prefixed names back to the bare manifest names. All observable
behaviour (manifest names, member keys, metrics, traces) byte-identical; the flat
graph and every metric unchanged (full suite green, incl. sweep/walkforward/MC).

Task 2 — RunManifest.topology_hash: Option<String> (Tier-1 optional, serde
default/skip — old manifests byte-identical, #156), threaded None into all 24
literal sites across 7 crates. Also fixed the aura-registry RunManifestRead
read-mirror to carry topology_hash (it was hardcoding None on load) — else a
stored hash would silently drop on the registry round-trip once Task 3 stores a
real one.

Task 3 — the shared run_signal_stage1r seam (hash the signal, wrap, compile with
params, bootstrap, run, manifest with params from param_space + topology_hash) +
the sha256_hex topology_hash helper (sha2 in aura-cli, off the frozen engine,
invariant 8). run_stage1_r now carries its topology_hash too (every run becomes
self-identifying, #158). The seam is unwired until Task 4 (transient
#[allow(dead_code)], retired there); RED-first seam tests added.

Verified: cargo test --workspace green (51 suites, 0 failures); cargo clippy
--workspace --all-targets -D warnings clean.

refs #165
This commit is contained in:
2026-06-30 20:23:42 +02:00
parent e25ca15be4
commit ff4a1b3d4a
18 changed files with 454 additions and 69 deletions
+1
View File
@@ -1002,6 +1002,7 @@ mod tests {
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: summarize(&equity, &exposure),
}
+2
View File
@@ -244,6 +244,7 @@ mod tests {
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: summarize(&equity, &exposure),
}
@@ -269,6 +270,7 @@ mod tests {
broker: "t".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: RunMetrics {
total_pips: v,
+14
View File
@@ -51,6 +51,12 @@ pub struct RunManifest {
/// one-directional serde widening as `selection` (C14/C18).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instrument: Option<String>,
/// SHA256 (hex) of the canonical `blueprint_to_json` of the run's signal
/// topology — the #158 reproducibility anchor. `None` for a run not built
/// from a serialized blueprint and for every pre-0092 line. One-directional
/// serde widening (Tier-1, #156), identical idiom to `selection` / `instrument`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub topology_hash: Option<String>,
}
/// A run's full structured result: the descriptor plus the metrics it
@@ -273,6 +279,7 @@ mod tests {
let m = RunManifest {
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
seed: 0, broker: "b".into(), selection: None, instrument: None,
topology_hash: None,
};
assert!(!serde_json::to_string(&m).unwrap().contains("selection"));
}
@@ -290,6 +297,7 @@ mod tests {
let with = RunManifest {
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
seed: 0, broker: "b".into(), selection: None, instrument: Some("GER40".into()),
topology_hash: None,
};
let json = serde_json::to_string(&with).unwrap();
assert!(json.contains("\"instrument\":\"GER40\""), "json: {json}");
@@ -299,6 +307,7 @@ mod tests {
let without = RunManifest {
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
seed: 0, broker: "b".into(), selection: None, instrument: None,
topology_hash: None,
};
// skip_serializing_if omits the key when None -> existing manifest bytes unchanged (C23).
assert!(!serde_json::to_string(&without).unwrap().contains("instrument"));
@@ -316,6 +325,7 @@ mod tests {
let m = RunManifest {
commit: "c".into(), params: vec![], window: (Timestamp(0), Timestamp(0)),
seed: 0, broker: "b".into(), selection: Some(sel.clone()), instrument: None,
topology_hash: None,
};
let back: RunManifest = serde_json::from_str(&serde_json::to_string(&m).unwrap()).unwrap();
assert_eq!(back.selection, Some(sel));
@@ -455,6 +465,7 @@ mod tests {
broker: "sim-optimal(pip_size=0.0001)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics,
}
@@ -559,6 +570,7 @@ mod tests {
broker: "sim-optimal(pip_size=1.0)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: RunMetrics {
total_pips: 12.0,
@@ -589,6 +601,7 @@ mod tests {
broker: "sim-optimal(pip_size=1.0)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: RunMetrics { total_pips: 12.0, max_drawdown: 1.0, bias_sign_flips: 1, r: None },
};
@@ -611,6 +624,7 @@ mod tests {
broker: "sim-optimal(pip_size=1.0)".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: RunMetrics { total_pips: 12.0, max_drawdown: 1.0, bias_sign_flips: 1, r: None },
};
+1
View File
@@ -669,6 +669,7 @@ mod tests {
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: summarize(&equity, &exposure),
}
+1
View File
@@ -293,6 +293,7 @@ mod tests {
broker: "t".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: summarize(&[], &[]),
}
@@ -117,6 +117,7 @@ fn run_point(point: &[Cell]) -> RunReport {
broker: "test".to_string(),
selection: None,
instrument: None,
topology_hash: None,
},
metrics: summarize(&equity, &exposure),
}