Files
Brummel 90e298d3b4 fieldtest: cycle-0008 — 5 examples, 6 findings
First fieldtest of the sum combinators. A standalone downstream-consumer crate
(fieldtests/cycle-0008-sum-combinators/) path-depends on the engine crates and
exercises the post-0008 surface from the public interface only (rustdoc + ledger
+ glossary + project-layout, never crates/*/src).

Primary axis empirically met: the north-star two-signal combine move now uses the
shipped Add (Add::new() dropped in exactly where the 0007 fixture hand-authored
an Add2) — the 0007 boilerplate is retired, end to end through SimBroker to a
deterministic recorded pip curve.

Findings: 0 bugs, 1 friction, 2 spec_gaps, 3 working.
  - working ×3: Add2 retired; LinComb weighted/variadic sums exact (<1e-12);
    warm-up barrier + empty-weights panic as documented.
  - spec_gap: Add/LinComb per-input firing policy (Firing::Any / mode-A as-of
    join) absent from the public surface — same class as the 0007 SimBroker gap.
  - spec_gap: a fired combinator emits one row per *cycle*, so a heterogeneous
    same-timestamp multi-source trace can carry >1 row per timestamp (correct per
    C4/C5, undocumented at this surface).
  - friction: nested consumer crate still needs the empty [workspace] table
    (carried from 0006/0007; #9 closed the docs half; folds into aura new).

Spec feeds the next plan as reference.
2026-06-04 18:21:57 +02:00

77 lines
3.1 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Example 5 (firing-policy probe) — combine two signals driven by two DIFFERENT
//! sources that update on different cycles.
//!
//! This is the genuinely-heterogeneous combine the north-star reaches for: a
//! fast price tick × a slower second feed, summed by LinComb. The interesting
//! question — invisible from the public surface — is LinComb's per-input firing
//! policy (C6): does a fired-this-cycle leg get combined with the OTHER leg's
//! *held* value (mode A, as-of join), or does LinComb only fire when both legs
//! are fresh in the same cycle (mode B barrier)?
//!
//! The rustdoc says only "Emits None until all inputs have a value" — it does
//! not state the firing policy. We feed two sources at interleaved timestamps
//! and RECORD what LinComb emits, to see which reading the shipped node took.
//!
//! source A (f64, fast) ─> LinComb slot 0 ┐
//! source B (f64, slow) ─> LinComb slot 1 ┴─> Recorder
mod recorder;
use std::cell::RefCell;
use std::rc::Rc;
use aura_core::ScalarKind;
use aura_core::{Scalar, Timestamp};
use aura_engine::{Edge, Harness, SourceSpec, Target};
use aura_std::LinComb;
use recorder::{Recorder, Tape};
fn main() {
const LINCOMB: usize = 0;
const REC: usize = 1;
let tape: Tape = Rc::new(RefCell::new(Vec::new()));
let nodes: Vec<Box<dyn aura_core::Node>> = vec![
Box::new(LinComb::new(vec![1.0, 1.0])),
Box::new(Recorder::new(tape.clone())),
];
// Two independent sources, both f64, each feeding one LinComb slot.
let sources = vec![
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: LINCOMB, slot: 0 }] },
SourceSpec { kind: ScalarKind::F64, targets: vec![Target { node: LINCOMB, slot: 1 }] },
];
let edges = vec![Edge { from: LINCOMB, to: REC, slot: 0, from_field: 0 }];
let mut harness = Harness::bootstrap(nodes, sources, edges).expect("bootstrap");
// Source A fires at t = 0,1,2,3,4,5; source B fires only at t = 1 and t = 4.
// (Distinct timestamps => distinct cycles, C4.)
let stream_a: Vec<(Timestamp, Scalar)> = (0..6)
.map(|i| (Timestamp(i), Scalar::F64(100.0 + i as f64)))
.collect();
let stream_b: Vec<(Timestamp, Scalar)> = vec![
(Timestamp(1), Scalar::F64(10.0)),
(Timestamp(4), Scalar::F64(20.0)),
];
harness.run(vec![stream_a, stream_b]);
let tape = tape.borrow();
println!("LinComb emitted {} rows from two interleaved sources:", tape.len());
println!(" (source A: a=100+t at t=0..5; source B: b=10 at t=1, b=20 at t=4)");
for (ts, v) in tape.iter() {
println!(" t={:>2} combined={:.4}", ts.0, v);
}
println!();
println!("Interpretation (public surface is SILENT on the firing policy):");
println!(" - mode A (as-of/hold): once both legs seen, every A-fresh cycle re-fires");
println!(" combining a's fresh value with b's HELD value.");
println!(" - mode B (barrier): LinComb fires only on cycles where BOTH legs are");
println!(" fresh at the same timestamp -> here only t=1 and t=4 would emit.");
println!(" The recorded rows above reveal which reading shipped.");
}