Commit Graph

10 Commits

Author SHA1 Message Date
Brummel 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
2026-07-10 13:03:10 +02:00
Brummel ea1ca32dbd feat(0088): §A shared gate predicates — edge_kind_check, resolution helpers, try_bind
Iteration-1 §A of the construction service (#157): the surface-agnostic shared
predicates the eager op-script path and the holistic finalize gates both call —
no second validator (C24).

- edge_kind_check (blueprint.rs): the per-edge producer/consumer kind check,
  lifted verbatim out of validate_wiring's edge loop into a pub(crate) fn that
  validate_wiring now calls — behaviour-preserving (same Bootstrap(KindMismatch)
  variant; the existing compile-time gate test stays green).
- resolve_input_slot / resolve_output_field (builder.rs): the exactly-one-match
  name→index resolution extracted as pub(crate) fns over (&NodeSchema, &str), so
  the runtime-String op-script names share GraphBuilder's resolution; GraphBuilder
  delegates and maps to the unchanged BuildError variants.
- try_bind + BindOpError (aura-core node.rs): the fallible Result twin of bind
  (bind keeps its panic contract and pinned messages verbatim; try_bind is a
  separate method reporting the same three conditions as values).
- check_param_namespace_injective + validate_wiring widened to pub(crate) for the
  finalize-stage reuse by the upcoming GraphSession::finish (§B).

Partial iteration: §B (the GraphSession/Op/replay surface + introspection) and
the §A→§B integration land next (plan Tasks 4-8). compile_with_params /
check_ports_connected are behaviourally unchanged. Full suite green; clippy clean.

Plan correction folded in: Task 3's test originally referenced aura_std::Sma —
infeasible inside aura-core (aura-std depends on aura-core; the reverse edge is a
dependency cycle). Adapted to a local PrimitiveBuilder probe with identical
assertions.

refs #157
2026-06-29 18:38:34 +02:00
Brummel 68605b7d88 refactor(stage1-r): rename the strategy-output param namespace exposure -> bias
Complete the deferred rename tail from cycle 0065's #126 (which renamed only the
typed metric, keeping the param namespace uniformly "exposure" so the single-run
rename would not smuggle in a ~30-site sweep-pinned change). Now renamed together
as one consistent unit, all driven by the Bias node's instance name:

- Bias::builder().named("exposure") -> .named("bias") (the harness builders +
  the engine fixtures in test_fixtures.rs / blueprint.rs);
- the derived knob path exposure.scale -> bias.scale (Composite::param_space
  derives the knob from the node name) -- the sweep .axis/.range bindings and
  .with() points (blueprint.rs, main.rs), the walkforward ParamSpecs
  (walkforward.rs), the member_key dir names, and the JSON byte-pins in
  cli_run.rs / report.rs / main.rs tests;
- the exposure_scale manifest param label -> bias_scale (the single-run manifest
  builders + the streaming_seam / real_bars fixtures).

The rename is surgical: the three targets (named("exposure"), exposure.scale,
exposure_scale) are syntactically distinct from the deliberately-kept "exposure"
forms, which are untouched -- SimBroker's pre-reframe exposure input slot +
prev_exposure + the exposure-integral; the LongOnly / gate / latch ports named
"exposure"; and the on-disk "exposure" tap label (the recorded bias series that
feeds `chart --tap exposure`, decoupled from the node name via
ColumnarTrace::from_rows("exposure", ...)).

No on-disk back-compat break: the knob path is a runtime param address, and
recorded params in old runs.jsonl / families.jsonl are inert data strings (a
pre-existing recorded "exposure.scale" still loads, it is just data).

INDEX.md cycle-0065 realization note amended: the param namespace is recorded as
the now-completed tail; the SimBroker slot + the on-disk tap label remain the
deliberate permanent keeps (#117), and exposure_sign_flips stays a serde alias.

Verified: cargo build --workspace --all-targets clean; clippy --all-targets
-D warnings clean; full suite 514 passed / 0 failed (source + byte-pins renamed
together); live `aura run` emits "bias_scale", `aura sweep` emits "bias.scale".

closes #134
refs #126 #117
2026-06-24 14:40:51 +02:00
Brummel 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
2026-06-23 19:48:58 +02:00
Brummel d858caf67b chore: scrub dangling references to deleted specs/plans from sources
docs/specs and docs/plans were retired (prior commit); the source/test
comments that cited them ("spec 0050 §4.1", "spec §Testing N", "per spec",
"the spec's ...") now point at nothing. Strip every such pointer while
preserving the technical substance, the design-ledger contract refs
(C1/C11/C20/C34/C12.1/...), and the Gitea issue refs (#41).

Comment/doc edits only across 20 files — no logic change; full workspace
suite green, clippy clean.
2026-06-18 11:16:28 +02:00
Brummel 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.
2026-06-16 12:12:52 +02:00
Brummel 21c1621bd0 docs,engine: drop coined "compilat", use FlatGraph / "flat graph"
"Compilat" (German "Kompilat") was a coined noun for the product of the
bootstrap compilation — neither English nor a natural fit. The runtime
artifact already has a code identifier for exactly this thing: the
`FlatGraph` struct (harness.rs). Replace the coinage with that identifier:

- prose mentions          -> "flat graph" (mirrors the type, reads plainly)
- definitional anchors    -> `FlatGraph` (C11, C23, the running-graph line)
- `render_compilat`       -> `render_flat_graph` (historical render symbol;
                             keeps the `render_blueprint` / `render_flat_graph`
                             source-vs-product pairing)
- `intra-compilat`        -> `intra-graph`
- `from_compilat` (test)  -> `from_flat`

"compilation" / "re-compilation" / "bootstrap-as-compilation" (the process,
ordinary English) are deliberately left untouched. Behaviour-preserving:
only comments, design ledger, specs/plans, one test-local variable and its
assert messages change. Full workspace test suite green; clippy clean.
2026-06-14 17:02:15 +02:00
Brummel 227d004c9d feat(aura-engine): reject unwired or double-wired input ports
A graph that leaves an interior input slot unconnected, or wires one slot from more
than one producer, is now a compile-time error instead of a silently-wrong run.
Until now an unwired interior slot was accepted and bootstrapped to an empty column
(harness.rs:137-148), so a forgotten connection ran a deterministic but wrong
backtest; two producers into one slot — ill-formed, since a slot holds one column —
was likewise uncompiled-against.

A single name-free check, `check_ports_connected`, is added to `validate_wiring`
after the existing index/kind checks and before the nested-composite recursion. It
counts coverage of each (interior-node, slot) uniformly across interior edges and
role targets, and rejects zero coverage (new `CompileError::UnconnectedPort`) or >1
(new `CompileError::DoubleWiredPort`). Because `validate_wiring` is called once from
`compile_with_params` and recurses, both the raw `Composite::new` path and the
`GraphBuilder::build()` path inherit it at every nesting level with no builder-side
code (mirrors `check_param_namespace_injective`, the param-side sibling). A
composite's own open input roles (source: None) are coverage providers — the
wired-by-enclosing boundary, the root already guarded by UnboundRootRole — not
consumers. Index-based and name-free: nothing reaches the compilat, so C23 holds.

Supporting fix: the check computes `signature()` on every interior node before the
recursion validates that node's interior, so `derive_signature` and
`interior_slot_kind` are made bounds-total (a placeholder kind for an out-of-range
OutField/target, never a panic; the real fault is still reported by validate_wiring's
guarded OutputPortOutOfRange/BadInteriorIndex). This latent panic was exposed by the
new check, not introduced by it.

Test maintenance (blast radius enumerated empirically, not hand-traced): four
existing tests relied incidentally on under-wiring being accepted — three negative
tests get a covering root role so their intended deep fault still surfaces via the
recursion, and one param-order nesting test is fully wired (param order unchanged).
Six new tests: five raw-surface (unwired, double-wire via edge+role and edge+edge,
nested, open-role boundary) and one builder-surface forgotten-leg (proving the
GraphBuilder inherits the check).

Narrowed scope of #65: the original "promote names to load-bearing wiring keys /
close the exposure-price swap structurally" goal was dropped — #64 already moved
authoring to handles + visible port names, so the residual same-kind swap is a
valid-but-wrong name choice no structural check catches, and a structural fix would
push domain semantics into the domain-free engine (C10/C7). This ships the name-free
half that stands on its own: wiring completeness, which catches forgotten and doubled
connections.

Verified: cargo test --workspace 231 passed / 0 failed; cargo clippy --workspace
--all-targets -- -D warnings clean.

closes #65
2026-06-14 16:39:40 +02:00
Brummel b6174cff80 refactor(aura-engine): rename NodeHandle::in_/out to input/output
The trailing underscore on in_() was a keyword-escape scar (`in` is reserved).
Rename the NodeHandle port/field accessors to input()/output(), which also
aligns the authoring surface with the schema's own nomenclature
(NodeSchema.inputs/output, "input port"/"output field"): node.input("lhs")
now reads in the same vocabulary the schema uses, instead of a second one.

Mechanical, behaviour-preserving: the two method definitions plus every call
site in builder.rs and the aura-cli sample blueprints. Full workspace green;
clippy --all-targets -D warnings clean.
2026-06-14 10:35:52 +02:00
Brummel 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
2026-06-14 02:27:03 +02:00