feat(engine): firing policies A/B + the k-way ingestion merge
Cycle 0004: the reactive-semantics layer. The engine now merges multiple
timestamped sources into one chronological cycle stream (C3) and gates each
node's re-evaluation by a per-input firing policy (C5/C6) — both rails shipping
together, as the user required ("must sit right from the start").
- `Firing` (aura-core) — a per-input enum on `InputSpec`, where C8 places firing:
`Any` (mode A, fire-on-any-fresh + hold / as-of join) and `Barrier(u8)` (mode B,
all-fresh barrier / synchronizing join). Default-free; Sma/Sub declare
`Firing::Any`, so under a single source ticking every cycle their behavior is
unchanged (backward compatible — the 0003 fan-out/join DAG still emits
[None,None,None,2,2,2]).
- `SourceSpec` + the k-way merge (aura-engine) — `run` takes one
`(Timestamp, Scalar)` stream per source and merges them by (timestamp, source
index) (C4 ties by declaration order). One record = one cycle. This is the
permanent ingestion core; the Source trait + data-server IO (C3/C11) are a
later orthogonal cycle (the user scoped this cycle to the reactive semantics).
- Two read clocks, both load-bearing (resolves the 0003 audit's "0004 owns
cycle_id" with no write-only field): each push stamps the input slot with
`fresh_at` (the cycle_id — freshness epoch, C5: fresh iff fresh_at == cycle_id)
and `last_ts` (the data timestamp — barrier token, C6: a barrier group is
complete iff all members carry last_ts == T). The `fires()` predicate ORs the
groups; the ">=1 member fresh this cycle" clause is the once-per-timestamp
guard. Sample-and-hold falls out of the push model: a non-firing node pushes
nothing, so consumers keep seeing the held value via window[0].
Key design call, studied against RustAst (src/ast/rtl/streams/): the barrier
token is the data *timestamp*, not cycle_id. RustAst's
last_cycle_per_input.all(== cycle_id) synchronizes one source's fan-out but
cannot synchronize independent sources — under C4 four same-timestamp sources
are four different cycle_ids, so the cycle_id barrier never fires for C6's own
OHLC example. Substituting timestamp is the minimal faithful generalization
(fires both the diamond rejoin and the multi-source bar). Mode A and the k-way
merge are both new (RustAst had neither); RustAst's total_count push counter is
aura's already-built Column::run_count.
Both rails proven on identical sources, firing-only difference:
- mode A (AsOfSum): [None,None,120,130,140,240] — holds the slow input;
- mode B (BarrierSum): [None,None,120,None,None,240] — waits for timestamp
coincidence;
- mixed A+B (MixedSum): the OR-combine fires both when the barrier pair completes
(holding the as-of input) and when the as-of input ticks (holding the pair).
Plus determinism (C1, bit-identical re-run of each rail) and the three bootstrap
rejections re-expressed on SourceSpec.
Gates green (re-run by the orchestrator, not just the agent): cargo build/test
(30: aura-core 19 + aura-std 3 + aura-engine 8) / clippy --workspace
--all-targets -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell)
clean — the harness doc phrases RustAst's model without the literal tokens to
avoid self-match.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,26 @@
|
||||
//! `aura-engine` — the headless, UI-agnostic reactive SoA engine.
|
||||
//!
|
||||
//! Delivered in cycle 0003 — the harness and its deterministic single-source
|
||||
//! run loop:
|
||||
//! Delivered across cycles 0003-0004 — the harness and its deterministic run
|
||||
//! loop:
|
||||
//!
|
||||
//! - [`Harness`] — the closed root graph that runs (a flat node array + an index
|
||||
//! edge table, topologically ordered) and its deterministic `run` loop: one
|
||||
//! source driven through a wired DAG of nodes, cycle by cycle, each output
|
||||
//! forwarded into its consumers' input columns (C1/C4/C7/C8/C9).
|
||||
//! - [`Edge`] / [`Target`] — producer->consumer and source->consumer wiring.
|
||||
//! edge table, topologically ordered) and its deterministic `run` loop: a
|
||||
//! k-way merge of timestamped sources (C3/C4) driven through a wired DAG of
|
||||
//! nodes, cycle by cycle, with freshness-gated recompute and the two firing
|
||||
//! policies (C5/C6) deciding when each node re-evaluates and what it holds.
|
||||
//! - [`Edge`] / [`Target`] / [`SourceSpec`] — producer->consumer wiring,
|
||||
//! source->consumer wiring, and a declared source (its kind + target slots).
|
||||
//! - [`BootstrapError`] — wiring faults caught once, at bootstrap (kind
|
||||
//! mismatch, bad index, directed cycle).
|
||||
//!
|
||||
//! Still to come (subsequent cycles): freshness-gated recompute / sample-and-hold
|
||||
//! (C5), the ingestion boundary (k-way merge of timestamped sources, C3), the
|
||||
//! broker-independent position-event output and downstream broker nodes (C10),
|
||||
//! and the atomic sim unit `(topology + params + data-window + seed) -> metrics`
|
||||
//! that the sweep / optimize / walk-forward / Monte-Carlo axes orchestrate.
|
||||
//! Still to come (subsequent cycles): the `Source` trait + data-server ingestion
|
||||
//! and source-native time normalization (C3/C11), the broker-independent
|
||||
//! position-event output and downstream broker nodes (C10), and the atomic sim
|
||||
//! unit `(topology + params + data-window + seed) -> metrics` that the sweep /
|
||||
//! optimize / walk-forward / Monte-Carlo axes orchestrate.
|
||||
//!
|
||||
//! Visualization is never here: it is a downstream consumer node on the streams.
|
||||
|
||||
mod harness;
|
||||
|
||||
pub use harness::{BootstrapError, Edge, Harness, Target};
|
||||
pub use harness::{BootstrapError, Edge, Harness, SourceSpec, Target};
|
||||
|
||||
Reference in New Issue
Block a user