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.
Ships the missing sum combinator(s) in aura-std so the north-star combine move
(C10) is expressible from shipped blocks. Two nodes: Add (parameterless two-input
f64 sum, companion to Sub) and LinComb { weights } (N-input weighted sum, weights
as tunable params + arity, C8/C12). Both withhold output until all inputs are
present (no implicit cold-leg 0.0).
Resolves the cycle-0007 fieldtest [friction]: the consumer had to hand-author a
project-local Add2 to combine two signals.
refs #11
A project crate that path-depends on the engine and lives under the engine
repo (common during authoring) is rejected by cargo unless its Cargo.toml
carries an empty [workspace] table. Surfaced repeatedly by the cycle-0006 and
cycle-0007 fieldtests as a non-obvious onboarding one-liner.
Document it on the consumer-facing surface (project-layout, the "A project
repo" section) so a nested-project author can predict and fix it. The aura new
scaffolder will emit the line once it exists (folds into the CLI work); the
docs capture closes the actionable half now.
closes#9
First fieldtest of the signal-quality loop. A standalone downstream-consumer crate
(fieldtests/cycle-0007-signal-quality/) path-depends on the engine crates and
exercises the post-0007 surface from the public interface only (rustdoc + specs +
ledger + glossary, never crates/*/src):
1 single-signal quality backtest, end to end (SMA-cross -> Exposure -> SimBroker
-> Recorder pip curve)
2 Exposure clamp + sizing (hard ±1 saturation, sign preserved)
3 sim-optimal integration: short-on-falling pays positive pips; pip_size=2 curve
exactly halves pip_size=1
4 north-star combine-two-signals (two MA-cross spreads summed into one exposure)
Findings: 3 working (carry-on), 2 spec_gap, 2 friction.
- spec_gap: SimBroker firing policy + cold-exposure->0.0 warm-up emission shape
not on the public surface (only in the feat commit body).
- spec_gap: SimBroker input slot order (0=exposure, 1=price) only in C10 prose /
commit body; a swapped wiring is not caught at bootstrap (both f64).
- friction: the north-star "combine two signals" move needs a sum/LinComb
combinator aura-std does not ship (hand-authored Add2 in the fixture).
- friction: standalone consumer crate still needs the empty [workspace] table
(carried from cycle-0006; tracked as #9).
Spec at docs/specs/fieldtest-0007-signal-quality.md feeds the next plan.
refs #4#5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architect drift review (f000373..HEAD) found one item: the C10 reframe was
amended in framing but carried no "Realization (cycle 0007)" note recording what
actually shipped — the spec's own ship gate required it (C8/C22 carry their
realization notes; C10 did not). Added: the signal-quality half realized as
Exposure { scale } + SimBroker { pip_size } on the unchanged, domain-free engine,
with the position-management half explicitly confirmed deferred.
Everything else holds: the reframe is propagated consistently across CLAUDE.md
invariant #7, the seven touched glossary entries + the new exposure-stream entry,
and project-layout.md (no stale "position table is the strategy output"
survivor); the engine stays domain-free (the only RefCell hit in aura-engine/src
is a #[cfg(test)] doc comment); C1 determinism and C2 causality are each pinned by
a behaviour test asserting observable output, not internals.
No regression scripts configured (commands.regression empty) — that gate is a
no-op; architect is the gate. Cycle 0007 is drift-clean.
refs #4#5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bite-sized, placeholder-free plan for spec 0007: Task 1 ships the Exposure node
(clamp signal/scale to [-1,+1]) in aura-std; Task 2 the SimBroker node (causal
lagged exposure*return -> cumulative pips); Task 3 two aura-engine end-to-end
tests (pip-equity recording with a hand-computed curve + determinism); Task 4 the
full workspace gates (test/clippy/purity grep). Each node's lib.rs module
registration is folded into its own task so every per-task compile gate is
satisfiable.
refs #4#5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cycle 0007 bundles Gitea #4 (exposure node) and #5 (sim-optimal broker) — the two
halves of the signal-quality loop, the realization of the C10 reframe. Ships two
aura-std nodes: Exposure { scale } (clamp(signal/scale, -1, +1)) and
SimBroker { pip_size } (causal prev_exposure * dprice / pip_size integration, no
look-ahead). Engine unchanged — pure node authoring on the frozen substrate.
grounding-check: PASS (all substrate assumptions ratified by green tests).
refs #4#5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DAG expresses exactly one state at time t and a node emits at most one record
per eval (C8), so a *sequence* of position events — where one decision instant
(a stop-and-reverse) needs a close AND an open at the same event_ts — cannot be
the DAG's per-cycle output without violating C8. The state the DAG can express
faithfully is the desired exposure (one value per cycle); the buy/sell/close
events are its first difference, a derived consequence.
C10 is reframed accordingly: the strategy's primary, backtestable output is an
intent/exposure stream (one signed, bounded f64 in [-1,+1] per cycle). Signal
quality is measured by the sim-optimal broker integrating exposure*return into a
synthetic pip-equity curve. The broker-independent position-event table survives
as a decoupled, derivable, downstream position-management layer (computed table,
not a per-eval output) feeding realistic broker nodes for viability/deploy — no
longer the DAG output nor the signal-quality measure.
Touches the ledger contract (INDEX.md C10 + provenance/milestone/C20 ancillary),
the always-loaded summary (CLAUDE.md invariant #7), the glossary (broker, equity
stream, position table, realistic broker, signal, sim-optimal broker, strategy
reframed + new exposure-stream entry), and the north-star layout doc. Sealed
specs/plans (0001-0006) left as historical record.
refs #4#5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the spec_gap from the cycle-0006 fieldtest
(docs/specs/fieldtest-0006-substrate.md): the public surface never stated that
an empty `output` is THE pure-consumer (sink) declaration, nor defined what a
`Some` return paired with an empty output means.
The behaviour was already principled and defended in the engine — this only
documents it (no code change):
- `output: vec![]` is the pure-consumer declaration; there is no separate
Sink type/trait/marker.
- `eval` must return a row whose width equals `schema.output.len()`, so a pure
consumer returns `None` or a zero-width `Some(&[])`; the run loop
debug-asserts the width (harness.rs).
- field-wise wiring resolves `Edge::from_field` against the producer's output
at bootstrap, so no edge can bind a field of a zero-output node (it fails
with `BadIndex`) — a sink is structurally unwireable as an in-graph
producer; its only output is the out-of-graph side effect.
Ledger C8 gains an "Encoding & return contract" clause; aura-core node.rs
rustdoc (module, NodeSchema, Node trait) updated from the pre-0006 "1..K,
producer or transformer" wording to the three-role (producer/consumer/both)
contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First fieldtest of the project. A standalone downstream-consumer crate
(fieldtests/cycle-0006-substrate/) path-depends on the engine crates and
exercises the post-0006 substrate from the public interface only (rustdoc +
specs + ledger, never crates/ source):
1 custom recording node via the Node contract + Ctx::now()
2 fan-out/fan-in DAG, 3-arg bootstrap, run() -> ()
3 two interior streams recorded from one run (the cycle headline)
4 byte-identical recorded output across two runs (C1 determinism)
5 mis-wired recording edge rejected (KindMismatch) at bootstrap
Findings: 3 working (carry-on), 2 friction, 1 spec_gap.
- friction: a nested standalone consumer crate fights the workspace
resolver (needs an empty [workspace] table — Cargo's own hint).
- friction: recorder boilerplate is hand-rewritten per author (C9
deliberately ships no library sink).
- spec_gap: the public surface never states output: vec![] is THE sink
declaration, nor defines a Some return paired with empty output.
Spec at docs/specs/fieldtest-0006-substrate.md feeds the 0007 plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architect drift review over 57310b8..HEAD: clean — no drift, no debt.
The C1/C7 boundary is correctly drawn (recorded values escape only via a
node-held mpsc::Sender with no out-edge; nothing recorded re-enters the
graph; the engine surface shrank). C2 holds (Ctx::now() is the present
cycle's ts). The C8/C22 cycle-0006 realization notes accurately describe
what shipped — no overclaim. Test fixtures stayed in #[cfg(test)] (C9).
Regression set is empty (profile) — architect is the sole gate; green.
Record-reality: the glossary 'sink' entry defined a sink as 'a pure
consumer node with no output', which the shipped TapForward node (records
AND forwards an output) now contradicts. Updated to reality: a sink is a
recording *role*, not a type — a node may be a pure consumer or record
and produce in the same eval (C8 'both'). No invention; the entry now
matches the substrate this cycle landed.
Replace the engine's single observe: usize recording affordance with
recording-by-node, so one run records many streams. A recording node
reads its typed input windows + ctx.now() in eval and pushes the record
to a destination it holds as a field (a channel, a chart handle) — an
out-of-graph side effect. There is no Sink type, trait, or engine flag:
a pure-consumer node returns None, and a node may record AND return a
forwarded output in the same eval (the C8 "both" case). In-graph routing
stays engine-owned data (the edge table); the escape out of the graph is
the node's own side effect, and that boundary is the determinism /
graph-as-data boundary.
Engine surface shrinks: Ctx gains now: Timestamp + now() (C2-causal, the
present cycle's timestamp); Harness loses the observe field, its
observe >= n bootstrap check, and the per-cycle observed-row collection;
bootstrap drops its 4th param; run returns (). Recorded streams are now
sparse and timestamped (a record per fired cycle) instead of the dense
Vec<Option<row>>.
The Ctx::new signature change touched 9 call sites across three crates
(not the 3 the spec estimated) — aura-std's node tests and the engine
run loop were threaded too. The engine test suite migrated to a
test-local Recorder fixture whose read-back is an mpsc channel, never
Rc/RefCell, keeping aura-engine/src purity-clean (C7). Eight new proof
tests cover the multi-sink headline, producer-and-sink, mixed-kind
recording, all-fields tap, both recorder firing modes, determinism, and
recorder-edge kind rejection. C8/C22 gain cycle-0006 realization notes.
Gates: workspace test 45 green (core 20, std 3, engine 22), clippy
-D warnings clean, purity grep clean (only a comment names Rc/RefCell).
closes#2
Six tasks: (1) aura-core Ctx::now(); (2) thread the Ctx::new signature
change through aura-std's five test call sites; (3) aura-engine
production shrink (remove observe, run -> (), thread the run-loop Ctx);
(4) migrate the engine test suite to a test-local Recorder fixture +
drained channel (the finish-threading task — the test module is one
compile unit, so it restores the full cargo test); (5) eight new
node-recording proof tests; (6) ledger C8/C22 realization notes + the
ship gate (workspace test, clippy, purity grep).
Recon found the spec undercounts Ctx::new call sites (9, not 3): the
aura-std and aura-engine run-loop sites also break on the signature
change and are threaded in tasks 2-3.
refs #2
A sink is a role, not a type: recording is an out-of-graph side effect a
node performs in eval, pushing to a destination it holds as a field. No
Sink type, no trait, no engine sink-awareness. The engine stays the
router for in-graph routing (edge table = graph-as-data); the node-side
push is the only out-of-graph escape, and that boundary is the
determinism boundary. A node may be producer and sink at once (C8 both).
Engine surface shrinks: Ctx gains now(); the observe index (field,
bounds check, per-cycle collection) is removed; bootstrap drops its 4th
param; run returns (). Recording streams are sparse and timestamped
(only fired cycles), replacing the dense Vec<Option<row>>.
Realization notes land on C8 and C22; no new contract.
refs #2
Architect drift review over 9ff90b5..5228542 (node-output-record): drift_found,
all low-severity, NO contract violation. The load-bearing invariants hold:
- C7 forbids intact — the reused `scratch` buffer (cleared+extended, warm after
cycle 1) and node-owned `[Scalar; K]` output buffers give zero per-cycle heap
alloc on the inter-node forward path; no fifth scalar type, no dyn-Any, no
heterogeneous buffer (the TwoField mixed-kind producer is rejected at bootstrap,
never streamed). Purity grep clean. The sharpened C7 guarantee text matches the
shipped per-field kind check + the Edge::from_field scatter.
- C8 revised text faithful — one output port (out_len), record of 1..K columns
(Vec<FieldSpec>), field-wise binding only (N edges, no whole-record bind), scalar
= degenerate K=1. No internal contradiction with C7/C9/C10.
- C6 untouched, verified against the run loop — one eval per node per cycle, every
out-edge stamps SlotState { fresh_at: cycle_id, last_ts: ts } with the same ts,
so the K fields of one record are co-fresh by construction. fires() unchanged.
- C3/C4/C5/C9 unperturbed (diff touches only node/edge plumbing); C2 lookback
enforcement unperturbed (column.rs/ctx.rs/any.rs not in the diff).
- C10 coherence confirmed — a position event is a 5-field record
(event_ts/action/position_id/instrument_id/volume), exactly the K-field
base-column record C8/C7 now sanction; the decision/sizing node is an ordinary
K=5 producer, the broker an N-edge field-wise consumer. No contract gap blocks
the next consumer; C10 correctly out of scope this cycle.
Resolved [low] (the one confirmed drift) — glossary record-reality. The ledger
C7/C8 moved this cycle but the glossary lagged. Fixed three entries to the
post-0005 reality (this is audit's autonomous glossary write authority,
record-reality only): `node` ("at most one output" -> "at most one output port; a
producer's output is a record of 1..K base-scalar columns, scalar = degenerate
K=1"), `composite` (now connects the multi-column stream to the node-output model
— the record a producer's eval returns, bound field-wise), `edge` (forwards "one
field (from_field) of a producer's eval output record", per-field kind check).
The architect named `node` + `composite`; `edge` is an orchestrator-observed
addition in the same record-reality pass (post-from_field it read "forwards one
node's eval output", now imprecise for K>1).
Regression gate: profile regression list empty — architect is the sole gate, now
clean (36 tests: aura-core 19 + aura-std 3 + aura-engine 14; clippy -D warnings
clean; purity grep clean). Cycle 0005 is drift-clean.
Carry-on forward note (next-cycle owner, not a fix now):
- [low] cycle 0004's depth>1 join-lookback guard test remains merely OWED, not a
fresh hole — the substrate enforces it via Window bounds (column.rs/ctx.rs
untouched this cycle) and 0005 added no depth>1 join. Co-fresh borrowed-row
routing does not interact with per-input lookback. A confirmation test is still
owed when a depth>1 join lands.
NOT a milestone close: the walking-skeleton milestone still needs its end-to-end
fieldtest (ingest -> signal -> backtest -> position table -> broker -> pip-equity).
This cycle delivers the record-output substrate that the trading half (C10) now
builds on.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cycle 0005 (BLOCKER #1): a node's output generalizes from one scalar to a record
of K >= 1 named base-scalar columns, with a scalar being the degenerate K = 1
case. A node keeps exactly one output port; its payload is now an ordered bundle
of base columns (a composite stream, C7). This unblocks every multi-column
producer the engine needs next -- OHLCV bars and, later, the C10 position-event
table -- none of which could exist while a node emitted at most one column.
Revises C8, sharpens C7 in the design ledger.
Contract (aura-core):
- `NodeSchema.output: ScalarKind` -> `Vec<FieldSpec>` (FieldSpec = { name:
&'static str, kind }; the field position is what an Edge binds, the name is
metadata for later sinks/playground, C18).
- `Node::eval -> Option<Scalar>` -> `-> Option<&[Scalar]>`: a node fills a buffer
it owns (sized once at construction) and returns a borrowed K-field row; `None`
still means filter/not-warmed. This is the C7-faithful representation chosen
with the user: zero per-cycle heap allocation on the forward path (rejected:
Option<Vec<Scalar>> allocates per fire; an inline fixed [Scalar; N] bakes a
width cap; engine-owned output columns invert the eval model). Scalar output is
the degenerate 1-field record -- no separate scalar path.
Field-wise binding (aura-engine):
- `Edge` gains `from_field: usize` -- which producer column this edge forwards.
Consuming a whole record is N edges; there is no "bind whole record" mechanism.
- bootstrap kind-checks per field (`from.output.get(from_field)` -> BadIndex if
out of range; `field.kind != slot.kind` -> KindMismatch).
- the run loop copies a producer's returned row into one reused scratch buffer
(resolving the borrow; no per-cycle alloc once warm) and scatters
scratch[from_field] into each out-edge slot. `NodeBox.out_len` (set at
bootstrap) backs a debug_assert on the returned row width. `run` returns
`Vec<Option<Vec<Scalar>>>` (the observed node's full row per cycle -- a
materialization-surface alloc, not the inter-node hot path).
- the K fields of one record are co-fresh by construction (one eval, one
timestamp), so C6 is untouched.
aura-std: Sma/Sub migrate to the 1-field degenerate case (hold a [Scalar; 1]
buffer; behaviour unchanged). Sub drops #[derive(Default)] (Scalar has no Default)
for a manual Default.
Proof (aura-engine tests, +5): an Ohlcv 5-field bundler fed by five
timestamp-aligned barrier sources emits one complete bar per timestamp
(ohlcv_bundles_five_field_record); a downstream Sub binds high(1)-low(2)
field-wise, observed end-to-end + a determinism re-run
(edge_binds_single_field_high_minus_low); a second consumer binds close(3)-open(0)
on the same record (distinct_edges_read_distinct_fields); must-fail:
bootstrap_rejects_from_field_out_of_range (BadIndex),
bootstrap_rejects_per_field_kind_mismatch (a TwoField [f64,i64] producer, the i64
field into an f64 slot). All prior 0003/0004 tests adapted to from_field + the Vec
return, expected vectors unchanged (scalar = 1-field record).
Gates re-run by the orchestrator (not just the agent): cargo build/test
--workspace --all-targets (36: aura-core 19 + aura-std 3 + aura-engine 14, 0
failed) / clippy --workspace --all-targets -D warnings clean; surface-purity grep
(no dyn-Any / Rc / RefCell) clean. Ledger C8/C7 edits verified
(Option<&[Scalar]> present, "one series per node" gone, cycle-0005 realization
note added). The glossary composite/node record-reality pass is the audit-time
follow-up.
closes#1
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four tasks, ordered by compile-gate discipline (each leaves a coherent gate
satisfiable):
1. aura-core contract (FieldSpec, NodeSchema.output -> Vec<FieldSpec>, Node::eval
-> Option<&[Scalar]>) + aura-std Sma/Sub degenerate migration, threaded in one
task. The signature change breaks aura-engine, so the gate is SCOPED to
`-p aura-core -p aura-std` (engine is knowingly broken until task 3).
2. aura-engine production: Edge.from_field, NodeBox.out_len, per-field bootstrap
kind/range check, reused-scratch field-indexed run forward, run ->
Vec<Option<Vec<Scalar>>>. Breaks the in-module tests, so the gate is a PARTIAL
`cargo build -p aura-engine --lib` + clippy --lib only.
3. aura-engine tests: migrate the 3 firing fixtures to the slice return; add the
Ohlcv 5-field bundler + TwoField mixed-kind fixture; the field-binding proof
(high-low, close-open), K>1 output, determinism, and must-fail (from_field OOB
-> BadIndex, per-field kind mismatch -> KindMismatch); adapt all prior
0003/0004 tests to from_field + the Vec return. Full workspace gate + purity
grep.
4. design ledger: C8 revision (one output port carrying a record) + C7 sharpening
(composite-bundle is the node-output model). Docs-only; glossary record-reality
is audit-time, not this plan.
Orchestrator decisions baked in: the spec's illustrative `schemas_out_len[nidx]`
is committed as a `NodeBox.out_len` field (set at bootstrap) read into a run-loop
local; the firing fixtures hold a `[Scalar; 1]` buffer constructed at each call
site; Sub drops `#[derive(Default)]` (Scalar has no Default) for a manual Default
that keeps clippy's new_without_default quiet; the engine lib.rs doc is left
untouched (no single-scalar phrase there -- only harness.rs doc + Edge doc move).
plan-recon confirmed every spec-named path exists and the compile-driven site set
(6 output: literals, 6 fn eval, 6 Edge literals, 11 .run callers); from_field /
FieldSpec / Ohlcv are net-new. No content-pin/golden-file twin-miss risk.
refs #1
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cycle 0005 (BLOCKER, milestone walking-skeleton): generalize a node's output
from a single scalar to a record of base-scalar columns (K >= 1), with a scalar
being the degenerate K = 1 case. A node keeps exactly one output port; that
port's payload becomes an ordered list of named, typed base columns (a composite
stream, C7). This unblocks every multi-column producer the engine needs next --
OHLCV bars and, later, the C10 position-event table -- none of which can exist
while a node emits at most one column. This revises C8 and sharpens C7.
The load-bearing design call, made with the user: the eval output representation
under C7's "no per-event heap allocation". eval becomes
`-> Option<&[Scalar]>` -- the node fills a buffer it owns (sized once at
construction) and returns a borrowed K-field row; the engine copies it into one
reused scratch row (resolving the borrow, zero per-cycle alloc once warm) and
scatters each field to its out-edges. `None` still means filter/not-warmed.
Rejected alternatives, on substance: returning `Option<Vec<Scalar>>` allocates
per fire (violates C7's forbids); an inline fixed `[Scalar; N]` record bakes a
magic width cap; engine-owned output columns invert the eval model (write-into-out
+ a fired flag) and split "fired" from "produced" for an output-lookback benefit
we do not need (history is bound as an input, not read from a node's own past).
The borrowed row is the minimal faithful generalization of today's
`Some(Scalar)` push route, K-wide.
Binding stays field-wise only: `Edge` gains `from_field`; consuming a whole
record is N edges; no "bind whole record" mechanism. The K fields of one record
are co-fresh by construction (one eval, one timestamp), so C6 is untouched. The
record is structural bundling over the four base types (C7 intact) -- no fifth
scalar type, no dyn Any, no heterogeneous buffer.
Scope is the substrate only: the contract change, the Sma/Sub degenerate
migration, the engine from_field wiring, and a neutral OHLCV proof (a 5-field
bundler fed by five timestamp-aligned barrier sources + a downstream Sub binding
high - low field-wise, plus must-fail cases: from_field out of range -> BadIndex,
per-field kind mismatch -> KindMismatch). The C10 position table and broker nodes
are out of scope -- later cycles that consume this capability.
Self-review clean; grounding-check PASS (14 load-bearing "before"-state
assumptions ratified by named green tests; C6/C7/C8 match the ledger as written;
parse-every-block a documented no-op -- profile declares no spec_validation).
refs #1
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Three tasks, ordered by compile-gate discipline (each leaves a coherent build
gate satisfiable):
1. aura-core Firing enum + InputSpec.firing, threaded through aura-std Sma/Sub
in the same task (the field addition breaks both callers) -> full workspace
gate green.
2. aura-engine production rewrite: SourceSpec, SlotState (fresh_at + last_ts, the
two read clocks), NodeBox/Harness fields, the k-way merge run loop, and the
fires() firing predicate. The bootstrap/run signature change breaks the
in-module tests, so this task ends on a PARTIAL gate (cargo build -p
aura-engine --lib, which excludes #[cfg(test)]).
3. aura-engine tests: rewrite the 5 cycle-0003 tests onto the multi-source API
and add the firing rails -- mode A as-of hold, mode B barrier wait, mixed A+B
OR-combine, each with worked expected vectors -> full workspace gate.
Orchestrator decisions baked in: run length-mismatch is a caller bug -> assert_eq!
(not a typed error, not a silent no-op); historical specs/plans (0002/0003) pin
pre-cycle disk state and are left frozen; the test module gets an explicit
`use aura_core::{InputSpec, NodeSchema}` (harness.rs production never names those
types, so `use super::*` does not bring them, and putting them in the top-level
use would be an unused-import under -D warnings); the harness doc keeps RustAst's
model phrased without the literal Rc/RefCell tokens so the purity grep does not
self-match.
Grounding-check on the parent spec PASS; plan-recon confirmed every spec-named
path exists and the "before" shapes match disk.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fourth walking-skeleton cycle: the reactive-semantics layer — both firing
policies (C6), the k-way ingestion merge (C3), the monotonic cycle_id clock
(C4), and freshness-gated recompute / sample-and-hold (C5). This is the first
point where the engine departs from RustAst rather than sharpening it.
Both rails ship together (user directive: "must sit right from the start"):
- A = fire-on-any-fresh + hold (as-of join);
- B = all-fresh barrier (synchronizing join), accumulating across cycles.
Per-input-group, mixable on one node. Encoded as Firing::{Any, Barrier(u8)} on
InputSpec (where C8 places firing); default Any keeps Sma/Sub unchanged.
Key design resolution (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).
Two read clocks, both load-bearing (resolves the 0003 audit's "0004 owns
cycle_id" with no write-only field): cycle_id is the freshness epoch (C5 reads
it via per-slot fresh_at == cycle_id); timestamp is the barrier token (C6 reads
it via last_ts == T). Sample-and-hold falls out of the existing push model — a
non-firing node pushes nothing, so consumers see the held value via window[0].
Scope (user-chosen): reactive semantics only — in-memory (timestamp, value)
sources; the merge is the permanent ingestion core, the Source trait (C11) +
data-server IO are a later orthogonal cycle. bootstrap/run/InputSpec signatures
change (the 0003 tests re-express on the multi-source API).
Headline test pair (identical sources, firing-only difference): A emits
[None,None,120,130,140,240] (holds); B emits [None,None,120,None,None,240]
(waits for timestamp coincidence). Plus determinism (C1), mixed A+B, the 0003
fan-out/join compat baseline, and the bootstrap rejections.
Grounding-check PASS: 10 load-bearing substrate assumptions ratified by green
tests; the spec's "before" shapes match disk exactly.
refs walking-skeleton
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Architect drift review over 71c8f6e..dd5c3fa (deterministic-sim-loop).
Invariants hold: C1 (single-threaded, bit-identical re-run, alloc-free per-cycle
ring path), C7 (AnyColumn type-erased edges, Box<dyn Node> is the node object not
a dyn-Any payload, kinds checked once at bootstrap), C8/C9 (DAG wired, Kahn
rejects cycles, None forwards nothing = sample-and-hold seed). Column::run_count
already exists, so the C5 freshness primitive lives at the substrate — the
deferral is honest, not a hole.
Resolved [high]: the runtime type was named `Sim`, colliding with the glossary's
load-bearing distinction — `sim` is the disjoint *executable unit / parallelism*
framing, whereas the thing built here (a closed root graph of source + nodes +
observe under a clock) is by C20 / the glossary a **harness**. Renamed
`Sim` -> `Harness`, `sim.rs` -> `harness.rs`, and the module/exports/doc to match,
before any other code takes a dependency on the wrong term. Gates re-run green
(26 tests, clippy -D warnings, purity grep).
Resolved [med] (glossary gap): added an `edge` entry anchoring the new wiring
vocabulary (Edge / source target) to the glossary — record-reality per the boss
glossary-write authority.
Carry-on with forward notes (next-cycle owners, not fixes now):
- [med] C4 monotonic cycle_id has no realization in the runtime type yet (the
cycle clock is the record iteration; the explicit counter's first reader is
freshness). Cycle 0004 owns materializing it together with C5.
- [low] Sub declares lookback 1; once depth>1 join nodes exist, add a test
guarding C8's "a node never reads beyond its declared lookback".
Regression gate: profile regression list empty — architect is the sole gate,
now clean. Next direction: freshness-gated recompute + a second source (C5/C3).
Three tasks, each leaving the workspace green:
1. Sub 2-input difference node in aura-std (+ test).
2. Sim in aura-engine: Edge/Target/BootstrapError + bootstrap (schema-sizing,
edge kind-check, Kahn topo-sort, cycle/bad-index/kind-mismatch rejection) +
the forwarding run loop (disjoint &mut-self destructure, no per-cycle alloc),
with 5 tests (chain, fan-out/join + determinism, cycle, kind-mismatch,
bad-index). aura-engine gains a test-only dev-dependency on aura-std.
3. Workspace gate: build / test (26) / clippy -D warnings / purity grep.
Orchestrator decisions baked in: observe captured at eval-time (no output
column); Sim stores only fields run reads (no write-only cycle_id/source_kind
that would trip `field never read`); the sim.rs doc avoids the literal
Rc/RefCell tokens so the purity grep does not self-match.
Third walking-skeleton cycle: aura-engine's first real content — a Sim that
runs a wired DAG of nodes deterministically, cycle by cycle. First point where
authored nodes execute against data, not just under a hand-fed Ctx.
Engine shape (contract-forced): a flat Vec<NodeBox> + an index edge table,
topologically ordered (Kahn), owned &mut by the loop — the flat, monomorphized
sharpening of RustAst's Rc<RefCell<dyn Observer>> push graph (C1/C7). Each node
owns its input columns (cycle-0002 shape), so Ctx is unchanged and the per-cycle
path allocates nothing. Wiring is by forwarding: a producer's eval output is
pushed into its consumers' input columns; a None forwards nothing (the seed of
sample-and-hold).
Bootstrap generalizes C7's "type check paid once at wiring" to the whole
topology: size input columns from each node's schema, kind-check every edge,
reject directed cycles (BootstrapError). A worked 2-input Sub node lands in
aura-std so the loop is proven on a real fan-out+join DAG
(source -> {SMA(2), SMA(4)} -> Sub -> observe), with a determinism assertion (C1).
Deliberately deferred (recorded as decisions): freshness gating / sample-and-hold
(C5 -> cycle 0004, only testable once a second source at a different rate exists);
the Source trait + data-server ingestion + k-way merge (C3/C11); the ergonomic
builder API (C19); the real sink + run registry (C18/C22); cross-sim parallelism.
Grounding-check PASS: all assumptions about the 0001 substrate and 0002 node
contract ratified by green tests; no phantom methods.
refs walking-skeleton
The second walking-skeleton cycle, on top of the 0001 streaming substrate:
the Node contract (C8) and its evaluation context.
Scope (one iteration):
- `Node` trait — `schema() -> NodeSchema` (inputs: kind + lookback; output
kind) + `eval(&mut self, Ctx) -> Option<Scalar>` (None = filter / not warmed).
- `Ctx` — a Copy borrow-wrapper handing eval zero-copy, financial-indexed
Windows per input (`ctx.f64_in(i)[k]`, index 0 = newest).
- Read-side `AnyColumn::as_f64/as_i64/as_bool/as_ts` — mirrors the existing
write-side `as_*_mut` and closes the cycle-0001 audit gap.
- `Sma` in aura-std — the worked producer node (the skeleton's first block),
driven by a hand-written test that mimics the future sim loop.
Deliberate deferrals (recorded as decisions, not gaps): the sim loop (C4),
freshness gating (C5), firing policies (C6, so InputSpec carries no firing
field yet), schema-level tunable params (C12/C19), and sinks / no-output
nodes (C8 consumer side). The hand-driven test stands in for the loop.
Grounding-check PASS: all load-bearing assumptions about the 0001 substrate
(Column/Window/AnyColumn/Scalar, financial indexing) ratified by green tests.
refs walking-skeleton
Five-task plan executing spec 0001: scalar set, Column/Window, KindMismatch,
AnyColumn, then a workspace build/test/clippy + surface-purity gate. Each task
adds its own module + re-export so the crate compiles green after every task;
14 unit tests pin the substrate contract incl. the wrong-kind-push guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first walking-skeleton cycle: the aura-core data substrate everything
streams through — the four scalar base types (Scalar/ScalarKind/Timestamp,
C7), the fixed-capacity financial-indexed SoA Column with its zero-copy
Window (C8), the type-erased AnyColumn edge with an edge-time kind check
(C7), and the per-column run_count freshness primitive (C5). Sharpens the
RustAst reference: no boxed Value, no VecDeque, no RefCell/Rc, no per-event
allocation. Node trait, Ctx, firing policies, and the sim loop are deferred
to subsequent cycles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build a 44-entry glossary from the prose surface (design ledger, project
layout, CLAUDE.md, crate doc-comments) in the convention's three-field
format, and set paths.glossary so it becomes standing reading for every
skill/agent role.
Contested clusters resolved with the user:
- bot vs frozen artifact kept as distinct entries (genus/species).
- backtest / sim / run pinned as three distinct framings, none banned.
- signal kept as its own concept (a score-producing node).
- sim-optimal broker distinguished from Aura.toml's "default broker".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ledger named both sibling projects only conceptually; their concrete
location lived only in session memory. Add an "External components" section
so the repo is the single source of truth.
- data-server: path + Gitea URL, its role as the first data source, and the
concrete API shape (Arc<[T]> chunks, next_chunk, stream_*_windowed). Note
its records are AoS (M1Parsed/TickParsed), so the ingestion boundary (C3)
transposes them into SoA columns (C7) and normalizes time_ms -> epoch-ns.
- RustAst: not just a conceptual reference but a working reference
implementation of the streaming substrate. Map its src/ast/rtl/ layer
(RingBuffer, ScalarValue, ScalarSeries, SoA RecordSeries, the register/
delay node, seeded generator) to the contracts it prefigures (C4/C5/C7/
C8/C9/C12/C19), and state how aura sharpens it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anchor the world/playground bundle. C21 (the World: harnesses are dynamically constructible first-class objects; orchestrating families of them via the C12 axes is aura's differentiator, not the single backtest which is commodity; registry is the World's memory). C22 (the World is a program -> nothing displayable until it runs; the only durable displayable substance is the recorded trace; sinks (C8) are the recording mechanism into the registry (C18); the playground plays ANY harness and is a trace explorer / execution viewer, never a scene editor; observability is explicit/selective; samples ship with sinks). Sharpen C20 (open node vs closed harness; a harness is NOT a node, it is the closure that runs / the root scope; harnesses do not nest as nodes — the World orchestrates them as objects). Correct C14 (playground is core, not 'freely deferrable'). Foundation positioning (World = product, single-harness engine = substrate). CLAUDE.md invariant 12; project-layout day-in-the-life.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anchor the implementation-design branch: C19 (construction is a bootstrap phase — param-generic blueprint -> frozen instance; recursive up to the harness; params size/configure but never change topology). C20 (strategy = reusable context-free composite blueprint with role inputs + position-event output; harness = the root sim graph and C1's disjoint unit, with structural axes = experiment matrix vs tuning params = sweep; both strategy AND experiment authored in Rust via builder APIs, not a config DSL). Extend C8 (schema declares tunable params+ranges), C16 (a project is a Rust crate: cdylib of node/strategy/experiment blueprints + static Aura.toml; hosted by aura during research, frozen to a binary for deploy), C17 (all logic is Rust — nodes/strategies/experiments; Aura.toml = static context only), C12 (frozen topology = a harness instance; structural matrix is the outer axis). Add CLAUDE.md invariant 11; refresh project-layout.md (experiments/ dir, Rust experiments, day-in-the-life).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correct the broker mechanism: a broker is an ordinary downstream consumer node (C8/C9), not an external plugin/subsystem. It consumes the position-event stream + price streams and emits an equity stream; several brokers (e.g. sim-optimal pip + realistic currency) can be attached to the same position table at once, yielding directly comparable equity curves. Updates C10, CLAUDE.md invariant 7, aura-engine/aura-std crate docs, and the day-in-the-life doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C8: a node is producer/consumer/both — at most one output, pure consumers (sinks: chart/equity/logger) have none; records the node-role taxonomy the substrate requires (was wrongly 'exactly one output', contradicting pure consumers and C14). C3: merge by timestamp, normalizing source-native units (data-server Unix-ms) to the canonical epoch-ns of C7. aura-engine lib doc: broker is a downstream plugin over the position table (sim-optimal pip / realistic currency), not an in-strategy 'broker/portfolio node'. Plus a provenance line-wrap nit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersede 'broker is part of the strategy'. A strategy outputs a time-ordered table of position events (event_ts, action[buy/sell/close], position_id, instrument_id, volume; open time = opening event's event_ts, so no open_ts; unsigned volume, direction in action). Brokers become downstream swappable plugins: a deterministic frictionless sim-optimal broker yields synthetic pip-equity for neutral comparison/optimization; realistic broker plugins add currency friction/constraints for viability/deploy. Stays within C7 (scalar columns). Updates CLAUDE.md invariant 7, the walking-skeleton line, provenance, and the day-in-the-life doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refine the structure per interview: aura is the reusable engine, research projects are separate external repos depending on it (C16). Add aura-std (universal standard-node tier) to the workspace; remove nodes/ from the engine (project concept). Record authoring-surface = Claude Code + skills pipeline, no embedded coding-LLM (C17), and project management = one-repo-one-project + Aura-native run registry (C18). Add CLAUDE.md invariants 9-10, code_roots -> [crates], and docs/project-layout.md documenting the day-in-the-life and project repo layout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the rough-sketch interview as 15 discrete contracts (guarantee / forbids / why): determinism, causality, single-merge-at-ingestion, cycle granularity, freshness-gated recompute, firing policies A/B, SoA scalar payloads, node contract, fractal acyclic composition, the broker-is-part-of-strategy execution chain, generalized sources with the record-then-replay boundary, the atomic sim unit + four axes, authoring-only hot-reload, headless two-faced core, and resampling/sessions. Also corrects the tracker repo name to Brummel/Aura.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cargo workspace aura-core -> aura-engine -> aura-cli (bin `aura`) plus nodes/ for hot-reloadable cdylib node crates. Crate bodies are intentionally API-free; types arrive from the first spec. Adds the skills profile (.claude/dev-cycle-profile.yml), the project CLAUDE.md with the eight domain invariants, and the design-ledger skeleton.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>