a6a314bb980fdbac1d54325b4f0126ee3d6a4ac8
23 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a6a314bb98 |
feat(aura-engine): name-based blueprint wiring via GraphBuilder
Add an additive, fluent GraphBuilder authoring surface that wires a blueprint's topology by typed node handles and port/field names instead of raw positional indices. Node references are Copy NodeHandle values (a node reference cannot be mistyped); only port/field names are strings, resolved against each node's cached NodeSchema by exactly-one-match at a single fallible build() -> Result<Composite, BuildError> — the Binder posture, one level up. build() lowers to the unchanged Composite::new, so the compilat stays index-wired (C23): names resolve at the authoring boundary and never reach FlatGraph. A new From<Composite> for BlueprintNode lets add() accept nested composites. Verified: builder-authored sma_cross is structurally equal to the hand-wired fixture, and the full builder-authored harness lowers to a byte-identical FlatGraph (equal edges + sources). The five BuildError variants (Unknown/Ambiguous In/Out, BadHandle) are covered, and the SimBroker exposure/price legs are now addressable by name (the #21 legibility win). The structural close of the exposure/price swap (#21) is deliberately out of scope — this builder makes the swap legible, not impossible — tracked as #65. Two plan-test corrections applied during implementation (both in-scope, no behaviour change): the harness parity test uses compile_with_params with a topology-invariant param vector on both sides (the harness declares three params, so a no-param compile() would trip ParamArity), and the error asserts use .err()/Some(...) rather than unwrap_err() (Composite is not Debug). aura-engine 123 tests green (+9); full workspace green; clippy --all-targets -D warnings clean. Existing index-form Composite::new sites and tests untouched (coexistence). closes #64 |
||
|
|
1f01badac7 |
refactor(aura-engine): share the SMA-cross harness test fixtures
Collapse the three verbatim-duplicated #[cfg(test)] fixtures — synthetic_prices, sma_cross, composite_sma_cross_harness — that lived in both blueprint.rs's and sweep.rs's test modules into one shared crate-root `#[cfg(test)] mod test_fixtures` (pub(crate) free fns). The two consumers now import from `crate::test_fixtures`; sweep.rs's test-mod `use` block sheds the imports that were only needed by the moved fns. Why: the topology had to move in lockstep across the two copies but no test pinned them equal, so an edit to one silently diverged the other (#53). One definition removes the hazard structurally — better than a pin-them-equal test, which loses its premise once there is a single definition. Test-only; no production code, public API, or runtime path changes. C16 untouched (a #[cfg(test)] module ships in no artifact). The CLI sample is no longer a third peer (cycle 0036 enriched it into a trend+momentum blend), so this is an engine-internal two-copy dedup, not the three-way the issue originally named. graph_model.rs keeps its own intentionally-minimal golden copy, out of scope. Plan glitch fixed in-flight: the plan's literal import list named sma_cross, but neither consumer calls it directly (only transitively via composite_sma_cross_harness), so importing it tripped the plan's own Step-7 clippy -D warnings gate. Trimmed to {composite_sma_cross_harness, synthetic_prices} — within the task's "drop now-unused imports" scope. Verified: cargo test --workspace green (aura-engine 114 passed, count unchanged — behaviour-preserving); the two load-bearing fixture consumers composite_sma_cross_runs_bit_identical_to_hand_wired (C1) and sweep_equals_n_independent_runs (C11/C12) both green; cargo clippy --workspace --all-targets -- -D warnings clean. closes #53 |
||
|
|
1b7e4ad169 |
feat(aura-engine): param_space() injectivity as a compile invariant
A non-injective param_space() — two slots under one path-qualified name — is the by-name knob address space (C12/C19) being broken: no binding can select one slot without the other. This cycle makes it a structural compile error. One check, check_param_namespace_injective over the param_space() names, replaces the whole fan-in machinery (signature_of, leaf_has_param, check_fan_in_distinguishability, check_composite_fan_in) and runs from compile_with_params AND from both binders before resolve/resolve_axes — so the canonical by-name author sees the structural cure (CompileError::DuplicateParamPath, carrying the path and pointing at .named(...)) instead of the AmbiguousKnob symptom (#59). With an injective space guaranteed before resolve, no bound name can multi-match, so BindError::AmbiguousKnob is dead and removed (both resolver arms -> unreachable!). param_space() stays infallible; the invariant lives in the compile step (C23). The check is strictly the by-name-addressability property. The old fan-in predicate (equal signature AND >=1 param) also rejected two collisions that are NOT param_space duplicates — an asymmetric param/paramless collision and a role-vs-leg collision — which guarded render identity, dead since the renderer was retired in 0026. Both are intentionally dropped; a future node-/wiring-name check, if wanted, is decoupled from injectivity. The new invariant correctly rejected a pre-existing fixture (multi_output_composite's two unnamed Sma legs); cured in place with the .named(fast/slow) the invariant mandates. A new E2E test drives the cured cross through the by-name binder end-to-end (resolve + bootstrap + run to a populated exposure trace). C9/C12/C19/C23 ledger notes amended. Verified: cargo build/test --workspace green (0 failed), clippy --workspace --all-targets -D warnings clean. closes #59 |
||
|
|
ffed8cc612 |
feat(aura-core,aura-engine,aura-cli): node-instance naming retires ParamAlias
Every blueprint node now carries a name. A primitive builder gains an optional
instance name (Sma::builder().named("fast")); when omitted it defaults to the
node's type label, ASCII-lowercased verbatim ("SMA"->"sma", "SimBroker"->
"simbroker"). param_space() is now uniformly <node>.<param> at every level
including the root (sma_cross.fast.length, exposure.scale). Fan-in
distinguishability (C9) and signature_of re-key onto the node name: two same-type
siblings both defaulting to "sma" collide and IndistinguishableFanIn fires, so
one act -- naming the legs "fast"/"slow" -- fixes both the naming collision and
the fan-in check. The index-addressed ParamAlias overlay, Composite.params, the
Composite::new 5th argument, aliases_on, and check_alias_indices are removed
(Role.name and OutField.name untouched). Ledger C9 and C23 amended.
This is why the change is correct, not merely convenient: blueprints are
value-empty (C11), so the thing that would otherwise distinguish two SMAs --
their length -- is not present at construction; identity must come from a name,
not a deferred value. Closes the #56 friction surfaced by the param-space &
sweep milestone fieldtest (a freshly-authored 2-SMA cross was unbindable and
would not bootstrap without two hand-counted ParamAlias overlays).
Two plan defects were corrected during implementation and verified:
- the three new fan-in/path tests authored sma_cross at root level, which would
qualify to "fast.length" (root prefix is empty) and is not the nested case the
fan-in check inspects; nesting sma_cross under a root (a shared
sma_cross_under_root helper) restores the asserted "sma_cross.fast.length" and
IndistinguishableFanIn{node:2};
- three cycle-0030 named-binder tests bound the real harness by the old names
(sma_cross.fast / scale) and were migrated to the new path strings, surfaced by
the workspace test gate.
Verified by the orchestrator: cargo build/test --workspace green (198 tests,
0 red), clippy --all-targets -D warnings clean. model_to_json emits the type
label + factory param names (node-name-independent), so sample-model.json and the
graph_model golden are byte-identical (untouched). NodeSchema gained a Default
derive (the builder default-name test constructs an empty schema). fieldtests/
are frozen non-workspace records, not migrated.
closes #56
|
||
|
|
937257e368 |
feat(aura-engine,aura-cli): named param binding — sweep axes .axis()/.sweep() (0030 iter 2)
Bind a sweep's axes by name instead of by a positional GridSpace:
bp.axis("sma_cross.fast", [2, 3]).axis("sma_cross.slow", [4, 5]).axis("scale", [0.5]).sweep(run)
The sweep half of spec 0030, completing the named-binding feature (#35). A pure
authoring layer over the existing GridSpace::new / sweep primitives; engine core
untouched (C1/C12/C19/C23).
This iteration:
- resolve_axes(): shares resolve's name->slot mapping (iter 1), adds the EmptyAxis
check (the variant defined in iter 1 is now first constructed here) and a
per-element axis kind-check (every element of each axis, first offending element
in axis order). Its validation is a strict superset of GridSpace::new's
(arity / non-empty / per-element kind), so SweepBinder::sweep's downstream
GridSpace::new(...).expect(...) is infallible by construction — it can never
panic on author input.
- Composite::axis -> SweepBinder -> SweepBinder::sweep (Result<SweepFamily, BindError>).
- The CLI sample sweep converted to the named form (behaviour-preserving).
Plan correction (verified by the orchestrator): the iteration plan said only the
GridSpace import was orphaned by the CLI conversion, but replacing the free
sweep(&grid, ..) call with the .sweep(..) method also orphans the free `sweep`
import — both were dropped from aura-cli to satisfy clippy -D warnings. The Scalar
import (from aura_core) stays used and was untouched.
Verified (run by the orchestrator): 6 new aura-engine tests green —
resolve_axes round-trip, EmptyAxis, MissingKnob, the per-element KindMismatch on a
mixed axis (the superset/no-panic guarantee), a builder no-panic wrong-kind test,
and a GridSpace .points() parity test; the two sweep JSON goldens
(sweep_report_renders_four_points_in_odometer_order + the cli_run integration
golden) green unchanged (name/value/order-preserving conversion); full
`cargo test --workspace` green and `cargo clippy --workspace --all-targets
-- -D warnings` clean.
refs #35
|
||
|
|
64b19ab3d5 |
feat(aura-engine,aura-cli): named param binding — single-run .with()/.bootstrap() (0030 iter 1)
Bind a blueprint's open knobs by name for a single run instead of by a positional,
Scalar-wrapped Vec in param_space() order:
bp.with("sma_cross.fast", 2).with("sma_cross.slow", 4).with("scale", 0.5).bootstrap()
A pure authoring layer over the existing Composite::param_space / bootstrap_with_params
primitives; the engine run loop and the construction core are untouched (C1/C12/C19/C23
preserved). Raw literals lower via Into<Scalar> (the literal fixes the variant: 2->I64,
0.5->F64, pinned by
|
||
|
|
a665a966a7 |
feat(aura-engine): param-sweep grid primitive (the World, cycle C / iter 1)
Turn one value-empty blueprint into a family of disjoint instances over a
declared grid, run them in parallel, and collect per-point metrics. This is
C12.1's grid axis — the first "N instead of 1": #30/#31 made a blueprint
param-generic and bindable; this reaps it.
New module crates/aura-engine/src/sweep.rs, three parts:
- GridSpace: a validated cartesian grid over a blueprint's param_space() —
one discrete value-list per slot, kind/arity/non-empty checked at
construction (SweepError::{Arity, KindMismatch, EmptyAxis}, the typed gate
before any run). points() enumerates in odometer order (last axis fastest),
deterministic.
- sweep(): closure-driven. The engine owns enumeration + disjoint execution
+ collection; the author owns run_one: Fn(&[Scalar]) -> RunMetrics (build
fresh -> bootstrap_with_params -> run -> drain -> summarize). Metrics
reduction is harness-specific sink glue the engine cannot generically own
(C8/C18), and a fresh per-point build resolves #31's two structural facts:
bootstrap_with_params consumes the blueprint, and a Recorder bakes its
channel in — so each point gets its own drainable instance.
- SweepFamily / SweepPoint: ordered, self-describing (params carry the point
coordinate), in enumeration order independent of thread completion.
Execution is std::thread::scope over available_parallelism() workers pulling
point indices from a shared atomic cursor (work-stealing balances uneven
per-point cost); results are tagged by index and sorted after join, so the
family is bit-identical across thread counts and repeated runs (C1: disjoint,
lock-free; order = enumeration, not completion). No external dependency —
std only, never rayon (C16: the engine workspace stays zero-dep for deploy
purity). A private sweep_with_threads(nthreads) lets the tests pin
determinism at 1 vs N workers while the public sweep() derives the count.
Out of scope this iteration: the `aura sweep` CLI demonstrator (cycle 0028
iter 2); random enumeration, per-point manifest assembly (#33), value-domain
validation (C20) are deferred per the spec.
One deviation from the plan's verbatim code: GridSpace derives Debug
(load-bearing — the three .unwrap_err() fault tests need the Ok type to be
Debug to compile).
Verified: 8 new sweep:: tests green (enumeration/odometer order, the three
typed faults, sweep == N independent runs, determinism across thread counts,
distinct-points-distinct-metrics); cargo test --workspace green (92 engine
lib tests, no regression); cargo clippy --workspace --all-targets
-- -D warnings clean.
refs #32
|
||
|
|
288f58953e |
feat(aura-engine): graph model serializer (0026 it1)
Iteration 1 of the graph render redesign: a read-only model_to_json(&Composite) that serializes the harness root + every distinct composite type into the canonical, deterministic JSON model the browser viewer will consume. New crates/aura-engine/src/graph_model.rs; lib.rs re-exports model_to_json. - House style: hand-rolled JSON like RunReport::to_json, NO serde (C14). - Model shape (faithful to the types, resolving the spec's prototype-flavoured example): node keys are indices (C23, the blueprint has no unique names); input ports carry kind+firing only, no name (PortSpec has none — acceptance criterion 3); a vec![] output is the C8 sink; bound root source-roles are minted as synthetic src_<role> source nodes; composite boundaries use @role (input) and #N (output) endpoints; distinct composites collected once. - Improvement over the plan: a composite's input kind is read from the actually fed interior slot (matching blueprint::derive_signature) rather than a hardcoded f64 — invents nothing. - Read-only (C9): &Composite -> String, no eval/compile/bootstrap on the path. - 10 new tests green: byte golden on the sample harness, determinism, structural mis-wire differs (param swaps are invisible to the pre-compile model — the property is carried by a structural re-target), read-only witness, sink / two-input / distinct-once structural assertions. Workspace green, clippy clean. aura graph still renders via ascii-dag (untouched); iteration 2 ports the viewer JS, vendors the WASM-Graphviz asset, emits the self-contained HTML, and retires ascii-dag. |
||
|
|
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
|
||
|
|
69d20949c0 |
tidy(aura-engine): single-own alias-validity + shared alias-count anchor
Collapse the two correctness-neutral debt items the 0021 fan-in pre-pass left behind (architect drift review at cycle close). Alias-validity: the `BadInteriorIndex` check was raised in two places — the structural pre-pass `check_composite_fan_in` and `inline_composite`. Since `compile_with_params` runs the pre-pass over every composite before any lowering, `inline_composite`'s copy was dead for that error path. The check now lives in one helper `check_alias_indices`, called solely by the pre-pass; `inline_composite` drops its copy (and the now-unused `param_aliases` binding — the overlay is a pure naming layer, unused in lowering, which is driven by the injected scalar vector). Alias-count: the per-node `a.node == node` predicate was re-derived in three spots (signature base, the unaliased-param test, the CLI render base-length). All three now route through one exported anchor `aliases_on`, so an alias-semantics change touches one site. Behaviour-preserving: the full workspace suite is the guard (15+6 cli, 25 core, 69 engine, 6+1+1 ingest/misc, 29 std — 0 failed), `out_of_range_param_alias_rejected` still green (now raised by the pre-pass), clippy -D warnings clean. No ledger change (the C9 refinement landed in 0021). closes #45 |
||
|
|
3f4d756ed2 |
feat(aura): fan-in input distinguishability — construction constraint + source-derived render
A fan-in node (>1 input) whose colliding sources hide an unnamed configuration axis is now illegal at construction, and the definition view renders each fan-in input as a source-derived recursive-signature identifier instead of the positional, meaningless #A/#B. The root defect was sma_cross: two Sma into a Sub with unaliased length params, rendering sma_cross() -> (cross) with two indistinguishable [SMA] and [Sub(#A,#B)] — the author meant fast vs slow but the identity hid it. Now it renders sma_cross(fast:i64, slow:i64) with [SMA(fast)]/[SMA(slow)] and [Sub(#Sf,#Ss)]. Shape (single source of truth for "signature" shared by engine + CLI): - aura-engine signature_of(): a node's recursive authoring identity — type initial + alias initials + each wired input's signature (recursing into interior leaves, stopping at named roles/composites at their initial). - aura-cli leaf_label/fan_in_identifiers: shortest sibling-unique prefix of a source's signature, never below its base (type + alias initials); a role-fed slot renders its name verbatim (#price); equal-signature interchangeable inputs keep the positional letter. - aura-engine IndistinguishableFanIn: a signature collision is a fault only when a colliding source carries an unaliased param (the unnamed axis); param-less interchangeable sources (fan_composite's Pass/Pass) stay legal. Param-aware criterion (refined during design): keeps the blast radius to the param-bearing alias-less fixtures (sma_cross CLI + engine, the nested fast_slow); fan_composite and the hand-wired flat fixtures stay green. Placement decision (deviates from the plan): the constraint runs as a structural pre-pass (check_fan_in_distinguishability) at the head of compile_with_params, BEFORE the param-arity gate — not inside inline_composite as the plan drafted. Reason: the no-param compile() of a param-bearing composite would hit ParamArity first and preempt the fault; the spec mandates a structural check needing no param values, so the pre-pass is the spec-aligned home. Alias-validity ordering (BadInteriorIndex before the fan-in fault) is preserved at the pre-pass head. C23 untouched: the check is construction-phase; the compilat stays name-free (compiled_view_golden byte-stable). Ledger C9 carries the refinement. Known debt (non-gating): the alias-index validity check now exists in both inline_composite and the pre-pass (the pre-pass reaches every composite first, making inline_composite's copy effectively dead). Left for a separate tidy — a 5-line, correctness-neutral removal with its own test surface. closes #44 |
||
|
|
41cbb5506f |
feat(aura-engine): name the composite boundary — input roles + param aliases
Brings the other two composite-boundary edge-kinds to the named-projection shape #40 gave outputs. input_roles changes from a bare Vec<Vec<Target>> to Vec<Role { name, targets }> (rendered [in:<name>]); a composite gains params: Vec<ParamAlias { name, node, slot }> that relabels an interior leaf param slot's surface name in param_space() (rendered [param:<name>]). Param aliasing is a PURE NAMING OVERLAY, not curation (the load-bearing decision, per spec 0019): every interior param slot stays in param_space() and sweepable; the alias only relabels in place, never reorders or hides. Proven empirically — the MACD run is byte-identical (total_pips 0.1637945563898923, 3 sign flips), only the param labels improved: param_space() now surfaces [macd.fast, macd.slow, macd.signal] instead of three indistinguishable macd.length, and the manifest reads ema_fast/ ema_slow/ema_signal. C23 honoured: role/param/output names are non-load-bearing debug symbols dropped at lowering; identity is positional (role index, param slot, output field). compiled_view_golden is byte-identical (verified: the golden region is untouched in the diff). An out-of-range alias (missing/ non-leaf node or slot past the leaf's param count) is rejected at compile_with_params as BadInteriorIndex, mirroring the output range-check (no new variant). Orthogonal to #36 — purely additive at the composite level. Aliasing is demonstrated on the CLI MACD site only (the spec's worked example + a new E2E test macd_param_space_surfaces_the_three_named_aliases); sma_cross, the engine test fixtures, and the construction-layer fieldtests get the forced role-name + empty params, so the param_space C23 anchor goldens (param_space_mirrors_compiled_flat_node_param_order + siblings) stay byte-identical. Verification (orchestrator-run, not trusted from the agent report): cargo build/test/clippy --workspace -D warnings all green (engine 66, cli 12); the separate-workspace construction-layer fieldtest crate builds (guards the #42 latent-drift recurrence); compiled_view_golden + MACD determinism unchanged. Two faithful repairs to the plan's literal test/code bodies, no semantic change: the out-of-range test uses .err()/Some(BadInteriorIndex) (the Ok arm Vec<Box<dyn Node>> is not Debug, so .unwrap_err() would not compile), and the inline_composite destructure binds params as `param_aliases` to avoid shadowing the injected `params: &[Scalar]` arg. closes #41 |
||
|
|
784e6c917a |
feat(aura-engine): composite output is a named multi-field record
Replace a composite's single output port (OutPort { node, field }) with
an ordered, named record (Vec<OutField { node, field, name }>). A
composite can now re-export K interior fields as one output; a consumer
selects one via the existing Edge::from_field — the same field-wise
selector that already works for multi-output leaf producers (OHLCV).
Why: multi-line indicators (MACD, Bollinger, Stochastic, Ichimoku) could
not be authored as a composition and re-exported as a unit — only one
line escaped the boundary. The MACD PoC was forced to expose only the
histogram. This lifts the three `field == 0` / `from_field == 0` caps at
the composite boundary (inline_composite nested arm, rewrite_edge
composite arm), range-checking the index instead.
Boundary completion, not an invariant change:
- C8/C7/C4 untouched — one port, one row per eval, K base columns (a
3-line MACD is identical in kind to OHLCV).
- C23 honoured — re-export names live at the blueprint boundary only and
are dropped in the compilat (raw index wiring). compiled_view_golden
stayed byte-identical (the regression guard): no name leaked into the
flat graph.
The MACD PoC now re-exports macd / signal / histogram as a record; the
strategy still trades the histogram, selected via from_field: 2. The
render shows K [out:<name>] markers per multi-output composite (input
roles stay [in:<index>] — naming them is the dependent #41).
Output naming ships with the capability (OutField is born name-ready) so
#41 — composite param aliasing + input-role naming — does not reopen the
type.
Verification: cargo build/test/clippy --workspace -- -D warnings all
green; aura-engine 62 tests (+3 capability, +1 through-run E2E), aura-cli
unchanged-count green; compiled_view_golden byte-identical.
Known debt (out of scope, pre-existing): the non-workspace construction-
layer fieldtests (fieldtests/milestone-construction-layer/mc_1..mc_4)
carry their OutField sweep but still do not compile standalone — they use
the Sma::new / BlueprintNode::from(Sma) API retired in the cycle-0016
value-empty migration, never propagated to these fixtures. Tracked as a
follow-up.
closes #40
|
||
|
|
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 |
||
|
|
561482c422 |
tidy(aura-engine): re-export scalar vocab (#29) + drop duplicate Recorder fixtures (#14)
Two behaviour-preserving tidies from the post-0013 fieldtest/backlog. #29: aura-engine re-exports Firing/Scalar/ScalarKind/Timestamp at the crate root. A Blueprint builder needs ScalarKind for SourceSpec.kind and Scalar/ Firing/Timestamp for sources & Recorder columns, but aura-engine previously re-exported only the wiring types — forcing a second aura-core import for the vocabulary its own public structs (SourceSpec.kind, ...) demand. The fieldtest flagged this paper cut (mc_4). One import surface now. Guarded by a new reexport_tests module. #14: the two near-identical #[cfg(test)] Recorder fixtures in report.rs and harness.rs are deleted in favour of the shipped aura-std::Recorder (already a dev-dependency). Constructor signature, schema, and try_iter() drain are identical, so no call site changed — only the struct/impl deletion plus adding Recorder to each file's existing aura_std import. report.rs additionally drops four imports (Ctx/InputSpec/Node/NodeSchema) that only the deleted fixture used. The separate TapForward fixture in harness.rs is untouched. Verified: cargo build/test/clippy --workspace all green (49 aura-engine lib tests incl. the new one); bit-identical, golden-snapshot, SMA-disambiguation, and mixed-kind recording tests all stay green — behaviour preserved. closes #29 closes #14 |
||
|
|
a4fb5d7182 |
feat(aura-engine): blueprint construction layer with composite inlining
Add the construction layer (C9/C19/C23): a named, param-generic graph-as-data (`Blueprint`) that *compiles* to the flat, type-erased instance the run loop already runs. The unit of reuse is the `Composite` — a nestable sub-graph fragment exposing one output port (C8) and named input roles — which `Blueprint::compile()` **inlines** by raw-index lowering into the flat `(nodes, sources, edges)` the unchanged `Harness::bootstrap` consumes. Design (settled in spec 0012, ledger C9/C19/C23): a composite is an authoring-level node, NOT a runtime `Box<dyn Node>` sub-engine. The chosen inlining approach keeps the sacrosanct deterministic run loop untouched and leaves the composite interior fully inspectable for the deferred cross-graph optimiser; the rejected "runtime sub-engine" reading would have put throwaway complexity into the run loop and kept the interior opaque. The compilat is wired by raw index, not by name; names survive only as non-load-bearing debug symbols (as `FieldSpec.name` already is). Lowering is recursive index rewriting: interior items append at an offset, interior edges rewrite by that offset, an edge into a composite fans through its input roles (one blueprint edge -> several flat edges), an edge out resolves to the interior output port, and nesting recurses inside-out. Construction-phase faults are caught as a typed `CompileError` (BadInteriorIndex / RoleKindMismatch / OutputPortOutOfRange); the lowered flat compilat is validated by bootstrap's existing kind- and Kahn-cycle-check (wrapped as `CompileError::Bootstrap`), with no re-implementation. Non-goals (deferred per C23/C16): no optimisation pass (CSE/DCE, sweep-invariant hoisting), no external optimisation crate, no named-handle ergonomic wiring, no `aura graph` render (#13). Verification: 48 engine tests green (8 blueprint: derived-schema, single + nested composite inlining, the three CompileError paths, bootstrap-error wrap, and the headline `composite_sma_cross_runs_bit_identical_to_hand_wired` C1 demonstrator — the SMA-cross composite lowers to a flat graph byte-identical to the hand-wired sample harness, producing bit-for-bit identical equity + exposure traces); `cargo clippy --workspace --all-targets -D warnings` clean. `Node`, `Harness::bootstrap`, the run loop, and `Edge`/`Target`/`SourceSpec` are unchanged. closes #12 |
||
|
|
80d7bfbbf0 |
feat(aura-engine): run metrics + manifest report surface
Cycle 0009 (Walking skeleton milestone). A run's two C18 "from day one"
artefacts — a reproducible manifest and summary metrics — land as a new,
pure-additive `report` module in aura-engine:
- summarize(equity, exposure) -> RunMetrics: a post-run pure reduction over a
run's recorded pip-equity + exposure streams. total_pips (last cumulative
value), max_drawdown (worst running-peak-to-trough), exposure_sign_flips (a
turnover proxy: adjacent normalized-sign changes, flat distinct from
long/short via a three-way sign0). Total and pure: empty -> zeros, identical
inputs -> identical metrics (C1/C12).
- RunManifest: the reproducible descriptor (commit, params as name->value
pairs, data-window, seed, broker label). Caller-supplied — the engine cannot
introspect a git commit or seed. params is the honest precursor to the
deferred typed param-space (node.rs; 0008 audit).
- RunReport { manifest, metrics } + to_json(): canonical, hand-rolled JSON for
the structured C14 face.
- f64_field(rows, field): bridges a recording sink's Vec<Scalar> rows to
summarize; panics on a kind/width mismatch (a wiring bug), like the engine's
other "checked at wiring" violations.
Why post-run, not a node: a node emits one record per eval with no terminal
eval (C8), so an end-of-run reduction has no home as a node; the World drains
its cycle-0006 recording sinks after Harness::run and folds them here. Matches
C18's "store manifests + metrics, re-derive results on demand".
Why hand-rolled JSON, not serde: the workspace is deliberately zero-dependency
and the schema is tiny/closed/flat; serde is reconsidered when the run-registry
milestone grows the schema. (User-approved at spec review.)
Scope: the surface + a determinism end-to-end test reusing the cycle-0007
two-sink harness; the `aura run` subcommand that prints the report is #8. No
engine/Harness/node-contract change; workspace stays zero-dependency.
Verified: cargo test --workspace green (aura-engine 40, incl. 10 new report
tests); clippy --all-targets -D warnings clean; RUSTDOCFLAGS=-D warnings cargo
doc clean.
closes #6
|
||
|
|
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>
|
||
|
|
02fe89176f |
audit: cycle 0003 tidy — rename Sim -> Harness (glossary alignment)
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). |
||
|
|
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
|
||
|
|
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> |
||
|
|
d8ccdcd806 |
docs: fix three drifts found in the session audit
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> |
||
|
|
744c2f31a8 |
chore: scaffold aura workspace and wire the skills dev-cycle
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> |