audit: cycle 0004 tidy — diamond-barrier test + ledger/glossary record-reality
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<Option<Scalar>>). 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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:** —
|
||||
|
||||
Reference in New Issue
Block a user