From 9ff90b5d807ce8d72a98dae2a27c9249a927426c Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 3 Jun 2026 14:27:00 +0200 Subject: [PATCH] =?UTF-8?q?audit:=20cycle=200004=20tidy=20=E2=80=94=20diam?= =?UTF-8?q?ond-barrier=20test=20+=20ledger/glossary=20record-reality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architect drift review over 02fe891..f37b2a3 (firing-policies-and-merge): drift_found, all medium/low, no contract violation. Invariants hold: C1 (the merge is a deterministic linear scan, strict < on (timestamp, source-index) gives C4 tie-break by declaration order; only per-call allocs), C3 (one merge at ingestion), C5/C6 (both rails proven with worked vectors; sample-and-hold falls out of the push model), C7 (type-erased edges, no dyn-Any/Rc/RefCell — purity grep clean). Resolved the 0003 forward-note: cycle_id is now a real read field (the freshness epoch, fresh_at == cycle_id), not write-only. Resolved [med] (test gap): the barrier's timestamp token was proven only for the multi-source same-timestamp case; C6's within-source diamond rejoin (one source, two unequal-latency paths rejoining at a barrier) was claimed by the spec but untested. Added `within_source_diamond_rejoin_barrier_fires` (source -> {SMA(2), SMA(4)} -> BarrierSum): once SMA(4) warms at cycle 4, both paths emit each cycle at the same timestamp and the barrier fires (15+13, 17+15, 19+17). The code was already correct — every push in a cycle stamps last_ts = that cycle's timestamp (source pushes and produced-edge forwards alike) — so this closes the coverage gap, not a bug. Resolved [med] (glossary record-reality): the run-count entry claimed it "drives freshness-gated recompute", now false — the engine's firing gate reads the per-wiring-slot cycle epoch (fresh_at), not Column::run_count. Sharpened the entry to record reality: run-count is the node-visible per-column push counter; fresh_at is the engine's per-slot freshness epoch. Not redundant (different levels); both are maintained consistently on every push. Resolved [low] (ledger silent on the barrier token): added a C6 "Realization (cycle 0004)" note recording the load-bearing decision — barrier token is the timestamp not cycle_id (forced by C4's same-timestamp-tie rule), firing tagged per input with Firing::{Any, Barrier(u8)}, a mode-A input being its own trivial group (so "per input group" and the per-input tag coincide). Carry-on forward notes (next-cycle owners, not fixes now): - [low] cycle_id is materialized internally (the freshness epoch) but not yet surfaced in run output (run returns Vec>). Its first observable consumer is a sink / clock node (C18) or a timestamped trace — materialize the surfacing then. - [low] all join fixtures declare lookback 1; a test guarding C8's "a node never reads beyond its declared lookback" for a depth>1 join is still owed (the substrate already enforces it via Window bounds, so this is confirmation, not a hole). Regression gate: profile regression list empty — architect is the sole gate, now clean (31 tests: aura-core 19 + aura-std 3 + aura-engine 9; clippy -D warnings clean; purity grep clean). Cycle 0004 is drift-clean. NOT a milestone close: the walking-skeleton milestone still needs its end-to-end fieldtest (ingest -> signal -> backtest -> position table -> broker -> pip-equity), of which this cycle delivers the firing/merge layer. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/aura-engine/src/harness.rs | 43 +++++++++++++++++++++++++++++++ docs/design/INDEX.md | 10 +++++++ docs/glossary.md | 2 +- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/aura-engine/src/harness.rs b/crates/aura-engine/src/harness.rs index 98a60eb..e2b5814 100644 --- a/crates/aura-engine/src/harness.rs +++ b/crates/aura-engine/src/harness.rs @@ -547,6 +547,49 @@ mod tests { assert_eq!(h2.run(vec![s0, s1]), out); // deterministic } + #[test] + fn within_source_diamond_rejoin_barrier_fires() { + // One source fans out through two unequal-latency producers (SMA(2), + // SMA(4)) that rejoin at a Barrier(0) node. Every push in a cycle carries + // that cycle's timestamp, so once both SMAs are warm and emit in the same + // cycle, both barrier inputs share the timestamp and the barrier fires — + // the within-source diamond rejoin the timestamp token handles (C6), + // distinct from the multi-source barrier above. + let build = || { + Harness::bootstrap( + vec![Box::new(Sma::new(2)), Box::new(Sma::new(4)), Box::new(BarrierSum)], + vec![SourceSpec { + kind: ScalarKind::F64, + targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }], + }], + vec![Edge { from: 0, to: 2, slot: 0 }, Edge { from: 1, to: 2, slot: 1 }], + 2, + ) + .expect("valid DAG") + }; + let prices = f64_stream(&[(1, 10.0), (2, 12.0), (3, 14.0), (4, 16.0), (5, 18.0), (6, 20.0)]); + + let mut h = build(); + let out = h.run(vec![prices.clone()]); + // SMA(4) warms at cycle 4; from then both paths emit each cycle at the same + // timestamp, so the barrier fires: SMA(2)+SMA(4) = 15+13, 17+15, 19+17. + assert_eq!( + out, + vec![ + None, + None, + None, + Some(Scalar::F64(28.0)), + Some(Scalar::F64(32.0)), + Some(Scalar::F64(36.0)), + ] + ); + + // determinism (C1): a second identical run is bit-identical + let mut h2 = build(); + assert_eq!(h2.run(vec![prices]), out); + } + #[test] fn mixed_a_and_b_or_combine_on_one_node() { // in0,in1 = barrier group 0 (sources 0,1); in2 = as-of (source 2). diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index 214ffdb..b0775e2 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -154,6 +154,16 @@ A single node may mix an A input and a B group. **Forbids.** A single global firing mode; forcing per-node-only granularity. **Why.** Both are genuinely needed; RustAst implemented only B. Per-input-group granularity is required by the OHLC-plus-bias case where one node needs both. +**Realization (cycle 0004).** Firing is tagged per input — `Firing::{Any, +Barrier(u8)}` on `InputSpec` — and inputs sharing a `Barrier` id form a group; a +mode-A input is its own trivial group, so "per input group" and the per-input tag +coincide. The barrier's synchronization token is the cycle **timestamp**, not the +`cycle_id`: under C4 four same-timestamp sources are four distinct cycles, so +RustAst's `cycle_id`-equality barrier could never fire across them. A group fires +when every member's last push carries the current cycle's timestamp, guarded by +"≥1 member fresh this cycle" (so a group completed earlier does not re-fire). This +fires both the multi-source bar and the within-source diamond rejoin (every push +in a cycle carries that cycle's timestamp). ### C7 — Four scalar base types, streamed as SoA **Guarantee.** Only `i64`, `f64`, `bool`, `timestamp` (newtype over i64, diff --git a/docs/glossary.md b/docs/glossary.md index 5b491ca..3718a85 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -133,7 +133,7 @@ The aura-native, per-project store of one record per run — a manifest + metric ### run-count **Avoid:** total_count -The per-series counter used to detect whether an input is fresh this cycle; it drives freshness-gated recompute. ("total_count" is RustAst's name for the same counter.) +The per-series push counter on every `Column` — bumped on each push, never moved by a read; the node-visible freshness primitive. The engine's per-cycle firing gate (C5/C6) reads a per-wiring-slot cycle epoch (`fresh_at == cycle_id`, the freshness epoch C4's cycle_id materializes as) rather than this counter, which remains the column-level count a node may read for its own logic. ("total_count" is RustAst's name for the same counter.) ### scalar base types **Avoid:** —