Commit Graph

20 Commits

Author SHA1 Message Date
Brummel 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
2026-06-08 16:38:09 +02:00
Brummel 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
2026-06-08 15:50:19 +02:00
Brummel b73d7076c3 feat(aura-cli): TTY-gated ANSI edge colouring + blank line after composite signature
Two presentation refinements to the `aura graph` views, render-only (no
engine change, C8/C9/C23 untouched).

Edge colouring: thread a `Color { Plain, Ansi }` through render_flat /
render_definition / render_blueprint / render_compilat. `Ansi` uses
ascii-dag's colored scanline pass (per-edge palette colours) so crossing
edges stay traceable; `Plain` is the existing monochrome `g.render()`.
The CLI selects `Ansi` only when stdout is a TTY (`IsTerminal`), so a
redirect to a file or pipe stays byte-clean — `aura graph | …` carries no
escape codes. ascii-dag colours only edges; node-label text is unaffected.

Tests render `Color::Plain` explicitly, so both goldens stay byte-stable
(compiled_view_golden remains the C23 guard); a new test pins that `Ansi`
emits escapes and `Plain` does not, and that labels survive colouring.

Blank line: render_definition now separates the typed signature title from
the graph body with an empty line. blueprint_view_golden re-captured.

Considered but rejected this round: per-node role highlighting of the
signature identifiers — needs either an ascii-dag fork (node colouring is
hard-disabled upstream) or a per-graph string-surgery pass that does not
generalise beyond hand-known labels. Only edge colouring survives here.
2026-06-08 12:08:13 +02:00
Brummel 32ac8a30ea feat(aura-cli): de-prefix input-role markers ([in:price] -> [price])
Symmetric with the output de-prefix shipped in 2f49165: the composite
definition render now shows input roles as bare [<name>] markers, matching
the [<name>] outputs. One-token change in render_definition's input-role
loop (format!("in:{}", role.name) -> role.name.clone()); the two render
doc comments + the blueprint_view_golden / needle / MACD-render asserts
updated. Render-only; compiled_view_golden byte-identical; aura run --macd
unchanged.

refs #41
2026-06-08 10:51:50 +02:00
Brummel 2f4916596d feat(aura-cli): composite definition renders as a typed signature
Refines #41's blueprint-definition render. The [param:<name>] marker nodes
#41 added bloated the ASCII MACD graph (three extra nodes + fan-in edges);
this moves the interface into the title line and folds names into the leaf
labels that already exist:

- title is the composite's typed signature, e.g.
  macd(fast:i64, slow:i64, signal:i64) -> (macd, signal, histogram)
  (param kinds from the aliased leaf's factory params via a new
  ScalarKind->lowercase kind_str; output NAMES only — output kinds need a
  pre-build factory interface, deferred to #43)
- leaf labels fold aliased param names: [EMA] -> [EMA(fast)]
- unnamed interior input slots render as ordered stubs: [Sub] -> [Sub(#A,#B)]
  (count/order derived render-side from c.edges() + c.input_roles(), no
  engine schema needed; arity-1 nodes get no stub)
- outputs de-prefixed: [out:macd] -> [macd]; the [param:*] marker nodes are
  gone
- two stale doc comments corrected: render_definition's [param:] prose and
  node.rs LeafFactory::label (the renderer went flat in 0017, so the cited
  ascii-dag cluster-garble rationale no longer applies — wide labels are
  safe; the factory label stays bare because alias names are composite-level)

Render-only: no engine/ParamAlias/param_space change. compiled_view_golden
byte-identical (C23 guard — verified untouched in the diff); aura run --macd
deterministic and unchanged (total_pips 0.1637945563898923, 3 sign flips).

The title format change repinned five .matches("name:")->.matches("name(")
definition-count asserts (sma_cross/outer/inner/dup/macd); the stub
separator is "," (no space) to match the [Sub(#A,#B)] surface.

Verification (orchestrator-run): cargo build/test/clippy --workspace
-D warnings all green; the redesigned MACD render confirmed by eye.

refs #41 #43
2026-06-08 10:33:13 +02:00
Brummel 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
2026-06-08 02:21:30 +02:00
Brummel 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
2026-06-08 01:00:04 +02:00
Brummel d8b2a2880d feat(aura-cli): MACD strategy PoC over the EMA node
Author MACD as a nested composite -- price -> fast/slow EMA -> Sub (MACD line) -> signal EMA -> Sub (histogram), the histogram exposed as the single output the strategy trades. A richer fixture than sma_cross: an EMA-of-EMA chain with interior fan-out (the MACD line feeds both the signal EMA and the histogram), exercising the nested-composite render shipped in cycle 0017.

It runs over the real path: the composite blueprint is compiled to a flat harness via compile_with_params + Harness::bootstrap (the same machinery as the SMA compiled view), not hand-flattened. New surfaces: aura run --macd (runs the strategy, prints metrics) and aura graph --macd [--compiled]. main's arg parsing is rewritten to a whole-argv match, preserving the #16 strict contract (trailing token / unknown subcommand -> usage, exit 2). MACD gets its own longer synthetic stream because the SMA-seeded EMAs warm up; the SMA sample's stream and goldens are untouched.

This PoC makes two parameter-space gaps concrete for the milestone: the strategy's param_space is [macd.length, macd.length, macd.length, scale] -- three identical names distinguishable only by slot (the named-param case, blueprint #30) -- and a composite exposes only one output port, so the MACD/signal/histogram tuple cannot all be tapped for display (the tuple-output case). The compiled view's valued labels (EMA(2)/EMA(4)/EMA(3)) are what currently disambiguate the three EMAs.

Tested: MACD compiles from the nested composite and runs deterministically (C1), the trace is non-trivial (>=1 exposure sign flip), and the blueprint view renders the composite definition once.
2026-06-07 23:20:42 +02:00
Brummel a4cfe5c53f audit: cycle 0017 (#38) — render_flat dedup + ledger #38 realization note
Architect drift review of the #38 cycle (a70c5fa..cd31447) against the design
ledger. No ledger-contract drift: C23 holds (render_compilat byte-identical,
compiled_view_golden green), C9/C12 honoured (the new main-graph + where:-
definitions view renders a composite as an opaque authoring-level node with its
body defined once — a faithful blueprint/source view vs. the inlined compiled
form), the nested-composite unimplemented! is genuinely removed and tested, and no
orphaned references to the removed ItemDisplay/producer_id/consumer_ids remain in
crates/ (only frozen historical plan docs mention them).

Two debt items, both fixed here (not carried):

- [medium] crates/aura-cli/src/graph.rs — render_compilat open-coded the same
  Graph::with_mode/add_node/add_edge/render idiom the new render_flat helper wraps.
  Fixed: render_compilat now builds its edge-pairs (its Edge structs + source
  targets) and delegates to render_flat, so render_flat is the single flat-build
  point shared by all three render functions. Behaviour-preserving — same node and
  edge insertion order — verified by compiled_view_golden staying byte-identical.

- [low] the retired cluster-box blueprint-render model and its substantive cause
  (ascii-dag 0.9.1's subgraph /2 centering bug; flat-only being the fix) lived only
  in the superseded spec 0013, not in the durable contract layer. Fixed: added a
  "Realization (cycle 0017 — blueprint render = main graph + definitions)" note to
  the ledger (INDEX.md, under the C22 blueprint-view detail) recording the
  opaque-node + where:-definitions model, the authoring-vs-compiled split, the
  ascii-dag-bug rationale, the scale/render-once/nested benefits, and the #37
  playground (enter/focus) counterpart vs. #38 static CLI form.

regression: profile declares no regression scripts and no architect_sweeps — the
architect review is the sole gate. No baseline to update.

Cycle 0017 is drift-clean after these fixes. NOT a milestone close — that needs the
end-to-end milestone fieldtest, still deferred until the sweep root (#32-#33) lands.
2026-06-07 22:29:42 +02:00
Brummel cd31447d98 feat(aura-cli): blueprint view = main graph + composite definitions (#38)
Rewrite the `aura graph` blueprint view from ascii-dag subgraph cluster boxes to
a "program with subroutines" model: a flat main graph that wires the harness with
each composite shown as a single opaque node, plus a `where:` section that defines
each distinct composite type once (its interior with [in:k]/[out] port markers).
`render_compilat` (flat, fully-inlined, C23) is byte-for-byte unchanged.

Why (not effort):
- Correctness under width. ascii-dag 0.9.1's subgraph level-centering rounds
  sibling x-positions with `/2`, overlapping wide sibling labels (a
  width/parity-sensitive bug with no config/padding dodge that survives
  unequal-width siblings; verified by reproduction). The flat layout is
  collision-free; this view renders only flat graphs, so the bug cannot arise.
- Scale. A composite is one opaque node in the main graph, so blueprint size
  tracks top-level wiring, not inlined node count — real strategies stay
  displayable.
- Render-once. A composite reused N times has its body rendered once
  (collect_distinct_composites dedups by name(), recursively).
- Removes the nested-composite `unimplemented!` — the definitions pass recurses,
  so nested composites render as opaque nodes with their own definitions.

Mechanics: ItemDisplay/producer_id/consumer_ids are gone (a composite is now one
display node, so boundary fan-resolution collapses); new render_flat (the same
no-subgraph idiom render_compilat uses), collect_distinct_composites, and
render_definition. The now-unused `Target` import is dropped.

Divergence from the plan's literal text: the plan's `collect_distinct_composites`
used a nested `if let { if .. }`; this toolchain's clippy (collapsible_if, rust
1.94) rejects it under -D warnings, so it is the behaviour-identical let-chain form
`if let BlueprintNode::Composite(c) = item && !seen.contains(&c.name())`.

Tests: the two cluster-asserting blueprint tests are replaced by four behavioural
tests (opaque-node main graph; defines-each-composite-once; nested-renders-without-
panic; reused-composite-defined-once) plus a recaptured blueprint_view_golden
(captured verbatim from `aura graph`, not hand-authored). The four must-stay-green
tests pass; compiled_view_golden is byte-identical (render_compilat untouched).
Verified: `cargo test --workspace` all green, `cargo clippy --workspace
--all-targets -D warnings` clean.

The interactive enter/focus navigation counterpart is the playground's, tracked
separately as #37.

closes #38
2026-06-07 22:25:48 +02:00
Brummel 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
2026-06-07 21:04:52 +02:00
Brummel 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
2026-06-05 20:56:52 +02:00
Brummel fef09d4a47 feat(aura-cli): strict aura run arg-parse rejects trailing tokens
`aura run` keyed only on the first token being `run`, so `aura run
extra-garbage` printed the full report and exited 0 -- a typo'd
`aura run --sweep` silently ran instead of hinting. #16 ratified the
strict reading over the lenient one (the lenient reading buys no real
forward-compat: future args would be parsed explicitly anyway, while the
silent-swallow actively hides typos and clashes with the exit-2 hygiene
the unknown-subcommand path already enforces).

Add a match guard so only a bare `run` reaches the JSON happy path:

    Some("run") if args.next().is_none() => println!(...)

A trailing token fails the guard and falls through to the existing
`_ =>` usage-error arm (stderr usage, exit 2) -- no literal duplication,
the error path is reused. Bare `aura run` is unchanged, pinned by the
positive-preservation assertion and the two sibling run-tests. Verified:
cargo test -p aura-cli green (6), clippy --all-targets -D warnings clean.

closes #16
2026-06-05 00:19:05 +02:00
Brummel 409b770ac6 test(aura-cli): RED for strict aura run arg-parse
`aura run extra-garbage` keys only on the first token being `run` and
ignores the rest, so it prints the full report and exits 0 -- a typo'd
`aura run --sweep` masquerades as a successful run instead of getting a
hint. The 0010 cycle_scope ("any other or missing subcommand -> usage +
exit 2") did not unambiguously cover `run <token>`; #16 ratifies the
strict reading.

This RED pins it: a trailing unexpected token takes the error path
(usage on stderr, empty stdout, exit 2), mirroring the no-args sibling;
bare `aura run` is held to its current happy path (JSON on stdout, exit
0) by a positive-preservation assertion so the strict guard cannot
regress it.

refs #16
2026-06-05 00:17:10 +02:00
Brummel 30e5abb6aa feat(aura-cli): self-identifying RunManifest.commit via build.rs git capture
RunManifest.commit read option_env!("AURA_COMMIT").unwrap_or("unknown"),
so a normal build's manifest identity field was a bare placeholder --
C8's audit trail (this run = this commit) had nothing to point back at
once runs are archived (C18 registry).

Add crates/aura-cli/build.rs: at compile time it shells out to the `git`
already in PATH -- `rev-parse HEAD`, plus a `-dirty` suffix when
`status --porcelain` is non-empty -- and emits
`cargo:rustc-env=AURA_COMMIT=<sha>`. The existing option_env! read is
untouched and now picks it up. Chose shelling to git over a libgit crate
to keep the zero-external-dependency commitment (C14/C18); the firewall
crate is aura-ingest, and a build-time process call adds no runtime dep.
No-git case (packaged source tree, no .git): build.rs returns early and
swallows git errors, leaving AURA_COMMIT unset so "unknown" still holds.

Staleness guard: build.rs emits rerun-if-changed on .git/HEAD and
.git/logs/HEAD (logs/HEAD grows on every HEAD move incl. branch switch),
so the captured sha is rebuilt when HEAD moves rather than going stale.
Determinism (C1) is intact: AURA_COMMIT is fixed at compile time, so two
runs of one build still produce byte-identical manifests.

Two tests that pinned the old "unknown" placeholder are relaxed to the
new structural contract (not scope creep -- they were the old contract):
the cli_run.rs JSON-shape prefix no longer pins the commit value, and
the main.rs unit test asserts commit is non-empty and stable across runs
instead of equal to "unknown". The headline contract (manifest.commit
contains the real HEAD sha) is pinned by run_manifest_commit_carries_
real_git_head (4552d0c). Verified: cargo test -p aura-cli green (5),
clippy --all-targets -D warnings clean.

closes #15
2026-06-05 00:10:42 +02:00
Brummel 4552d0c9c2 test(aura-cli): RED for self-identifying RunManifest.commit
RunManifest.commit comes from option_env!("AURA_COMMIT") defaulting to
"unknown", so the C18 manifest's identity field -- which code produced
this run -- is a bare placeholder in a normal build. C8's audit-trail
invariant (this run = this commit) needs the manifest to point back at
its source once runs are archived.

This RED pins the desired contract structurally: a normal `aura run`'s
manifest.commit CONTAINS the current git HEAD sha (obtained in-test via
`git rev-parse HEAD`) and is NOT the bare "unknown" literal. Structural
(contains, not equals) on purpose: the working tree may be dirty when
the test runs (this test file itself was uncommitted at author time), so
a build that appends a "-dirty" suffix must still pass. GREEN is a
build.rs in aura-cli capturing HEAD at compile time, "unknown" retained
only for the no-git case.

refs #15
2026-06-05 00:07:37 +02:00
Brummel e4eb70f083 feat(aura-cli): --help/-h print usage to stdout and exit 0
`aura --help` and `aura -h` are a newcomer's reflex first command but
fell through to the catch-all unknown-subcommand arm: exit 2, empty
stdout, error text on stderr. The conventional help affordance returning
the error path is a C22 first-contact friction (milestone fieldtest).

Add a `Some("--help") | Some("-h")` arm before the catch-all that prints
the usage to stdout and returns normally (exit 0). The `_ =>` error path
is untouched, so an unknown subcommand still prints `aura: usage: aura
run` to stderr and exits 2 -- pinned by the negative-preservation
assertion in the RED test (07ba20a). Verified: cli_run.rs all green
(help/no-args/run), clippy --all-targets -D warnings clean.

Minimal slice per the tdd handoff: the stdout help literal ("usage: aura
run") and the stderr error literal ("aura: usage: aura run") are left as
two distinct user-facing strings rather than factored into a shared
usage constant -- a single usage source is a deliberate non-goal here,
left for a future cycle if one wants it.

closes #20
2026-06-05 00:04:54 +02:00
Brummel 07ba20a991 test(aura-cli): RED for --help/-h success-path affordance
`aura --help` and `aura -h` are a newcomer's reflex first command, but
today they hit the unknown-subcommand error path: exit 2, empty stdout,
one-line `aura: usage: aura run` on stderr. For a C22 "newcomer sees a
populated trace" milestone the conventional help affordance returning
the error path is a first-contact friction.

This compile-clean RED pins the desired success-path contract: `--help`
and `-h` print non-empty usage to stdout and exit 0, and the usage names
the `run` subcommand. A negative-preservation assertion keeps an unknown
subcommand (`frobnicate`) on the exit-2 error path so the fix cannot
regress bad-args handling. Asserts structural facts only (exit code,
non-empty stdout, mentions `run`), not exact help prose.

refs #20
2026-06-05 00:03:10 +02:00
Brummel 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
2026-06-04 20:45:30 +02:00
Brummel 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>
2026-06-03 01:31:14 +02:00