0bbc8190b8f32d1fc5aa95dceadaad7b6ab4198e
22 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8bd52eaff |
refactor(aura-core): own FieldSpec.name to drop the derive_signature heap leak
FieldSpec.name was &'static str, forcing derive_signature to Box::leak every re-exported composite OutField name. Because compile_with_params reruns derive_signature per sweep point, an N-point sweep over an M-output-field composite leaked N×M boundary names unbounded — a scaling wall the moment the World/sweep layer compiles families per point. Pull FieldSpec.name to String, mirroring the already-owned PortSpec.name and ParamSpec.name: a composite re-exports interior fields under runtime boundary names, so ownership is the correct model, not a workaround. FieldSpec loses Copy; the only fallout is one borrow at the render site. leak_name/Box::leak deleted. Behaviour-preserving: signature contents unchanged, all 231 tests green. |
||
|
|
7d587d0c4e |
feat(aura-core): PrimitiveBuilder::bind — fix a node param as a structural constant
Add `PrimitiveBuilder::bind(slot, value)`: bind a declared param to a structural constant, removed from param_space entirely rather than pinned in the injected vector. This closes the gap #55 names — param-bearing nodes (Sma/Exposure/LinComb) had no constant path, only the no-sweep-mode SimBroker precedent. The motivating case is "SMA2-entry": the 2 is bound to the two-candle construction, so sweeping it would enumerate deformed strategies as valid family members. Approach (Option B, builder-level — the originally-planned direction): bind shrinks the builder's declared param surface (schema.params) and wraps its build closure to re-splice the captured constant at its original positional slot. The construction layer (collect_params / lower_items / param_space / compile_with_params) is byte-unchanged: both dock sites already key off builder.params(), so the shrink propagates for free. Addressing is by param name (the by-name authoring address space, C23/0032); the compilat stays wired by raw index — names never reach it. bind enforces "exactly one open param matches" itself (collect-all-then-reject, panic on zero / on duplicate), mirroring the resolve binder's posture, because a node's own schema.params is not covered by check_param_namespace_injective. Chained binds reconstruct the correct positional vector because each layer computes its slot index relative to the param list it sees (no global position table). Considered and rejected: a per-node builder_const(N) constructor (explodes combinatorially over which subset is constant, not dynamic); a ParamBinding overlay struct on Composite (would need new bookkeeping the builder-level form avoids). Verified: cargo build/clippy/test --workspace all green; new tests cover slot removal, positional reconstruction (chained + partial, via eval), the three panic paths, and the composite param_space projection; blueprint.rs construction layer confirmed byte-unchanged. Deferred (out of scope, follow-up issue): exporting a named frozen strategy as a blueprint-as-values collection — the issue's third fork, needs its own brainstorm. closes #55 |
||
|
|
e304dbaae1 |
feat(aura-core): name input ports
PortSpec gains a non-load-bearing `name: String`, so an input port is named just as FieldSpec.name (output) and ParamSpec.name (param) already are — input ports were the lone unnamed member of the node signature. Identity stays positional by slot (C23); the name is render/debug only, never read by bootstrap or the run loop. PortSpec drops Copy (String is not Copy), exactly as ParamSpec already does. - Every aura-std node names its input slots: SMA/EMA "series", Sub/Add "lhs"/"rhs", Exposure "signal", SimBroker "exposure"/"price" (the slots become self-documenting), LinComb "term[i]" and Recorder "col[i]" generated in their build loops (mirroring LinComb's existing weights[i] param loop). - derive_signature carries a composite's Role.name into the derived input port (it was dropped before — the output side already carried FieldSpec.name), so the graph model is homogeneously named at both levels. - model_to_json (port_json + the composite-header inputs in scope_json) emits the name as a third tuple element: ["f64","any","exposure"]. The byte golden was re-captured (machine bytes) and its substring twins updated; the model is now fully named across inputs/outputs/params. - All 16 PortSpec construction sites threaded in one compile-gate change; test fixtures carry fixture names. C8 realization note added to the design ledger. Why name-only, no validation: the name is a pure debug symbol. Wire-by-name was rejected (it would be a C23 contract change). Bootstrap slot-wiring validation (which would close #21's same-kind swap footgun) is deferred to its own cycle — a name alone does not catch the swap; it makes the slots self-documenting and gives a future validation something to check against. Verified: cargo test --workspace 168 green; clippy --all-targets -D warnings clean; cargo build --workspace clean. Read-only render path (C9), no serde (C14), scalar kinds unchanged (C4). closes #50 refs #21 refs #51 |
||
|
|
d6f59d7a52 |
audit: cycle 0024 (#43, #36) — drift-clean; ledger reconciled to the pre-build signature
Architect drift review (862882b..1b39093): NO code drift, no contract weakened. C1 (behaviour-preserving: 150 tests green, render goldens byte-identical, no behavioural assertion altered), C8 (sink output: vec![] preserved), C9/C23 (read-only render touches no eval/build; derive_signature's Box::leak is cold-path-only, leaked names non-load-bearing) all hold. Regression scripts: none configured (no-op) — the architect review is the gate. The only drift was design-ledger prose, anticipated and deferred to this audit by the plan. Resolved here (fix path, orchestrator-applied — docwriter is barred from the design ledger): - docs/design/INDEX.md C8 Guarantee: rewritten — a node's signature (NodeSchema) is declared pre-build on the value-empty recipe; the built node implements lookbacks() (the one param-dependent sizing quantity) + eval(); schema() is gone. - INDEX.md C8 cycle-0015 note: Blueprint::param_space -> Composite::param_space; the #36 lockstep-debt clause marked closed by 0024. - INDEX.md: added a C8 cycle-0024 realization (signature lives once on the recipe, the signature/sizing split, #43/#36 closed) and a C19 cycle-0024 realization (struct Blueprint collapsed into the root Composite; Role.source/runnable-iff- bound/UnboundRootRole; FlatGraph as the named compilat). Stale symbol names in the dated 0016/0017 notes annotated in place with their 0024 renames. - aura-std/src/lincomb.rs module doc: Blueprint::param_space -> Composite::param_space. Ledger-gap (four new load-bearing invariants previously unrecorded) closed by the two new realization notes. Green baseline re-confirmed after the edits: cargo test --workspace 150 passed / 0 failed; cargo clippy --all-targets -D warnings clean. refs #43 #36 |
||
|
|
1b3909316e |
feat(aura): node signature lives in the blueprint; collapse Blueprint into Composite
Consolidate the node data structure so every node's signature (NodeSchema:
inputs/output/params) is declared once and exists in the blueprint pre-build,
and dissolve the special "root graph" type. Behaviour-preserving (C1).
Signature vs sizing
- NodeSchema is now the static signature only: InputSpec -> PortSpec{kind,firing},
with lookback removed. The signature is fully static per blueprint (input
kinds/firing, output fields, params); LinComb's variable arity is a builder arg,
not an injected param.
- The one param-dependent quantity, an input's buffer lookback (e.g. Sma's window =
its injected length), moves out of the signature to Node::lookbacks() -> Vec<usize>,
read only by bootstrap for sizing. Node::schema() is removed.
- LeafFactory -> PrimitiveBuilder, which carries the full NodeSchema. The built node
no longer re-declares it: closes the params-declared-twice drift (#36, the 8
per-node factory_params_match_built_node_schema lockstep tests are deleted — their
subject is now structurally impossible) and a value-empty recipe exposes its full
I/O interface pre-build (#43).
Root is just a bound composite
- struct Blueprint is deleted; its compile/bootstrap/param_space methods move onto
Composite. Role gains source: Option<ScalarKind> (None = open interior port,
Some = bound ingestion feed). A composite is runnable iff every root role is bound;
the "main graph" is no longer a category, only the fully-source-bound composite.
New error CompileError::UnboundRootRole for an open root role.
- BlueprintNode::signature() answers uniformly for both arms: Primitive returns the
builder's declared schema, Composite derives it from the interior (role kinds in,
OutField kinds out, aggregated params), pre-build, no build.
compile -> FlatGraph -> bootstrap
- compile validates structure pre-build via signature() (validate_wiring: range +
kind, returning the same variants as before, so an edge kind fault is now caught
before any build closure fires) and emits FlatGraph{nodes,signatures,sources,edges}.
- bootstrap consumes the FlatGraph: kinds/firing/output from the carried signatures,
buffer depth from node.lookbacks(). SourceSpec survives as the flat descriptor.
Renames: BlueprintNode::Leaf -> Primitive, LeafFactory -> PrimitiveBuilder.
Render (aura-cli/src/graph.rs) is migrated compile-only: it takes &Composite, maps
bound roles to the same source-entry shape, so both render goldens reproduce
byte-identical output (no re-capture needed). Render-fidelity tuning is the next cycle.
Verification (orchestrator-run, not agent-reported): cargo build --workspace green;
cargo test --workspace 150 passed / 0 failed; cargo clippy --workspace --all-targets
-D warnings clean. All pinned determinism/run-output tests pass with values unchanged;
no behavioural assertion was altered to go green. 5 new tests assert the signature is
pre-build and uniform, that compile rejects a kind mismatch without building (via a
panicking builder), UnboundRootRole, and lookbacks()/signature arity agreement.
Deferred to cycle-close audit (per plan): docs/design/INDEX.md and some aura-std
module docs still name the old Node::schema()/LeafFactory/BlueprintNode::Leaf/
Blueprint::param_space contracts; prose reconciliation is the architect's at audit.
closes #43 #36
|
||
|
|
ae1d4564f5 |
feat(aura-std): EMA node (SMA-seeded, O(1) per cycle)
Add Ema, the std-lib's first recursive/stateful producer: exponential moving average with alpha = 2/(length+1). The building block MACD (and DEMA, MACD-histogram, etc.) needs, and a worked example of a node whose param sizes its math, not its input window. Seeding is ratified as SMA-seeded (ta-lib convention): the EMA stays silent until it has seen length samples, seeds with their SMA, then runs the recurrence. Chosen over a first-value seed for three substantive reasons -- warm-up consistency with Sma (both emit None until length samples), parity with the EMA traders expect (ta-lib MACD seeds this way), and statistical honesty (no average claimed from a single observation). A first-value seed was the proof-of-concept convenience; rejected. Performance: recursive state means lookback = 1 (length sizes alpha, not the window); eval is O(1) time and state via ema += alpha*(x - ema), with no hot-path allocation (the output buffer is reused). Tested: warm-up-then-smooth (a step input separates it from a plain SMA), length-1 identity, label, and factory/schema agreement. |
||
|
|
4b64409036 |
feat(aura-core,aura-std,aura-engine,aura-cli): inject a param-set vector at bootstrap (#31)
Cycle B of milestone "The World — parameter-space & sweep". A blueprint is now value-empty: a leaf holds a param-generic recipe, not a built node, and a positional Scalar vector is bound slot-by-slot at bootstrap — so one blueprint bootstraps into many distinct instances under different vectors, with no cdylib rebuild (C12/C19). This is the binding primitive a sweep (#32) drives. Ratified design (brainstorm): value-empty reconstruct-through-new() over mutate-in-place and over a default-bearing variant. The value lives only in the injected vector (no baked default), keeping the blueprint a pure param-generic recipe (C19); every injected value flows through the node's own constructor (the single sizing/validation gate). - aura-core: LeafFactory { name, params, build } — the recipe (params -> sized node through `new`); Scalar::as_i64/as_f64 value accessors. Node trait unchanged. - aura-std: each of the 7 nodes exposes factory() (SMA length:I64, Exposure scale:F64, LinComb arity x weights[i]:F64; Sub/Add/SimBroker/Recorder paramless, capturing their non-param construction args — pip_size, the Recorder channel). - aura-engine: BlueprintNode::Leaf(LeafFactory), From<LeafFactory>; param_space() reads factory.params() pre-build; compile_with_params/bootstrap_with_params build each leaf from its kind-checked slice while lowering (build-then-wire), arity checked up front via param_space().len(); CompileError::{ParamKindMismatch, ParamArity}. compile/inline/edge-rewrite are structurally unchanged, so the compilat stays bit-identical for a given point (the bit-identity and both param_space mirror tests stay green). The vestigial pre-build Composite::schema / BlueprintNode::schema (no live caller — interface resolution is structural on the built flat nodes) are removed. - aura-cli: the blueprint view labels leaves by bare type via LeafFactory::label() (`[SMA]`) — the ascii-dag renderer cannot render wide cluster-sibling labels (see spec); the compiled view still labels valued (`SMA(2)`). The sample supplies its point as a vector; the mis-wiring swap moved to the compiled view. The #34 dual-traversal drift hazard is subsumed: compile_with_params consumes the vector in the same recipe walk param_space() reports, so the two share one traversal. Verified: cargo build/test --workspace green (127 tests, incl. bit-identity, both mirror tests, and 4 new injection tests — different-vector-different-run, kind mismatch, arity, determinism); clippy --workspace --all-targets -D warnings clean; `aura graph` blueprint view renders cleanly. Scope: one vector -> one instance. Deferred: sweep enumeration (#32), domain validation (#32/C20), single-run authoring convenience (#35). closes #31 |
||
|
|
931109df58 |
feat(aura-core,aura-std,aura-engine): declare node tunable params (#30)
Cycle A of milestone 'The World — parameter-space & sweep'. A node now declares its tunable parameters in its C8 schema, and a blueprint aggregates them into one flat, inspectable param-space — the root that unlocks the C12 orchestration axes (#31 bind, #32 sweep), filling the gap C8/C23 name 'deliberately not in the schema yet'. - aura-core: ParamSpec { name: String, kind: ScalarKind } as a third schema- declaration type; NodeSchema gains a third field 'params'. name is String (not &'static like FieldSpec) because a vector knob carries a runtime index and aggregation prefixes the composite path. - aura-std: Sma declares [length:I64], Exposure [scale:F64], LinComb expands to N flat [weights[i]:F64] (N = its input arity, topology-fixed per C19); Sub/Add/ SimBroker/Recorder declare none (pip_size is metadata, C10/C15; Recorder is wiring). - aura-engine: Blueprint::param_space() walks the graph-as-data depth-first in lower_items order, path-qualifying via the already-public Composite::name() — a read-only projection (C9). compile/inline_composite/lower_items are untouched, so the compilat stays bit-identical (composite_sma_cross_runs_bit_identical_to_hand_wired and the golden render tests stay green). Design: param identity is positional (slot after the deterministic inline order, C23 'by index not name'); the path-qualified name is a non-load-bearing debug symbol — same-type siblings in one composite share a name, uniqueness is at the slot. Flat over a structured arity-bearing ParamSpec because flattening is unavoidable (a sweep enumerates a flat point-space) and structure-in-the-runtime is the nested-composite reading C23 rejects (see spec 0015 for the full rationale). Scope is declaration + aggregation + inspection only; binding (#31), sweep enumeration (#32), search-range, validity-constraint, and default-range/slider are deferred. An E2E test pins the load-bearing C23/#31 invariant: param_space() slot order mirrors compile()'s flat-node param order, kind-by-slot, on the SMA-cross harness. fieldtests/ (excluded crates) left as frozen snapshots. Verified: cargo build/test --workspace green (64 tests), clippy --all-targets -D warnings clean; compile/inline diff empty. closes #30 |
||
|
|
0a855c3943 |
feat(aura-cli): aura graph renders a wired graph as an ASCII DAG (#13)
Make a wired graph introspectable so a mis-wiring becomes visible. Two
selectable views, both authored from one built-in sample blueprint:
aura graph clustered blueprint view — composites drawn as named
ascii-dag cluster boxes (pre-inline, C9)
aura graph --compiled flat compilat view — composite boundaries dissolved
(C23); the graph the run loop actually runs
The payoff is intrinsic, param-carrying node labels: two SMAs read SMA(2) /
SMA(4), so a swapped fast/slow input reads back differently. This is realized
by a `label()` default method on the core Node trait — a non-load-bearing
render symbol the run loop never reads (wiring is by index), the symbol C23
already reserves citing #13. Recorded as a C8 refinement in the ledger.
Rejected the alternatives in brainstorm: a separate Describe trait (forces a
combined trait-object dance for a label the ledger calls non-load-bearing) and
extrinsic parallel labels (decoupled from the node, so an author can label the
slow SMA "fast" and mask the very bug the render exists to surface).
Mechanism:
- aura-core: Node::label() default method (additive, object-safe, single-line).
- aura-std: per-node overrides — SMA(n)/Exposure(s)/SimBroker(p) carry params;
Sub/Add/LinComb/Recorder are bare (their identity is not a mis-wiring axis).
- aura-engine: Composite gains an authored `name` (cluster title; dissolves at
inline) + read-only graph-as-data accessors on Blueprint/Composite. No
external dependency — the engine stays dependency-pure (C16); ascii-dag lives
only in aura-cli. The label-free index-wired compilat (C23) is unchanged.
- aura-cli: ascii-dag v0.9.1 + a graph.rs adapter (Vertical mode; labels
materialized into an owned Vec<String> that outlives the borrow-based Graph).
`aura run` is untouched (the sample duplication is dedup idea #14).
Tests: a concern-defining test (swapped sample renders != correct), structure
pins (cluster present in blueprint view, absent in compiled view), per-node
label disambiguation, and two frozen byte-goldens of the deterministic render.
The cycle-0012 bit-identical and run-sample non-regression tests stay green.
Verified by the orchestrator (not the agent report): cargo build --workspace
clean; cargo test --workspace all green; cargo clippy --workspace --all-targets
-D warnings clean; both views run live and render as expected.
closes #13
|
||
|
|
559903a14e |
feat(aura-cli): aura run — end-to-end sample-harness CLI
The Walking-skeleton milestone's closing seam: a real `aura run` binary that
bootstraps a built-in sample harness, runs it deterministically, and prints the
cycle-0009 metrics+manifest report as canonical JSON to stdout — the headline
C14 "run a sim, emit structured metrics" move, end-to-end, from a binary for the
first time.
Two deliverables:
- aura-std::Recorder — a reusable recording sink node (the glossary *sink* role,
C8/C22): a pure consumer (output: vec![]) over kinds.len() input columns,
holding an mpsc::Sender<(Timestamp, Vec<Scalar>)> as its out-of-graph
destination. mpsc keeps the engine's purity invariant (C7): no Rc/RefCell.
Supports all four base scalar kinds; returns None until every column is warm.
Promotes the shape the #[cfg(test)] fixtures already proved into a shipped,
reusable block (the fixtures stay as historical snapshots; de-dup is a later
tidy).
- aura-cli `run` subcommand — synthetic_prices / sample_harness / run_sample /
main. The sample harness (synthetic source → SMA(2)/SMA(4) → Sub → Exposure →
SimBroker → two Recorder sinks) is authored in plain Rust over the raw
Harness::bootstrap(nodes, sources, edges) API (C17/C20) — the open
experiment-builder DSL thread is deliberately not committed this cycle. main
hand-parses one subcommand: `run` prints run_sample().to_json() (exit 0),
anything else prints a one-line usage to stderr (exit 2). aura-cli gains
aura-std + aura-core path deps; the workspace stays zero-(external-)dependency.
The built-in synthetic stream (7 ticks, rises then reverses) yields a non-trivial
demo trace: equity [0,0,0,0,-0.08,-0.17,-0.13] → total_pips -0.13, max_drawdown
0.17, exposure_sign_flips 1. The run_sample unit test pins the integer flip count
exactly and the two f64 metrics within 1e-9 (the real run's float dust is ~7e-15,
confirming the tolerance); determinism is pinned exactly (two runs → identical
JSON). tests/cli_run.rs drives the built binary for the observable exit/stdout
contract.
manifest.commit is filled from option_env!("AURA_COMMIT") (defaults to
"unknown"); real HEAD capture via build.rs is deferred. Non-goals untouched:
experiment-builder DSL, aura new, Aura.toml schema, the data-server source (#7).
One deviation from the plan's verbatim code: a scoped
#[allow(clippy::type_complexity)] on sample_harness (its (Harness, Receiver,
Receiver) return tuple trips the lint under -D warnings) — consistent with the
identical allow on the build_two_sink_harness fixture in aura-engine. Verified
myself: cargo test --workspace (61 green, 0 red), clippy --all-targets
-D warnings (clean), cargo doc -D warnings (clean), and both smoke runs.
closes #8
|
||
|
|
521a16e56f |
docs(aura-std): document Add/LinComb firing policy + multi-row-per-ts shape
Resolves the two spec_gaps from the cycle-0008 fieldtest
(docs/specs/fieldtest-0008-sum-combinators.md), the same class the 0007 fieldtest
raised for SimBroker (and which
|
||
|
|
45920faa8f |
audit: cycle 0008 tidy — correct LinComb param-claim wording
Architect drift review (fa2835e..HEAD) at cycle close. Regression gate is a no-op (commands.regression empty); architect is the sole gate. What holds (no drift): purely additive C16 extension, one node per file, no engine/manifest change; C2/C8 warm-up discipline correct (None until every leg present, no implicit cold-leg 0.0); C7 zero per-cycle alloc. Shipping the named convenience nodes Add (and pre-existing Sub) beside the general LinComb is NOT C9 adapter-zoo drift — they are distinct named producers, not coercion shims, mirroring the already-shipped Sub. Fixed [ledger-gap/high]: lincomb.rs rustdoc and the feat commit body called the weights "the node's tunable parameters (C8/C12)", but aura_core/src/node.rs states tunable params (C12/C19) are deliberately not part of the schema yet (spec 0002 "Out of scope"). The weights are construction parameters that configure the node and fix its arity (C11) — the natural home for combination tuning params once the schema-level param surface lands, but not yet surfaced to any optimizer. Rustdoc reworded to say exactly that. No ledger change: the C8 text vs node.rs tension pre-exists this cycle and is already documented in node.rs; only the new rustdoc over-claimed. Carry-on [drift/high]: the cycle-0007 fieldtest still imports Sub-only and keeps its hand-authored Add2 (acceptance said the fieldtest *can* drop it). The 0007 fieldtest crate is a historical snapshot of what that run did; rewriting it retroactively would falsify why Add2 existed. The structural drop-in feasibility (the acceptance content) is confirmed by the architect (add.rs == sub.rs modulo operator; LinComb([1,1]) == Add; Add2 byte-identical to Add). End-to-end demonstration belongs to a cycle-0008 fieldtest (fresh example on the shipped node), dispatched next — not a retroactive edit of the 0007 record. Gates: cargo test -p aura-std 14 passed / 0 failed; clippy --all-targets -D warnings clean; RUSTDOCFLAGS="-D warnings" cargo doc -p aura-std clean. |
||
|
|
84130c2cab |
feat(aura-std): Add + LinComb sum combinators
aura-std shipped Sma, Sub, Exposure, SimBroker but no sum, so the north-star
"combine one signal with another" research move (C10) could not be expressed
from shipped blocks — the cycle-0007 fieldtest had to hand-author a project-local
Add2. This adds the missing combinator(s):
- Add — two-input f64 sum (a + b), the parameterless companion to Sub. Mirrors
sub.rs modulo the operator.
- LinComb { weights } — N-input weighted sum (Σ wᵢ·xᵢ). The weights are the
node's tunable parameters (C8/C12) and fix its arity (weights.len() inputs);
this is the form the north-star "combine A and B *with weights*" reaches for.
LinComb([1,1]) is Add; LinComb([1,-1]) is Sub.
Both withhold output (None) until *all* inputs are present — consistent with Sub,
and causally clean: a cold input leg is never silently folded in as 0.0.
LinComb::new panics on empty weights (build-time param error, like Sma::new).
Both ship because each is independently reached-for: Add for readability symmetry
with Sub, LinComb for the weighted/tunable combination — mirroring the project's
already-shipped choice to keep Sub as a named node beside a general form.
Hand-driven unit tests in the established aura-std style (5 new): Add sum, the
Add == LinComb([1,1]) identity, the N>2 warm-up, and the empty-weights panic.
Verified: cargo test -p aura-std (14 passed), clippy --all-targets -D warnings
clean, RUSTDOCFLAGS="-D warnings" cargo doc -p aura-std --no-deps clean.
closes #11
|
||
|
|
e9f4352640 |
docs(aura-std): document SimBroker input slots + firing/warm-up shape
Resolves the two spec_gaps from the cycle-0007 fieldtest
(docs/specs/fieldtest-0007-signal-quality.md): the consumer-facing SimBroker
struct rustdoc stated only the integration formula, leaving two things a
downstream author cannot infer from the public surface —
- input slot order (slot 0 = exposure, slot 1 = price). Both slots are f64, so
a swapped wiring is NOT caught at bootstrap (no kind mismatch) and silently
yields a wrong-but-plausible equity curve. The order is now documented as
load-bearing on the struct doc, with that hazard called out.
- firing / warm-up emission shape: both inputs are Firing::Any, so the broker
fires on every price-fresh cycle and reads a cold exposure leg as flat 0.0 —
emitting equity rows (leading 0.0s) from the first price tick, one per price
cycle, not one per exposure value. A consumer needs this to predict the
recorded curve's length and leading values.
Doc-only change; no behaviour change. Verified: RUSTDOCFLAGS="-D warnings" cargo
doc -p aura-std clean (intra-doc links to crate::Exposure and aura_core::Firing
resolve); clippy -p aura-std --all-targets -D warnings clean.
refs #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
3b49074156 |
feat(aura-std): signal-quality loop — Exposure node + sim-optimal broker
Realizes the C10 reframe (cycle 0007): the strategy DAG's output is an
intent/exposure stream, and a sim-optimal broker integrates it into a synthetic
pip-equity curve that measures SIGNAL QUALITY — the project's first trading
result, and its primary research loop.
Two new aura-std nodes (plain structs; the engine stays domain-free):
- Exposure { scale }: the decision/sizing node — clamp(signal/scale, -1, +1),
one f64 exposure per fired cycle, None until warmed up. Sizing/risk live in
`scale`.
- SimBroker { pip_size }: class (a) of C10 — consumes exposure (slot 0) + price
(slot 1), integrates the return earned by the exposure HELD INTO each cycle
(prev_exposure, decided at t-1) so it is causal / look-ahead-free (C2), and
emits cumulative pips. pip_size is held reference metadata (C7/C15), never
streamed.
End-to-end proof in the aura-engine harness test module (it dev-depends on
aura-std): a SMA(2)-SMA(4) cross -> Exposure -> SimBroker -> Recorder sink wires
a full signal-quality backtest; the recorded equity matches a hand-computed pip
curve ([0,0,0,0, 0.035]) and is bit-identical across two runs (C1). The engine
itself is unchanged — pure node authoring on the frozen substrate.
Verified: cargo test --workspace green (20 core + 30 engine + 9 std; 8 new);
clippy --all-targets -D warnings clean; engine/core surface-purity grep (no
dyn Any/Rc/RefCell) clean.
Note: the plan inlined `Window::first()`, which aura-core's Window does not
expose; the shipped code uses the semantically identical `Window::get(0)`
(newest value, or 0.0 when the exposure leg is cold).
closes #4
closes #5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1100a60c76 |
feat: recording is a node role, not a type (multi-sink substrate)
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 |
||
|
|
5228542bfd |
feat: node output is a record (composite stream), not a single scalar
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> |
||
|
|
f37b2a35d3 |
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>
|
||
|
|
dd5c3fad96 |
feat(engine): the deterministic single-source sim loop
Cycle 0003: aura-engine's first real content — a Sim that runs a wired DAG of
nodes deterministically, cycle by cycle. First point in the project where
authored nodes execute against data, not just under a hand-fed Ctx.
- `Sim` (aura-engine) — a bootstrapped, frozen root graph: a flat Vec<NodeBox>
(each node owns its input columns, the cycle-0002 shape) + an index edge
table, topologically ordered (Kahn). `bootstrap` sizes every input column from
its node's schema, kind-checks each edge and source target, and rejects
directed cycles — C7's "type check paid once at wiring" generalized to the
whole topology (`BootstrapError::{KindMismatch, BadIndex, Cycle}`).
- `run` — the deterministic loop: per record, forward the source value into its
target slots, evaluate nodes in topo order, capture the observed node's output
at eval time, and forward each `Some` output into its consumers' input columns
(a `None` forwards nothing — the structural seed of sample-and-hold). The loop
destructures `&mut self` into disjoint field borrows and allocates nothing on
the per-cycle path. This is the flat, monomorphized sharpening of RustAst's
reference-counted, interior-mutable observer push graph (C1/C7).
- `Edge` / `Target` (aura-engine) — producer->consumer and source->consumer wiring.
- `Sub` (aura-std) — a 2-input f64-difference node, so the loop is proven on a
real fan-out + join DAG (source -> {SMA(2), SMA(4)} -> Sub), with a determinism
assertion (C1: a second identical run is bit-identical).
Engine library depends only on aura-core; a test-only dev-dependency on aura-std
lets the integration test wire real Sma/Sub nodes. Sim carries a hand-written,
node-opaque `Debug` impl (Box<dyn Node> is not Debug; the impl prints
nodes.len() + the index/topology fields, needed by the bootstrap-rejection tests'
`unwrap_err`).
Deliberately deferred (recorded as decisions): freshness-gated recompute /
sample-and-hold (C5 -> cycle 0004, with the second source that makes it testable);
the explicit monotonic cycle_id counter (its first reader is freshness, 0004);
the Source trait + data-server ingestion + k-way merge (C3/C11); the builder API
(C19); the real sink + run registry (C18/C22).
Gates green: cargo build/test (26: 18 aura-core + 3 aura-std + 5 aura-engine)/
clippy -D warnings clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.
refs walking-skeleton
|
||
|
|
77a1d26017 |
feat(core): the node contract — Node, schema/eval, Ctx, and a worked SMA
Cycle 0002, the second walking-skeleton slice on top of the 0001 substrate:
the interface every node forever implements (C8), proven end-to-end by a real
node with no engine present.
- `Node` (aura-core) — `schema() -> NodeSchema` declares inputs (kind +
lookback) and the single output kind; `eval(&mut self, Ctx) -> Option<Scalar>`
computes one cycle's output (`None` = filter / not-yet-warmed-up). `&mut self`
so a node may keep its own derived state.
- `Ctx<'a>` (aura-core) — a `Copy` borrow-wrapper handing `eval` zero-copy,
financial-indexed `Window`s per input (`ctx.f64_in(i)[k]`, index 0 = newest).
A kind mismatch panics ("engine bug") — wiring guarantees the kind, so a
mismatch can only mean the wiring layer is broken; this keeps node-author code
clean (`w[k]`, not `w?[k]`).
- `AnyColumn::as_f64/as_i64/as_bool/as_ts` (aura-core) — the read-side mirror of
the existing `as_*_mut`, the mechanism `Ctx` uses to get a typed window from a
type-erased edge. Closes the read-side gap the cycle-0001 audit recorded.
- `Sma` (aura-std) — the skeleton's first real block: a producer node computing
the moving mean of one f64 input, emitting `None` until warmed up. Authored in
a downstream crate (proving aura-core's contract is usable across the crate
boundary) and driven by a hand-written test that mimics exactly what the sim
loop will later generalize: push fresh input, eval, collect.
Deliberately deferred (spec 0002 "Out of scope", recorded as decisions): the
firing policies (C6 — InputSpec carries no firing field yet), the sim loop (C4)
and freshness gating (C5), schema-level tunable params (C12/C19), and the
no-output sink refinement (C8 consumer side).
Gates green: cargo build/test (20: 18 aura-core + 2 aura-std)/clippy -D warnings
all clean; surface-purity grep (no dyn-Any / Rc / RefCell) clean.
refs walking-skeleton
|
||
|
|
1467fcd30f |
docs: brokers are consumer nodes (C10), several attachable for comparable curves
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> |
||
|
|
b99912ca59 |
docs+scaffold: engine/project split (C16-C18), aura-std, usage doc
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> |