953d04a77495ef19c6eb1511626ecb8ebab2e8ba
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
953d04a774 |
feat(engine): composite doc field — authoring, serde, identity, model emit (refs #125)
Composite gains doc: Option<String> (the prose twin of name, a C23 debug symbol): with_doc/doc() on Composite, a doc knob on GraphBuilder, a Tier-1 additive-optional CompositeData field (no format-version bump; absent-field documents keep their exact bytes), identity-stripped like the name. The graph model emits an optional trailing doc fragment in both scopes; json_str is hardened for free text (\n/\t/\r named, control chars as \u00XX — the doc is the first multi-line value through it). e2e: a hand-authored doc'd blueprint renders with the doc embedded; the doc moves the content id but never the identity id. refs #125 |
||
|
|
bc88e18247 |
feat(engine,cli): GraphBuilder gang cadence + render carrier (#61 tasks 5-6)
Builder: NodeHandle::param -> ParamRef, GraphBuilder::gang, build() resolves
the gang table (collect-then-reject, BuildError::{UnknownParam,AmbiguousParam,
BadGang}) and terminates in Composite::with_gangs — the same predicate the
op-script and serde boundaries use. C24 lockstep extended: an op-script gang
replay serializes byte-identically to the GraphBuilder twin (FlatGraph edges +
sources + canonical JSON).
Render: model_to_json emits a per-scope gangs fragment (name, kind, [node,pos,
"param"] members) for ganged scopes only — the gang-free model stays
byte-identical (model_golden untouched). graph-viewer.js annotates a ganged
open param with its public knob name; guarded by a viewer_gang_param.{mjs,rs}
pair RED-first per the established viewer_bound_param convention (a welcome
extra beyond the plan's file list).
Ratified deviation: the model's kind string uses the serde form ("I64") — the
plan's implementation snippet (kind_str -> "i64") contradicted its own test;
resolved toward the test + the blueprint_serde form. Two quality nits held as
principled plan-holds (inline resolution block; the unreachable unwrap_or
placeholder check_gangs refuses first).
Verified: aura-engine lib 264/0 (model_golden, lockstep, builder, carrier all
green), viewer_gang_param 1/0, workspace clippy -D warnings clean. Slice
resumed across a transient API failure via workflow resume (cached prefix).
refs #61
|
||
|
|
07a73fb66e |
docs(cli,ledger): sweep #159 demo-retirement drift — stale comments + ledger notes
Cycle-close audit tidy for the #159 demo-retirement arc: behaviour-free reword of prose/comments/docs left pointing at symbols the arc deleted. Adversarially verified by the audit workflow; cargo build --workspace --all-targets green. - README: drop the removed `--harness` token; restrict the legacy built-in form claim to walkforward/mc/generalize (run/sweep no longer resolve one). - glossary: the built-in `--strategy` sweep surface is retired (#159), not a live inline path. - INDEX.md ledger: append [HISTORY] supersession markers (house style) to the dated realization notes advertising the retired `--harness` / built-in `--strategy` CLI forms (cost-flag #153, cycles 0065/0066/0067, name-res, #210 status); historical text preserved. - aura-engine test-fixture docs: drop dead cross-crate refs to the deleted aura-cli fns sample_harness / build_sample and the deleted `macd` composite. - cli_run.rs: reword the #159-cut-2 pin doc to the current clap generic-usage reality (no strategy_from / Strategy::RBreakout); r_sma_sweep_family -> blueprint_sweep_family. - verb_sugar.rs: fix the pre-existing `E[R]` unresolved-doc-link warning. Remaining tidy — the main.rs comment cluster plus its rustdoc link at :1546 (the second cargo-doc warning) — held for a follow-up issue. The dead cost-graph branch is tracked in #221. refs #159 |
||
|
|
4a3ae05db2 |
test(graph_model): cover a producer backing >1 re-exported output field
#47 — close a coverage gap in the graph model serializer: a single interior producer node backing more than one re-exported output field was never exercised by a fixture (every existing one maps each output to a distinct producer or a single field). The issue was filed at cycle 0022 against `aura-cli/src/graph.rs`'s `render_definition` / `output_binding` helper, which rendered a textual `(n1, n2) := <producer>` tuple binding. That file and helper no longer exist — the textual definition renderer was replaced by the JSON model emission now in `aura-engine/src/graph_model.rs`, where the multi-OutField case is plain flat iteration over `c.output()` (one `[name,kind]` entry + one `#N` boundary binding per `OutField`, by `(node, field)`), not a special tuple-binding arm. So the original branch is gone; this guards the flat emission that replaced it. New test `multi_outfield_producer_emits_one_binding_per_field` (+ two fixtures): a two-field producer (`hi`:f64, `lo`:i64 — distinct kinds, so the per-field `field_kind` lookup is exercised) re-exported as both composite outputs, nested in a root so it renders through `composite_def_json`. Asserts both fields surface with their own kind (`"outputs":[["hi","f64"],["lo","i64"]]`) and both bindings reference the same producer at distinct ordinals (`0.o0`/`#0`, `0.o1`/`#1`). Test-only; no production code touched (the behaviour was already correct). Verified: cargo test -p aura-engine green (237 in the lib suite); clippy clean. closes #47 |
||
|
|
2c43296c2c |
feat(stage1-r): bias rename + stop-rule + position-management + summarize_r (iter 1)
Iteration 1 of the Stage-1 "R-based signal quality" cycle (spec 0065): a strategy's signal quality, measured in R on its unsized bias stream, feed-forward. New discrete-trade machinery, NOT a SimBroker extension. What lands: - exposure -> bias (refs #126): the Exposure node/type/file/output-field renamed to Bias (computation unchanged: clamp(signal/scale,-1,+1); the SEMANTICS change — the output is the unsized strategy bias, sizing leaves the strategy). Scoped to the semantic core; behaviour-preserving cosmetics are deferred (see below). - stop-rule nodes (refs #119): VolStop = k*EMA(|price - prev_price|) (one fused node; the volatility that defines 1R, close-to-close — true-range ATR deferred, needs OHLC) + FixedStop (constant, the test fixture / fixed-vs-vol structural-axis sibling). - PositionManagement (refs #127): the stateful heart. Latches the entry-cycle stop distance as the FROZEN R-denominator (never re-read), marks/exits no-look-ahead like SimBroker (a position earns from the next cycle; an exit realises against the cycle's close), and emits ONE dense per-cycle R-record (C8). Stop-outs are NOT capped at -1R (a gap through the stop realises R < -1 — the honest loss tail); R is computed size-invariantly (size = (exit-entry)*dir / latched_dist cancels). The trade ledger is the rows where closed_this_cycle; the R-equity is cum_realized + unrealized; a position open at window end is the last row (open=true) — explicit, never silent MtM. - summarize_r + RMetrics (refs #129): the post-run fold (NOT an in-graph node — recorders drain post-run). E[R], win-rate, profit-factor, avg win/loss R, by-trade max R-drawdown, n_open_at_end (window-end force-close). SQN / conviction terciles / net-of-cost / RunMetrics.r are iteration 2. Design adversarially hardened before implementation (12-juror refute panel; decisions on #117). Ratified implementer deviations from the plan snippet, verified by hand: - col-4 `direction` tracks the OPEN position at cycle end (window-end synthesis; names the reopened leg on a reversal), falling back to the closed trade's dir only when flat. summarize_r does not read col 4; the R-metrics are unaffected. - .named("exposure") kept on the 4 previously-unnamed Bias nodes so the auto-derived param-path stays exposure.scale (no manifest/dir-name drift) — the unnamed nodes would otherwise flip to bias.scale. - intra-doc links [`Exposure`] -> [`Bias`] in sim_broker/latch + the sample-model test pin (cosmetic, behaviour-neutral; broken intra-doc links fail cargo doc). - the E2E drives a full bootstrapped Harness (stronger than the plan's direct-node drive — exercises the real cross-crate producer->consumer seam). Deferred (behaviour-preserving, separate cosmetic pass — NOT this iteration): RunMetrics.exposure_sign_flips / Metric::ExposureSignFlips, SimBroker/LongOnly "exposure" input ports, the "exposure" tap/trace names, and the .named("exposure") instance labels. Iteration 2: the Sizer seam, the RiskExecutor composite (+ the Veto documented-seam), summarize_r enrichment, RunMetrics.r, and the CLI/recording surface. Verification (orchestrator-run, not agent-claimed): - cargo build --workspace: clean. - cargo test --workspace: all green, 0 failed (incl. the new bias/stop_rule/ position_management unit tests, the summarize_r arithmetic tests, and the stage1_r_e2e capstone + layout guard). - cargo clippy --workspace --all-targets -- -D warnings: clean (exit 0). - Keystone RED tests pass: no_lookahead_bias_exit_realises_the_held_move, stop_out_is_not_capped_at_minus_one_r, no_gap_stop_is_exactly_minus_one_r, reversal_closes_one_leg_and_reopens, open_at_window_end_is_carried_on_the_last_row. refs #117 #119 #126 #127 #129 |
||
|
|
82635fa259 |
refactor: Cell becomes the hot-path value carrier (C7/C8 realization)
Motivation ---------- The C7 realization note ( |
||
|
|
cd3d1ca9ed |
refactor(aura-core): split Scalar into a tag-free Cell + ScalarKind
Motivation
----------
`Scalar` was a tagged enum (I64/F64/Bool/Ts), so every scalar value
physically carried its own kind tag. But the kind is already known from
the schema/port/column the value flows through (C7: the type is a
property of the column, not of the value — the hot path is already
columnar `Column<T>`, and `AnyColumn::get` *reconstructs* the tag from
the column on the way out). The per-value tag was therefore redundant
with the kind the surrounding context already holds.
That redundancy had three costs:
* It baked an implicit `match` (a branch) into every function that read
a Scalar payload — even where the caller statically knew the type.
The tag could never be exploited away.
* Size: a tagged enum is tag + payload = 16 bytes (f64/i64 alignment),
twice the 8 bytes the value needs. A `Column<Scalar>` would be double
the memory and half the cache utilisation.
* It is the shared root of several downstream papercuts we keep hitting
— the lossy f64 manifest field, the `unreachable!` panic on a
non-numeric param, the serde-tag question — all symptoms of "the type
is baked into the value".
Change
------
Introduce `Cell`: a type-erased 64-bit word (`struct Cell(u64)`) that is
not readable without external type context. It is constructed per base
type (`from_i64/from_f64/from_bool/from_ts`) and read only by naming the
type at the call site (`i64()/f64()/bool()/ts()`) — each a branch-free
bit-cast. The hot path resolves the kind once at the boundary (from the
schema) and then reads natively, with no per-value branch. `Cell` knows
nothing of `Scalar` or `ScalarKind`; the dependency is strictly one-way,
and it lives in its own `cell.rs` (more is planned on top of it).
`Scalar` becomes `struct { kind: ScalarKind, cell: Cell }` — the
self-describing form for the dynamic boundaries (builder binding,
serialization, rendering), built on top of `Cell`. Its `as_*` accessors
now `debug_assert` the kind and return the native value (free in
release); calling the wrong accessor is a caller bug, not a checked
`Option`. The variant constructors `Scalar::I64(..)` become associated
fns `Scalar::i64(..)`.
`PartialEq` is hand-written (not derived) to preserve the former enum's
value semantics: kinds must match, then native payloads compare, so f64
keeps IEEE-754 behaviour (`NaN != NaN`, `+0.0 == -0.0`) and a kind
mismatch is never equal even when the raw words coincide. A fixture
(`scalar_eq_is_value_not_bitwise`) pins exactly the cases where bit- and
value-equality diverge, so it can't silently regress. `Cell`'s own
`Eq`/`Hash` stay bitwise — correct for a raw word.
The change is behaviour-preserving: Scalar's observable behaviour is
identical to the pre-Cell enum (the value-equality fixture proves it);
only the internal representation changed. The ~440 call sites across the
workspace are a mechanical constructor rename plus ~12 destructuring
sites (match-arms / `let`-patterns) rewritten to `kind()` + `as_*`.
Verified: cargo build --workspace --all-targets, cargo clippy --workspace
--all-targets -- -D warnings, cargo test --workspace — all green.
|
||
|
|
6021ce6aeb |
docs(aura-engine): drop two stale aura-cli cross-references in graph_model
kind_str and collect_distinct_composites no longer exist in aura-cli (it consumes model_to_json directly), so the "mirrors aura-cli's ..." notes pointed at nothing. Keep the substantive rationale (Debug is PascalCase). |
||
|
|
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. |
||
|
|
47e0e605e1 |
feat(aura): surface bind-bound params in the graph model + viewer signature
A node built with `.bind(slot, value)` fixes a declared param to a structural
constant: the slot correctly leaves the tunable surface (param_space / sweep axes),
but it also vanished from the RENDERED signature, because prim_record emitted only
schema.params. The cycle-0036 `blend` (a LinComb(3) with weights[2] bound to 0.5)
rendered `LinComb[weights[0], weights[1]]` — a 2-arity signature over a 3-port box,
with the fixed 0.5 invisible. The signature misrepresented the node.
bind now ANNOTATES instead of erasing. All slots are shown; the bound one renders
`name=value`, dimmed. The blend renders:
blend: LinComb[weights[0], weights[1], weights[2]=0.5]
Structurally a twin of cycle 0035's instance-name thread (commit
|
||
|
|
d069484559 |
audit: cycle 0036 — refresh stale sample cross-refs; drift-clean otherwise
Close-audit for cycle 0036 (commit range 560c2d0..94af4c7). Architect drift review against the design ledger + CLAUDE.md: no contract drift. No regression scripts are configured (the architect is the gate); build + test + clippy verified green. cycle 0036 tidy (clean): - C8/C9/C10 preserved: the enriched sample is multiply-nested composites over shipped primitives — multi-output is the macd composite re-exporting columns (not a >1-record primitive); brokers stay downstream nodes. - C12/C19 preserved: the eight sweep axes are a bijection with the injective, path-qualified param_space under nesting; the bound blend.weights[2] is correctly absent from both the axes and the surface. The re-captured render golden, the two in-crate pins, and the cli_run E2E pin all carry the identical eight-tuple in lockstep. - C14 preserved: the re-captured sample-model.json is the deterministic model of the enriched sample; the engine's own minimal model_golden (sample_root) is independent and byte-unchanged. - C23 preserved: bind resolves the param name to a fixed position; the fixed blend weight is a structural constant (deform-not-tune), dropped from the tuning surface, and the compilat is unchanged. Two stale cross-references found and refreshed (doc debt, no behaviour change): - crates/aura-engine/src/graph_model.rs: sample_root's "Mirrors build_sample() (...:161-190)" was doubly stale — build_sample moved and the CLI sample is now the richer signals graph; re-grounded to state sample_root is a deliberately minimal, independent serializer fixture. - crates/aura-cli/src/main.rs: sample_blueprint_with_sinks' "single source of the sample topology" + "SMA lengths + exposure scale" overclaimed post-enrichment; re-grounded to the root harness whose signal is the nested signals composite, with the eight free params. |
||
|
|
0ae8320d14 |
feat(aura): surface explicit node instance name in the graph viewer
A leaf primitive built with `.named("fast")` now renders its `aura graph` viewer
box head as `fast: SMA[length]` — the instance name as a `:` declaration prefix.
An unnamed leaf keeps the bare `SMA[length]`.
Engine half: a new raw `PrimitiveBuilder::instance_name() -> Option<&str>` (the
explicit name, default not resolved) feeds a conditional leading `"name"` field
in prim_record, present only for an explicitly-named node; both golden twins
(inline model_golden + sample-model.json) re-captured. Viewer half: adaptNodes
carries `name`, the genDot leaf-emit forwards it, and cellLabel prepends the
`name: ` prefix when present (composites stay bare). `named()`'s non-empty
debug_assert is unchanged, its doc re-grounded to the now load-bearing Some/None
invariant (knob-address segment + prefix switch).
The name is a render/model-only debug symbol — dropped at lowering (C23), and the
model stays deterministic (C14). Parameter ganging (one knob, several nodes) was
considered and spun off as an explicit composite-shared-param idea (#61), not via
name collision (param_space is injective, C12/C19).
closes #58
|
||
|
|
52e0214adb |
audit: cycle 0033 (#54) — refresh stale JSON-writer cross-refs after to_json→serde
Architect drift review over 065cf1d..HEAD. Code + contracts hold; the only drift was documentation cross-references made stale by retiring RunReport's hand-rolled to_json. Contracts (all hold): - C14: to_json still emits canonical, flat, deterministic JSON — now serde-produced. model_to_json (the graph-model JSON) stays hand-rolled and was correctly untouched. - C16: serde_json dev-dep → normal-dep in aura-engine is squarely sanctioned by the amended per-case policy (INDEX.md:576-583) — a vetted standard crate used where it does the job (incl. the frozen bot), removing a hand-rolled writer. No gap. - C1: report types have no HashMap; serde is declaration-order/deterministic; the r1.to_json()==r2.to_json() asserts and the new to_json_equals_serde_disk_shape pin stay green. Regression: none configured (profile regression: []) — no-op; architect is the gate. Resolution — fix (3 prose cross-refs, fixed inline this commit): - graph_model.rs:5 (module doc) — claimed the model JSON is in the "RunReport::to_json house style (no serde)"; to_json is serde now. Reframed: the model JSON is hand-rolled (no serde), and is the engine's last hand-rolled JSON writer since 0033. - graph_model.rs:30 (json_str doc) — cross-referenced RunReport::json_str, deleted this cycle. Reworded to describe the escape set directly (graph_model's own json_str stays). - docs/design/INDEX.md:729 — "golden-tested like RunReport::to_json" framed the hand-rolled model JSON by a now-serde to_json. Reframed to name model_to_json as the surviving hand-rolled writer. Cycle 0033 drift-clean after this tidy. Not a milestone close (the param-sweep milestone fieldtest is a separate deliberate act). Gates: clippy --workspace --all-targets -D warnings clean (doc-only edits, compile-inert). refs #54 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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. |