audit: param-gang cycle close — drift-clean, mirror-walk interleave pinned

Architect review over 962b249..fc9cf23: clean. What holds: check_gangs is the
single structural predicate at all three minting boundaries (builder.rs:226,
construction.rs:396, blueprint_serde.rs:222 — the eager op arm is the
sanctioned by-identifier subset, connect/edge_kind_check precedent); identity
stays debug-name-blind per #171 (gang/member names blanked, (node,pos,kind)
survives); format version 1 + Tier-2 dormancy note consistent across code and
C24 ledger; no stale two-axis / 'separate #61' claims survive in docs or
comments; commit-body deviation claims verified true against the diff.

The one [low] finding — the new collect_params<->expansion_map mirror-walk
lockstep pair untested for gang members interleaved with other open params —
is closed here (fix disposition) with
ganged_interleave_shift_through_matches_member_bound_twin: LinComb.weights[1]
ganged with Bias.scale while weights[0] stays open earlier on the SAME node
and Sma.length follows the gang in walk order; equality asserted on
param_space order, wiring, and full run traces vs the member-bound twin.
Load-bearingness proven by an injected double-count bug failing the test.

Verified: aura-engine lib 265/0, clippy -D warnings clean.

refs #61
This commit is contained in:
2026-07-10 14:19:39 +02:00
parent fc9cf23b87
commit 29bf601558
+147 -1
View File
@@ -1154,7 +1154,7 @@ mod tests {
use crate::test_fixtures::{composite_sma_cross_harness, synthetic_prices};
use crate::{f64_field, summarize, ParamRange, RandomSpace, RunManifest, VecSource};
use aura_core::{Cell, Ctx, FieldSpec, Firing, NodeSchema, Timestamp};
use aura_std::{Bias, Ema, Recorder, SimBroker, Sma, Sub};
use aura_std::{Bias, Ema, LinComb, Recorder, SimBroker, Sma, Sub};
use std::sync::mpsc;
/// One knob fanning into two sibling open params passes the gate; the
@@ -1447,6 +1447,152 @@ mod tests {
assert!(!g_trace.is_empty());
}
/// Interleaved-gang fixture (architect [low] finding, cycle close): one gang
/// member sits at a NON-zero pos among its own node's open params
/// (`LinComb`'s `weights[1]`, with `weights[0]` still open and EARLIER in
/// walk order), the other member is a sibling node's sole param
/// (`Bias.scale`), and a THIRD node's independent param (`Sma.length`) walks
/// AFTER the gang — so `expansion_map`'s "un-ganged slot consumes the next
/// public index" branch must correctly shift past the gang in the MIDDLE of
/// the walk, not only at an edge of it.
fn ganged_interleave() -> Composite {
Composite::new(
"sig3",
vec![
LinComb::builder(2).into(),
Bias::builder().into(),
Sma::builder().named("c").into(),
],
vec![],
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 },
Target { node: 0, slot: 1 },
Target { node: 1, slot: 0 },
Target { node: 2, slot: 0 },
],
source: Some(ScalarKind::F64),
}],
vec![
OutField { node: 0, field: 0, name: "lincomb_out".into() },
OutField { node: 1, field: 0, name: "bias_out".into() },
OutField { node: 2, field: 0, name: "sma_out".into() },
],
)
.with_gangs(vec![Gang {
name: "shared".into(),
kind: ScalarKind::F64,
members: vec![
GangMember { node: 0, pos: 1, name: "weights[1]".into() },
GangMember { node: 1, pos: 0, name: "scale".into() },
],
}])
.expect("weights[1] (pos 1, not 0) ganged with bias.scale, both F64")
}
/// The un-ganged twin of [`ganged_interleave`]: identical topology, all four
/// params (`lincomb.weights[0]`, `lincomb.weights[1]`, `bias.scale`,
/// `c.length`) stay independently open.
fn unganged_interleave() -> Composite {
Composite::new(
"sig3",
vec![
LinComb::builder(2).into(),
Bias::builder().into(),
Sma::builder().named("c").into(),
],
vec![],
vec![Role {
name: "price".into(),
targets: vec![
Target { node: 0, slot: 0 },
Target { node: 0, slot: 1 },
Target { node: 1, slot: 0 },
Target { node: 2, slot: 0 },
],
source: Some(ScalarKind::F64),
}],
vec![
OutField { node: 0, field: 0, name: "lincomb_out".into() },
OutField { node: 1, field: 0, name: "bias_out".into() },
OutField { node: 2, field: 0, name: "sma_out".into() },
],
)
}
/// Wrap [`ganged_interleave`]/[`unganged_interleave`] under a recording root
/// and run the synthetic price fixture, taping all THREE composite outputs
/// (one per node): topology here is param-value-invariant (`edges` equality
/// alone would not catch a mis-shifted index landing a value on the wrong
/// node), so the trace over every affected node is what actually detects it.
fn run_triplet(bp: Composite, params: &[Scalar]) -> Vec<(Timestamp, Vec<Scalar>)> {
let (tx, rx) = mpsc::channel();
let root = Composite::new(
"h3",
vec![
BlueprintNode::Composite(bp),
Recorder::builder(
vec![ScalarKind::F64, ScalarKind::F64, ScalarKind::F64],
Firing::Any,
tx,
)
.into(),
],
vec![
Edge { from: 0, to: 1, slot: 0, from_field: 0 },
Edge { from: 0, to: 1, slot: 1, from_field: 1 },
Edge { from: 0, to: 1, slot: 2, from_field: 2 },
],
vec![Role {
name: "src".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![],
);
let prices = crate::test_fixtures::synthetic_prices();
let mut h = root.bootstrap_with_params(params.to_vec()).expect("bootstraps");
h.run(vec![Box::new(VecSource::new(prices))]);
rx.try_iter().collect()
}
/// THE property under test: `collect_params` (public `param_space` order) and
/// `expansion_map` (raw-slot -> public-index) mirror each other even when a
/// gang's first member is NOT its node's first open param and an independent
/// param on a LATER node follows the gang in walk order — the "shift-through"
/// branch (an un-ganged slot consuming the next public index around a gang)
/// fires in the MIDDLE of the walk, not only at an edge. Values are pairwise
/// distinct (7 / 4 / 3) so a mis-mapped index changes which node receives
/// which value, diverging the run trace. `c.length` stays small (3) so the
/// SMA warms up within the 7-row synthetic price fixture.
#[test]
fn ganged_interleave_shift_through_matches_member_bound_twin() {
let space = ganged_interleave().param_space();
let names: Vec<&str> = space.iter().map(|p| p.name.as_str()).collect();
let kinds: Vec<ScalarKind> = space.iter().map(|p| p.kind).collect();
assert_eq!(
names,
["lincomb.weights[0]", "shared", "c.length"],
"the gang publishes at its first member's walk position, flanked by \
an earlier independent param on the SAME node and a later \
independent param on a DIFFERENT node"
);
assert_eq!(kinds, [ScalarKind::F64, ScalarKind::F64, ScalarKind::I64]);
let ganged_point = [Scalar::f64(7.0), Scalar::f64(4.0), Scalar::i64(3)];
let twin_point = [Scalar::f64(7.0), Scalar::f64(4.0), Scalar::f64(4.0), Scalar::i64(3)];
let g = ganged_interleave().compile_with_params(&ganged_point).expect("ganged compile");
let t = unganged_interleave().compile_with_params(&twin_point).expect("twin compile");
assert_eq!(g.edges, t.edges, "identical wiring");
let g_trace = run_triplet(ganged_interleave(), &ganged_point);
let t_trace = run_triplet(unganged_interleave(), &twin_point);
assert_eq!(g_trace, t_trace, "ganged run diverged from the member-bound twin");
assert!(!g_trace.is_empty());
}
#[test]
fn edge_kind_check_accepts_match_and_rejects_mismatch() {
use super::edge_kind_check;