The drift review found the run handle keying on the blueprint's *content* id, which hashes `doc` fields and every other C23 debug symbol. C29's Id treatment forbids a description influencing an identity, and the repository already carried the right projection unused. The digest now substitutes `blueprint_identity_json`'s debug-symbol-blind projection (#171) for `topology_hash` in the hashed value. `manifest.topology_hash` itself is untouched, so #343's reference semantics and the committed record-line fingerprint both stand. That projection blanks more than descriptions — the render name and node, role, output, tap and gang names — while param openness stays identity-bearing. This is correct rather than incidental: names are non-load-bearing debug symbols (C8/C23), so blueprints differing only in them compute bit-identically and one address is the right answer. The reach is now stated in C22 and in the digest's own doc, together with the one observable consequence: two structurally identical blueprints whose declared tap names differ share a directory while writing differently named tap files, since the write path never prunes (#352). Two code twins the cycle left stale are corrected. `name_gate`'s doc comment still carried the C23 clause this cycle superseded and moved to its sidecar, and the authoring guide claimed a same-identity re-run replaces the directory's contents — it does not, for the same non-pruning reason. Bench is report-only and all five fingerprints are unchanged. `campaign_sweep` reports a peak-RSS NOTICE (+12.8%); the campaign leg is untouched by this cycle and its fingerprint is stable, so nothing is ratified against it here. Routed rather than fixed: #353 — naming authority for one flat namespace is spelled in three crates and the single-run leg has no claim path — filed into the recorded-stream-store milestone, where the container consolidates anyway; a data-identity seam recorded on #320 (a run's identity carries the window, never the data's content, so a corrected archive is a silent overwrite until that store supplies a recording identity); and a second instance of the non-pruning class on #352. The cycle's spec and plan are removed — git-ignored working files, read by the drift review before deletion. refs #311
aura
aura is a "game engine for traders." It is a Rust framework — and a playground — for authoring trading strategies as composable dataflow nodes, backtesting them deterministically and massively in parallel, validating them (sweep / Monte-Carlo / walk-forward), and freezing a validated strategy into a standalone bot with a broker connection. Its differentiator is the World: a program that constructs and orchestrates whole families of backtests, not the single run.
This README is a discoverability map of the shipped aura CLI — what commands
exist and what they do. It deliberately keeps per-flag detail light; the two
authoritative sources for exact syntax are always:
aura <command> --help— the exhaustive flag grammar.- The design ledger (
docs/design/INDEX.md) and glossary (docs/glossary.md) — the concepts and invariants behind the surface.
Build & run
aura is a Cargo workspace. Build everything and you get one binary named aura:
cargo build --workspace # binary at target/debug/aura
cargo build --workspace --release # optimized, at target/release/aura
Invoke it as aura <command> … (examples below use the plain name).
Concepts in one breath
- Blueprint — a serialized signal graph as data: a param-generic
price → biasnode graph. A blueprint file (blueprint.json) is the unit a downstream consumer loads and executes or varies. - Open vs. closed — a blueprint with unbound (free) numeric knobs is open;
one with every knob bound is closed. A plain
execwants closed; naming a knob as a campaign axis (or anexec --override) is what reopens it. - Family — the set of runs a campaign document's process pipeline produces over a blueprint (a sweep grid, a Monte-Carlo seed set, a walk-forward window sequence). Families are persisted in a content-addressed store and can be listed, ranked, and reproduced.
- Axis — one named, sweepable knob of a blueprint (e.g.
fast.length), bound with a comma-separated value list. A gang fuses several sibling knobs into one axis (one value drives all members).
Executing a document
aura exec <target> (#319) is the one executor verb: <target> is either a
loaded blueprint file (a single synthetic run) or a campaign document
(a .json file or its registered 64-hex content id — instruments × windows ×
strategy × param axes × process, see docs/authoring-guide.md §3). graph
renders a blueprint's structure so a mis-wire is visible before a run.
| Command | Purpose |
|---|---|
aura exec <bp.json> |
Run one backtest of a closed blueprint and print its report. An open (free-knob) blueprint is refused with a clean error — bind it, or vary it as a campaign axis. |
aura exec <bp.json> --override <node.param>=<value> |
Reopen one bound param for this single execution only (the value is recorded raw in the manifest); repeatable. |
aura exec <bp.json> --tap <name>=<fold> |
Subscribe a declared tap to a fold (record/count/sum/mean/min/max/first/last) for this run; repeatable. |
aura graph <bp.json> |
Render the blueprint's structure as an interactive HTML DAG so a mis-wire is visible before a run. Omit the file to render the built-in sample; a named-but-unreadable file is a usage error. |
aura graph introspect --params <bp.json|id> |
Discover a blueprint's open, sweepable knobs. Prints each as <name>:<kind>, bound params trailing default=<value>. Run this first to learn the axis names for a campaign document. |
aura exec <campaign.json|id> [--parallel-instruments <n>] |
Execute a campaign document — a grid family, gates, walk-forward, Monte-Carlo, and cross-instrument generalization per its process document, one cell per (strategy, instrument, window). |
aura runs families |
List every persisted family (id, kind, member count). |
aura runs family <id> [rank <metric>] |
List one family's members, optionally ranked best-first by an R metric (e.g. sqn_normalized, expectancy_r). |
aura reproduce <family-id> |
Re-derive every member of a persisted family from the content-addressed store and check it is bit-identical (the C18/C1 determinism guarantee). |
Important contract — every open knob is mandatory on a campaign axis. A
campaign document's strategies[].axes must name every open knob
graph introspect --params lists: there is no default value — pin a knob you
don't want to vary with a single-value axis ({"kind":"I64","values":[4]}).
Omitting one is a clean error naming the missing knob, not a silent default.
Use aura exec --help for the exact flag grammar, docs/authoring-guide.md
for the campaign document shape (data window, naming, presentation/emit).
Authoring & introspecting topology
A blueprint is authored declaratively from a JSON op-list: an ordered list
of by-identifier construction ops replayed against the standard node vocabulary.
Adding a new node type to that vocabulary is Rust, not JSON — see
docs/authoring-guide.md, §0, "Authoring a new node in Rust".
| Command | Purpose |
|---|---|
aura graph build |
Read an op-list document on stdin, build the blueprint, and print its canonical JSON to stdout (no trailing newline — it is the content-addressed artifact). |
aura graph introspect --vocabulary |
List every node type available to op-lists. |
aura graph introspect --node <T> |
Show one type's input ports, output fields, and param paths (with the bind-value form). |
aura graph introspect --unwired |
Read a partial op-list on stdin and list its still-open interior slots. |
aura graph introspect --content-id |
Read an op-list on stdin and print the SHA-256 content id of the blueprint it would build. |
aura graph introspect --identity-id |
Read an op-list on stdin and print the SHA-256 identity id — the topology's debug-name-blind id, so two documents (or an op-script and a Rust-built blueprint) expressing the same topology print the same id. Combinable with --content-id (both ids, one per line, content id first). |
Op-list shape
Each element is one op, tagged by "op". Node params are bound with the typed
Scalar form — a tagged, capitalized-kind object: {"I64": 5}, {"F64": 1.5}, {"Bool": true}. A worked example (an SMA-crossover → bias signal):
[
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
{"op":"add","type":"Sub"},
{"op":"add","type":"Bias"},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"connect","from":"sub.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"}
]
Piping this document into aura graph build emits a blueprint.json you can
then feed to exec above:
aura graph build < crossover.ops.json > crossover.bp.json
aura graph introspect --params crossover.bp.json
The op kinds are source, input, add, feed, connect, expose, tap,
gang, doc, and use (tap declares a recorded measurement point on an
interior wire; doc declares the composite's one-line meaning, required at
register — C29; use splices a registered blueprint in as a nested composite,
by content id or label; add additionally takes an args object for
arg-bearing types (Session, LinComb, CostSum) — structural,
non-scalar construction consumed before bind; see the authoring guide). See
aura graph introspect --node <T> for a type's exact ports and the op-script
grammar in the design ledger for the full semantics.
Where to go deeper
aura <command> --help— exhaustive, always-current flag grammar.docs/design/INDEX.md— the design ledger: architecture, invariants, the World.docs/glossary.md— canonical terminology (blueprint, family, axis, sweep / Monte-Carlo / walk-forward, R, content-id, …).