Compare commits
15 Commits
32eb5a6a9e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 77ad0465cb | |||
| e6b60bf680 | |||
| 2f1baceec7 | |||
| 3aa63833f1 | |||
| 1476990cfd | |||
| dc5f1742f4 | |||
| 9eb6d6b4f6 | |||
| 06d7e0f30a | |||
| db8f947441 | |||
| 24782caaec | |||
| 5dc8e03249 | |||
| 696d7fe59a | |||
| da19e27b6a | |||
| 78b80ec0fd | |||
| 9cfe2965c0 |
@@ -31,47 +31,46 @@ Invoke it as `aura <command> …` (examples below use the plain name).
|
||||
|
||||
- **Blueprint** — a serialized signal graph as data: a param-generic
|
||||
`price → bias` node graph. A blueprint file (`blueprint.json`) is the unit a
|
||||
downstream consumer loads and drives verbs over.
|
||||
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*. Different verbs want different states
|
||||
(see each verb below).
|
||||
- **Family** — the set of runs one verb 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.
|
||||
one with every knob bound is *closed*. A plain `exec` wants closed; naming a
|
||||
knob as a campaign axis (or an `exec --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).
|
||||
|
||||
## Running & orchestrating a loaded blueprint
|
||||
## Executing a document
|
||||
|
||||
These verbs all take the **blueprint file as their first positional argument**.
|
||||
Most drive a family of runs over it; `graph` renders its structure so a mis-wire
|
||||
is visible before a run. `walkforward`, `mc`, and `generalize` share
|
||||
`sweep`'s generic grammar — the blueprint positional plus `--real` and a
|
||||
repeatable `--axis <name>=<csv>` (`generalize` takes `--real <SYM1,SYM2,…>`, at
|
||||
least two instruments, with a single value per axis) — see `--help`.
|
||||
`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 run <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 use `sweep`. |
|
||||
| `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 sweep <bp.json> --list-axes` | **Discover** the blueprint's open, sweepable knobs. Prints each as `<name>:<kind>`. Run this first to learn the axis names. |
|
||||
| `aura sweep <bp.json> --axis <name>=<csv> [--axis …]` | Run a **grid family** over the named axes and persist it. |
|
||||
| `aura mc <bp.json> --seeds <n>` | Run a **synthetic Monte-Carlo family** of `n` seeded realizations. Wants a **closed** blueprint (the inverse of sweep). |
|
||||
| `aura mc <bp.json> --real <sym> --axis <name>=<csv> [--axis …]` | Run a **Monte-Carlo R-bootstrap campaign** over recorded data — driven through the same generated-campaign pipeline as `sweep`; `--block-len`/`--resamples`/`--seed` tune the bootstrap. |
|
||||
| `aura walkforward <bp.json> --axis <name>=<csv> [--axis …]` | Run an **in-sample-refit walk-forward family** — rolling optimize + out-of-sample test across windows, stitched into one verdict + parameter stability. `--select` chooses the per-window objective. |
|
||||
| `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 `sweep` and
|
||||
`walkforward`, the knobs enumerated by `--list-axes` are *all* required: you must
|
||||
supply an `--axis` for each open knob. There is no default value — pin a knob you
|
||||
don't want to vary with a single-value axis (`--axis name=4`). Omitting one is a
|
||||
clean error naming the missing knob, not a silent default.
|
||||
**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 <cmd> --help` for the full data-window (`--real`/`--from`/`--to`),
|
||||
naming (`--name`), and selection (`--select`) flags.
|
||||
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
|
||||
|
||||
@@ -111,11 +110,11 @@ Each element is one op, tagged by `"op"`. Node params are bound with the **typed
|
||||
```
|
||||
|
||||
Piping this document into `aura graph build` emits a `blueprint.json` you can
|
||||
then feed to `run` / `sweep` / `mc` / `walkforward` above:
|
||||
then feed to `exec` above:
|
||||
|
||||
```sh
|
||||
aura graph build < crossover.ops.json > crossover.bp.json
|
||||
aura sweep crossover.bp.json --list-axes
|
||||
aura graph introspect --params crossover.bp.json
|
||||
```
|
||||
|
||||
The op kinds are `source`, `input`, `add`, `feed`, `connect`, `expose`, `tap`,
|
||||
|
||||
@@ -95,31 +95,18 @@ fn blueprint_stems(dir: &Path) -> Vec<String> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Seed one blueprint into the scratch store via a tiny synthetic sweep (the
|
||||
/// E2E `seed_blueprint` pattern) and return its content id.
|
||||
/// Seed one blueprint into the scratch store via `graph register` (#319: the
|
||||
/// surviving registration surface) and return its content id.
|
||||
fn seed_blueprint(bin: &Path, dir: &Path, file: &str, name: &str) -> Result<String, String> {
|
||||
let before = blueprint_stems(dir);
|
||||
let (out, code) = run_in(
|
||||
bin,
|
||||
dir,
|
||||
&[
|
||||
"sweep",
|
||||
file,
|
||||
"--axis",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
name,
|
||||
],
|
||||
)?;
|
||||
let (out, code) = run_in(bin, dir, &["graph", "register", file, "--name", name])?;
|
||||
if code != Some(0) {
|
||||
return Err(format!("seed sweep failed ({code:?}): {out}"));
|
||||
return Err(format!("seed register failed ({code:?}): {out}"));
|
||||
}
|
||||
blueprint_stems(dir)
|
||||
.into_iter()
|
||||
.find(|s| !before.contains(s))
|
||||
.ok_or_else(|| "seed sweep must store a new blueprint".to_string())
|
||||
.ok_or_else(|| "seed register must store a new blueprint".to_string())
|
||||
}
|
||||
|
||||
fn register_process(bin: &Path, dir: &Path, file: &str, doc: &str) -> Result<String, String> {
|
||||
@@ -261,10 +248,10 @@ pub fn winner_fingerprint(stdout: &str) -> Result<String, String> {
|
||||
|
||||
fn campaign_rep(bin: &Path, sizing: Sizing, process_doc: &str) -> Result<RepOutcome, String> {
|
||||
let (scratch, doc) = build_scratch(bin, sizing, process_doc)?;
|
||||
let timed: TimedChild = run_timed(bin, &["campaign", "run", &doc], &scratch.0)?;
|
||||
let timed: TimedChild = run_timed(bin, &["exec", &doc], &scratch.0)?;
|
||||
if timed.exit != Some(0) {
|
||||
return Err(format!(
|
||||
"campaign run exited {:?}\nstdout: {}\nstderr: {}",
|
||||
"aura exec exited {:?}\nstdout: {}\nstderr: {}",
|
||||
timed.exit, timed.stdout, timed.stderr
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! CLI fixed cost: the spawn floor (`aura --help`, no fingerprint — nothing is
|
||||
//! computed) and a minimal data-only project run whose fingerprint is the
|
||||
//! FNV-1a hash of the run's single stdout JSON line. Fresh scratch project per
|
||||
//! computed) and a minimal data-only project exec whose fingerprint is the
|
||||
//! FNV-1a hash of the exec's single stdout JSON line. Fresh scratch project per
|
||||
//! repetition, so store counters start identical.
|
||||
|
||||
use super::{median, RepOutcome};
|
||||
@@ -11,7 +11,7 @@ use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Fingerprint of an `aura run` record line, invariant across rebuilds: the
|
||||
/// Fingerprint of an `aura exec` record line, invariant across rebuilds: the
|
||||
/// line's `manifest.commit` is the aura binary's compile-time build sha
|
||||
/// (crates/aura-cli/build.rs — the invariant-8 audit trail), so hashing the
|
||||
/// raw line would flip the fingerprint on EVERY new commit and report a
|
||||
@@ -59,9 +59,9 @@ pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
|
||||
let mut run_walls = Vec::new();
|
||||
let mut first_line = None;
|
||||
for _ in 0..RUN_SAMPLES {
|
||||
let timed = run_timed(bin, &["run", "bench_sma_a.json"], &scratch.0)?;
|
||||
let timed = run_timed(bin, &["exec", "bench_sma_a.json"], &scratch.0)?;
|
||||
if timed.exit != Some(0) {
|
||||
return Err(format!("aura run exited {:?}: {}", timed.exit, timed.stdout));
|
||||
return Err(format!("aura exec exited {:?}: {}", timed.exit, timed.stdout));
|
||||
}
|
||||
run_walls.push(timed.wall_s * 1000.0);
|
||||
if first_line.is_none() {
|
||||
@@ -70,7 +70,7 @@ pub fn rep(bin: &Path) -> Result<RepOutcome, String> {
|
||||
.stdout
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or("aura run must print its JSON record line")?
|
||||
.ok_or("aura exec must print its JSON record line")?
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ mod tests {
|
||||
|
||||
/// `run_reps` composed with the real campaign sweep surface — a fresh
|
||||
/// scratch project, a freshly seeded blueprint, and a genuine
|
||||
/// `aura campaign run` child process spawned per repetition (not an
|
||||
/// `aura exec` child process spawned per repetition (not an
|
||||
/// in-process shortcut). Protects that two independently built-and-run
|
||||
/// scratch campaigns agree on the winner-ordinal fingerprint extracted
|
||||
/// from the child's stdout — C1's determinism carried through the
|
||||
@@ -136,7 +136,7 @@ mod tests {
|
||||
|
||||
/// `run_reps` composed with the real CLI fixed-cost surface — a genuine
|
||||
/// `aura --help` spawn-floor wall plus a fresh scratch project and a real
|
||||
/// `aura run` child process per repetition (not an in-process shortcut).
|
||||
/// `aura exec` child process per repetition (not an in-process shortcut).
|
||||
/// Protects that two independent scratch runs agree on the run-line FNV
|
||||
/// fingerprint extracted from the child's stdout, and that the metrics
|
||||
/// map carries the wall-clock keys the fixed_cost `BaselineDoc` needs.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! `aura campaign run` — the driver that turns a stored campaign document into
|
||||
//! `aura exec`'s campaign legs (#319; formerly `aura campaign run`) — the
|
||||
//! driver that turns a stored campaign document into
|
||||
//! a realized run-set (#198). The execution *semantics* (preflight, cell loop,
|
||||
//! stage sequencing, selection, registry writes) live in `aura-campaign`; the
|
||||
//! [`MemberRunner`](aura_campaign::MemberRunner) implementation over the shipped loaded-blueprint machinery
|
||||
@@ -27,7 +28,7 @@ use aura_engine::FamilySelection;
|
||||
use aura_registry::CampaignRunRecord;
|
||||
use aura_research::{
|
||||
campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign,
|
||||
validate_process, CampaignDoc, DocRef,
|
||||
validate_process, Axis, CampaignDoc, DocRef,
|
||||
};
|
||||
|
||||
use aura_runner::axes::is_content_id;
|
||||
@@ -108,27 +109,17 @@ struct CampaignRunLine<'a> {
|
||||
campaign_run: &'a CampaignRunRecord,
|
||||
}
|
||||
|
||||
/// Stdout shape of a campaign run. `Full` is `aura campaign run` (emit-gated
|
||||
/// member/selection lines + the always-on final `campaign_run` record line).
|
||||
/// `MemberLinesOnly` is dissolved-verb sugar: the generated document's
|
||||
/// `emit: ["family_table"]` already limits emission to member lines; the mode
|
||||
/// additionally suppresses the final record line so the verb's stdout
|
||||
/// contract is reproduced byte-for-byte. The record append is identical in
|
||||
/// both modes — presentation changes, the record does not.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum RunPresentation {
|
||||
Full,
|
||||
MemberLinesOnly,
|
||||
}
|
||||
|
||||
/// `aura campaign run <target>`: resolve, gate, execute, emit. Every `Err`
|
||||
/// `aura exec <target>` (campaign legs): resolve, gate, execute, emit. Every `Err`
|
||||
/// is a refusal `campaign_cmd` renders as `aura: {msg}` + exit 1; an `Ok`
|
||||
/// carries the failed-cell count (#272) so the caller can exit 3 on a
|
||||
/// completed-with-failures run via `exit_on_campaign_result`.
|
||||
/// completed-with-failures run via `exit_on_campaign_result`. `overrides`
|
||||
/// (#319 Task 4, the `exec --override` campaign leg) is threaded through
|
||||
/// unchanged to `run_campaign_returning`, the one injection seam.
|
||||
pub(crate) fn run_campaign(
|
||||
target: &str,
|
||||
env: &Env,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<usize, String> {
|
||||
// Project gate FIRST: nothing (not even the file-sugar registration)
|
||||
// touches a store outside a project.
|
||||
@@ -162,7 +153,7 @@ pub(crate) fn run_campaign(
|
||||
));
|
||||
};
|
||||
|
||||
run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments)
|
||||
run_campaign_by_id(&campaign_id, env, parallel_instruments, overrides)
|
||||
}
|
||||
|
||||
/// An executed campaign plus the context its presenter and the dissolved-verb
|
||||
@@ -181,20 +172,15 @@ pub(crate) struct CampaignRun {
|
||||
/// The one campaign executor path from a resolved content id onward: fetch
|
||||
/// the stored canonical bytes by id (so file addressing and id addressing
|
||||
/// produce the same realization by construction) and re-run the intrinsic
|
||||
/// tier on them, execute, and emit per `presentation`. Shared by
|
||||
/// `run_campaign` (`RunPresentation::Full`) and the dissolved-verb sugar path
|
||||
/// (`verb_sugar::run_sweep_sugar`, `RunPresentation::MemberLinesOnly`) — no
|
||||
/// project gate here: the sugar path must work project-less exactly as the
|
||||
/// inline verb it replaces did (store mechanics run against `env.registry()`
|
||||
/// in both cases).
|
||||
/// tier on them, execute, and emit. Called by `run_campaign`.
|
||||
pub(crate) fn run_campaign_by_id(
|
||||
campaign_id: &str,
|
||||
env: &Env,
|
||||
presentation: RunPresentation,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<usize, String> {
|
||||
let run = run_campaign_returning(campaign_id, env, parallel_instruments)?;
|
||||
present_campaign(run, presentation, env)
|
||||
let run = run_campaign_returning(campaign_id, env, parallel_instruments, overrides)?;
|
||||
present_campaign(run, env)
|
||||
}
|
||||
|
||||
/// The one campaign executor path from a resolved content id up to (not
|
||||
@@ -204,18 +190,32 @@ pub(crate) fn run_campaign_by_id(
|
||||
/// outcome bundled with the context the presenter needs. Shared by
|
||||
/// `run_campaign_by_id` (which then presents) and a forthcoming dissolved-verb
|
||||
/// sugar path that reads the outcome and self-prints (lands in a later task,
|
||||
/// alongside the generalize translator's own runner).
|
||||
/// alongside the generalize translator's own runner). `overrides` (#319
|
||||
/// Task 4) is injected into the parsed document AFTER intrinsic validation
|
||||
/// and BEFORE the referential gate — the stored bytes and the recorded
|
||||
/// `campaign_id` stay those of the ORIGINAL document; only the executed
|
||||
/// run-set changes, its audit record being the raw member manifests.
|
||||
///
|
||||
/// One deliberate exception to "every refusal is an `Err`" (review Minor-4):
|
||||
/// the injection loop's axis-collision check exits directly with code 2
|
||||
/// instead of returning `Err`. That collision is a USAGE-class fault (C14) —
|
||||
/// `--override` naming a path the document already declares as an axis is a
|
||||
/// malformed invocation, exactly like `--override`'s own malformed-token
|
||||
/// refusal (`parse_override_tokens`, exit 2) — not a RUNTIME-class refusal
|
||||
/// (missing environment/recorded state, exit 1), which is what this fn's
|
||||
/// `Result<_, String>` return otherwise carries end to end.
|
||||
pub(crate) fn run_campaign_returning(
|
||||
campaign_id: &str,
|
||||
env: &Env,
|
||||
parallel_instruments: NonZeroUsize,
|
||||
overrides: &[(String, Scalar)],
|
||||
) -> Result<CampaignRun, String> {
|
||||
let registry = env.registry();
|
||||
let campaign_text = registry
|
||||
.get_campaign(campaign_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("no campaign {campaign_id} in the project store"))?;
|
||||
let campaign = parse_campaign(&campaign_text)
|
||||
let mut campaign = parse_campaign(&campaign_text)
|
||||
.map_err(|e| doc_error_prose("stored campaign document", &e))?;
|
||||
let faults = validate_campaign(&campaign);
|
||||
if !faults.is_empty() {
|
||||
@@ -225,6 +225,25 @@ pub(crate) fn run_campaign_returning(
|
||||
));
|
||||
}
|
||||
|
||||
// --override NODE.PARAM=VALUE (#246 residue, #319 Task 4): inject as a
|
||||
// single-value axis over the named bound param into EVERY strategy
|
||||
// entry. Collision with a document-declared axis refuses (an override
|
||||
// overrides a bound value, never an axis); an unknown path then falls
|
||||
// out of `validate_campaign_refs` below with its existing did-you-mean
|
||||
// prose — no `RefFault` change, no registry change.
|
||||
for (path, val) in overrides {
|
||||
for entry in &mut campaign.strategies {
|
||||
if entry.axes.contains_key(path) {
|
||||
eprintln!(
|
||||
"aura: exec: --override `{path}` collides with a declared axis \
|
||||
of the campaign; an override overrides a bound value, never an axis"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
entry.axes.insert(path.clone(), single_value_axis(*val));
|
||||
}
|
||||
}
|
||||
|
||||
// Referential gate: zero faults or refuse (the campaign-validate seam).
|
||||
let resolve = |t: &str| env.resolve(t);
|
||||
let ref_faults = registry
|
||||
@@ -295,15 +314,11 @@ pub(crate) fn run_campaign_returning(
|
||||
Ok(CampaignRun { outcome, campaign, strategies, server: runner.server() })
|
||||
}
|
||||
|
||||
/// Emit an executed campaign's results per `presentation`: the zero-survivor
|
||||
/// stderr notes, the emit-gated family/selection lines, the always-on record
|
||||
/// line (Full only), then trace persistence. Split out of `run_campaign_by_id`
|
||||
/// so the dissolved-verb sugar can consume the outcome without this stdout tail.
|
||||
fn present_campaign(
|
||||
run: CampaignRun,
|
||||
presentation: RunPresentation,
|
||||
env: &Env,
|
||||
) -> Result<usize, String> {
|
||||
/// Emit an executed campaign's results: the zero-survivor stderr notes, the
|
||||
/// emit-gated family/selection lines, the always-on record line, then trace
|
||||
/// persistence. Split out of `run_campaign_by_id` so a forthcoming caller
|
||||
/// could consume the outcome without this stdout tail.
|
||||
fn present_campaign(run: CampaignRun, env: &Env) -> Result<usize, String> {
|
||||
let CampaignRun { outcome, campaign, strategies, server } = run;
|
||||
|
||||
// Zero-survivor stderr notes (exit stays 0 — a null result is a valid
|
||||
@@ -355,6 +370,17 @@ fn present_campaign(
|
||||
let emit_family = campaign.presentation.emit.iter().any(|e| e == "family_table");
|
||||
let emit_selection = campaign.presentation.emit.iter().any(|e| e == "selection_report");
|
||||
for cell_out in &outcome.cells {
|
||||
// #313 zero-trade note (#319 Task 5, migrated from the retiring
|
||||
// `dispatch_walkforward`/`run_walkforward_sugar` paths): every campaign
|
||||
// cell's `std::walk_forward` family, if any, gets the same shared
|
||||
// `note_zero_trade_windows` producer over its OOS members' trade
|
||||
// counts — independent of `emit` (a stderr diagnostic, not gated
|
||||
// stdout presentation).
|
||||
if let Some(wf) = cell_out.families.iter().find(|f| f.block == "std::walk_forward") {
|
||||
crate::diag::note_zero_trade_windows(
|
||||
wf.reports.iter().map(|r| r.metrics.r.as_ref().map_or(0, |m| m.n_trades)),
|
||||
);
|
||||
}
|
||||
if emit_family {
|
||||
for fam in &cell_out.families {
|
||||
for report in &fam.reports {
|
||||
@@ -381,23 +407,21 @@ fn present_campaign(
|
||||
}
|
||||
}
|
||||
}
|
||||
if presentation == RunPresentation::Full {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record })
|
||||
.expect("campaign run record serializes")
|
||||
);
|
||||
eprintln!(
|
||||
"aura: campaign run {} recorded: {} cells{}",
|
||||
outcome.record.run,
|
||||
outcome.record.cells.len(),
|
||||
if failed == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", {failed} failed ({})", fail_labels.join(", "))
|
||||
}
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record })
|
||||
.expect("campaign run record serializes")
|
||||
);
|
||||
eprintln!(
|
||||
"aura: campaign run {} recorded: {} cells{}",
|
||||
outcome.record.run,
|
||||
outcome.record.cells.len(),
|
||||
if failed == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", {failed} failed ({})", fail_labels.join(", "))
|
||||
}
|
||||
);
|
||||
|
||||
// Trace persistence runs LAST, after the always-on record line (0109):
|
||||
// stdout stays data-pure (the record line is the wire contract) and the
|
||||
@@ -417,6 +441,13 @@ fn present_campaign(
|
||||
Ok(failed)
|
||||
}
|
||||
|
||||
/// Build the single-value `Axis` an `exec --override` injects (#319 Task 4):
|
||||
/// exactly the shape the document form `{"kind": "I64", "values": [v]}`
|
||||
/// deserializes to — `Scalar::kind` names the tag, one value in the list.
|
||||
fn single_value_axis(v: Scalar) -> Axis {
|
||||
Axis { kind: v.kind(), values: vec![v] }
|
||||
}
|
||||
|
||||
/// #272: the fault-kind label the failed-cell summary line names — the same
|
||||
/// closed `CellFaultKind` vocabulary an aggregate over `campaign_runs.jsonl`
|
||||
/// counts by. Debug-leak-free (a fixed lowercase-snake label, not a Debug
|
||||
|
||||
@@ -32,11 +32,11 @@ fn zero_trade_note_text(n_windows: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The #313 zero-trade note, shared by both walk-forward paths (the
|
||||
/// synthetic-blueprint path in `main.rs` and the `--real` sugar path in
|
||||
/// `verb_sugar.rs`) so the wording AND the condition live in exactly one
|
||||
/// place. Takes the per-window trade counts; a no-op unless there is at
|
||||
/// least one window and every one of them traded zero times.
|
||||
/// The #313 zero-trade note (#319 Task 5: migrated onto the campaign
|
||||
/// walk-forward leg in `campaign_run.rs`, the surviving executor path) so
|
||||
/// the wording AND the condition live in exactly one place. Takes the
|
||||
/// per-window trade counts; a no-op unless there is at least one window and
|
||||
/// every one of them traded zero times.
|
||||
pub(crate) fn note_zero_trade_windows(mut window_trades: impl ExactSizeIterator<Item = u64>) {
|
||||
let n_windows = window_trades.len();
|
||||
if n_windows > 0 && window_trades.all(|n| n == 0) {
|
||||
|
||||
@@ -642,10 +642,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
} else if let Some(target) = cmd.params.as_deref() {
|
||||
// --params <FILE|ID> (#196): the RAW composite param space — the one
|
||||
// namespace campaign axes are validated against
|
||||
// (`validate_campaign_refs`) and `aura sweep --list-axes` prints
|
||||
// (#328: the wrapped `<blueprint>.<node>.<param>` form was retired —
|
||||
// there is exactly one axis namespace now, so the two discovery
|
||||
// surfaces agree line-for-line, see `params_lines`'s own doc comment).
|
||||
// (`validate_campaign_refs`) and `exec --override`'s own resolution
|
||||
// (`override_paths`, member.rs) resolves against (#328: the wrapped
|
||||
// `<blueprint>.<node>.<param>` form was retired — there is exactly
|
||||
// one axis namespace now, so the two surfaces agree line-for-line,
|
||||
// see `params_lines`'s own doc comment).
|
||||
match params_lines(target, env) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
@@ -663,8 +664,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
// The content id (#158) is the SHA256 of the same `blueprint_to_json`
|
||||
// bytes `graph build` emits; the identity id (#171) the SHA256 of the
|
||||
// debug-name-blind `blueprint_identity_json` form — both via the one
|
||||
// shared `crate::content_id` primitive `topology_hash` also uses, so all
|
||||
// surfaces agree by construction.
|
||||
// shared `crate::content_id` primitive, the same hash the run
|
||||
// manifest's `topology_hash` uses (computed inline in
|
||||
// `aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — #319
|
||||
// Task 9 retired the CLI-side `topology_hash` wrapper this comment
|
||||
// used to name), so all surfaces agree by construction.
|
||||
let composite = match cmd.content_id.as_ref() {
|
||||
Some(Some(file)) => {
|
||||
let text = match std::fs::read_to_string(file) {
|
||||
@@ -722,7 +726,7 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
/// Phrase a blueprint-document `LoadError` as prose (Debug-leak-free; the
|
||||
/// engine error types are `Display`-free by convention — this is the CLI's
|
||||
/// presentation layer, like `format_op_error`). `pub(crate)`: shared by the
|
||||
/// `graph build` path (below) and `aura run`'s loaded-blueprint branch
|
||||
/// `graph build` path (below) and `aura exec`'s blueprint leg
|
||||
/// (main.rs) — env-agnostic on purpose, so both callers get identical wording
|
||||
/// (#184).
|
||||
pub(crate) fn blueprint_load_prose(e: &LoadError) -> String {
|
||||
@@ -805,41 +809,6 @@ pub(crate) fn unresolved_namespace_hint(e: &LoadError, env: &aura_runner::projec
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a blueprint-slot document — the `sweep`/`walkforward`/`mc`
|
||||
/// loaded-blueprint branches' dispatch-boundary check (#210 c0110 fieldtest
|
||||
/// finding: the sweep slot used to print the raw `{e:?}` Debug leak). Single-
|
||||
/// sourced here so the three dual-grammar subcommands cannot diverge (#184's
|
||||
/// convention, extended). Two refusals, checked in order:
|
||||
///
|
||||
/// 1. The document parses as JSON but is an op-script ARRAY (#157), not a
|
||||
/// built blueprint envelope — refused with a targeted hint pointing at
|
||||
/// `aura graph build`, since `blueprint_from_json` would otherwise surface
|
||||
/// a confusing internal serde shape mismatch.
|
||||
/// 2. `blueprint_from_json` fails to load the envelope — phrased house-style
|
||||
/// via [`blueprint_load_prose`] (+ [`unresolved_namespace_hint`] where it
|
||||
/// applies), never the raw `LoadError` Debug form.
|
||||
///
|
||||
/// `Ok(())` means the document loaded cleanly; callers that only need the
|
||||
/// validation (not the `Composite`) re-parse `doc` themselves afterward
|
||||
/// (`blueprint_from_json` is cheap and infallible at that point).
|
||||
pub(crate) fn blueprint_slot_prose(doc: &str, env: &aura_runner::project::Env) -> Result<(), String> {
|
||||
if matches!(serde_json::from_str::<serde_json::Value>(doc), Ok(serde_json::Value::Array(_))) {
|
||||
return Err(
|
||||
"this is an op-script (an op array), not a built blueprint — run \
|
||||
'aura graph build' first"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
blueprint_from_json(doc, &|t| env.resolve(t)).map(|_| ()).map_err(|e| {
|
||||
let mut msg = blueprint_load_prose(&e);
|
||||
if let Some(hint) = unresolved_namespace_hint(&e, env) {
|
||||
msg.push_str(" — ");
|
||||
msg.push_str(&hint);
|
||||
}
|
||||
msg
|
||||
})
|
||||
}
|
||||
|
||||
/// #196: build a `Composite` from a document that is EITHER a #155 blueprint
|
||||
/// envelope (a JSON object: `format_version` + `blueprint`) OR a construction
|
||||
/// op-list (a JSON array) — shape-discriminated on the top-level JSON type,
|
||||
@@ -893,34 +862,22 @@ pub(crate) fn composite_from_any(text: &str, env: &aura_runner::project::Env) ->
|
||||
/// function — C29's "registered artifacts are never retroactively
|
||||
/// invalidated" stays intact.
|
||||
///
|
||||
/// **Deliberate exceptions — direct `gate_authored_root_name` callers
|
||||
/// (main.rs).** A handful of fresh-FILE intakes reach the same unsanitized
|
||||
/// `traces/<name>/` seam (or `put_blueprint` the loaded envelope straight
|
||||
/// into the registry) without going through this wrapper, because each
|
||||
/// **Deliberate exception — the one direct `gate_authored_root_name` caller
|
||||
/// (main.rs).** One fresh-FILE intake reaches the same unsanitized
|
||||
/// `traces/<name>/` seam without going through this wrapper, because it
|
||||
/// already parses the document its own way and only needs the shared root-
|
||||
/// name gate bolted on, not the shape-discrimination `composite_from_any`
|
||||
/// does:
|
||||
/// - `aura run <blueprint.json>` (`dispatch_run`): envelope-only grammar (no
|
||||
/// op-script fallback), feeds `signal.name()` into
|
||||
/// - `exec <blueprint.json>` (`exec_blueprint_leg`, #319): envelope-only
|
||||
/// grammar (no op-script fallback), feeds `signal.name()` into
|
||||
/// `run_signal_r`/`run_measurement` -> `bind_tap_plan` ->
|
||||
/// `TraceStore::begin_run`.
|
||||
/// - `validate_and_register_axes` (shared by `generalize`,
|
||||
/// `sweep --real`, `walkforward --real`, `mc --real`): `put_blueprint`s
|
||||
/// the loaded envelope by topology hash before any of those four verbs
|
||||
/// touches an archive.
|
||||
/// - `run_blueprint_sweep` / `run_blueprint_walkforward` / `run_blueprint_mc`
|
||||
/// (the synthetic, no-`--real` family builders): same `put_blueprint`
|
||||
/// reason, on the routes that bypass `validate_and_register_axes`
|
||||
/// entirely (#331 cycle-close — these used to plant an ungated envelope
|
||||
/// in the store; see each fn's own comment).
|
||||
/// - `list_blueprint_axes` (`aura sweep <FILE> --list-axes`, main.rs): no
|
||||
/// registry write and no trace directory here, but a shape-violating root
|
||||
/// name otherwise mangles through `wrapped_to_raw_axis` into a printed,
|
||||
/// non-bindable axis name instead of refusing (#331 fieldtest finding
|
||||
/// c331_2e) — the class rule is every CLI intake reading an authored
|
||||
/// envelope from a file gates the root name, not only the writing ones.
|
||||
///
|
||||
/// All of these share the identical `name_gate` + `name_gate_fault_prose`
|
||||
/// (The quintet's other direct callers — `dispatch_run`,
|
||||
/// `validate_and_register_axes`, `run_blueprint_sweep`/`_walkforward`/`_mc`,
|
||||
/// `list_blueprint_axes` — retired with the quintet, #319 Task 8.)
|
||||
///
|
||||
/// This shares the identical `name_gate` + `name_gate_fault_prose`
|
||||
/// primitives this wrapper uses, so the refusal wording is byte-identical
|
||||
/// across every authored-file-intake route even though the shape-
|
||||
/// discrimination step is not shared by them.
|
||||
@@ -934,10 +891,10 @@ pub(crate) fn composite_from_authored_text(
|
||||
}
|
||||
|
||||
/// The root-name shape gate itself (#331 delta re-review), factored out of
|
||||
/// [`composite_from_authored_text`] so `dispatch_run`'s narrower-grammar file
|
||||
/// intake (see that fn's doc comment) can share the exact `name_gate` call and
|
||||
/// `name_gate_fault_prose` wording without going through the shape-discriminating
|
||||
/// wrapper.
|
||||
/// [`composite_from_authored_text`] so `exec_blueprint_leg`'s narrower-grammar
|
||||
/// file intake (see that fn's doc comment) can share the exact `name_gate`
|
||||
/// call and `name_gate_fault_prose` wording without going through the
|
||||
/// shape-discriminating wrapper.
|
||||
pub(crate) fn gate_authored_root_name(name: &str) -> Result<(), String> {
|
||||
name_gate(name).map_err(|fault| name_gate_fault_prose(name, &fault))
|
||||
}
|
||||
@@ -945,7 +902,7 @@ pub(crate) fn gate_authored_root_name(name: &str) -> Result<(), String> {
|
||||
/// Resolve a blueprint document's bytes from a file path or a 64-hex content
|
||||
/// id in the project store (the campaign-run target-addressing convention).
|
||||
/// The id shape is `aura_runner::axes::is_content_id` — the same predicate
|
||||
/// `campaign run`'s target resolution uses, so the two FILE-or-id surfaces
|
||||
/// `exec`'s campaign-target resolution uses, so the two FILE-or-id surfaces
|
||||
/// cannot drift apart on what counts as a store address. The `bool` names
|
||||
/// which branch fired: `true` for a FRESH FILE read, `false` for a STORE
|
||||
/// (content-id) fetch — `params_lines` (#331 delta re-review) uses it to
|
||||
@@ -982,8 +939,9 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
|
||||
/// the same form `introspect --node` already uses for param kinds. Followed
|
||||
/// by one `{name}:{kind:?} default={value}` line per BOUND param (#328):
|
||||
/// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name
|
||||
/// concatenation is needed — line-identical to the reconciled `--list-axes`
|
||||
/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon.
|
||||
/// concatenation is needed — line-identical to the retired `aura sweep
|
||||
/// --list-axes`'s own bound pass (`list_blueprint_axes`, main.rs, gone with
|
||||
/// the #319 sugar retirement), same `render_value` lexicon.
|
||||
/// #331 delta re-review: the FILE target is a FOURTH freshly-authored-file
|
||||
/// intake this fn's own `composite_from_any` call had missed gating — a FILE
|
||||
/// routes through [`composite_from_authored_text`] instead; a content id
|
||||
|
||||
+620
-2147
File diff suppressed because it is too large
Load Diff
@@ -65,7 +65,7 @@ impl DocIntrospectCmd {
|
||||
/// Parse `--parallel-instruments` in domain terms: clap's built-in
|
||||
/// `NonZeroUsize` parser would leak the Rust type name ("number would be
|
||||
/// zero for non-zero type") at exactly the moment a user needs guidance.
|
||||
fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
|
||||
pub(crate) fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
|
||||
s.parse::<usize>().ok().and_then(std::num::NonZeroUsize::new).ok_or_else(|| {
|
||||
"must be a whole number of at least 1 — it bounds how many distinct \
|
||||
instruments are resident in parallel"
|
||||
@@ -411,19 +411,6 @@ pub enum CampaignSub {
|
||||
Introspect(DocIntrospectCmd),
|
||||
/// Register a valid campaign document into the store under the runs root.
|
||||
Register { file: PathBuf },
|
||||
/// Execute a stored campaign into a realized run-set (a .json file is
|
||||
/// register-then-run sugar; the canonical address is the content id).
|
||||
Run {
|
||||
target: String,
|
||||
/// Bound on distinct instruments resident in parallel (the RAM
|
||||
/// lever; 1 reproduces the sequential loop's footprint).
|
||||
#[arg(
|
||||
long,
|
||||
default_value_t = aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
value_parser = parse_parallel_instruments,
|
||||
)]
|
||||
parallel_instruments: std::num::NonZeroUsize,
|
||||
},
|
||||
/// List stored campaign realizations, or dump one campaign's records
|
||||
/// (the bare store lines, not the run-emit wrapper).
|
||||
Runs { campaign: Option<String>, run: Option<usize> },
|
||||
@@ -542,18 +529,7 @@ fn campaign_runs(campaign: Option<&str>, run: Option<usize>, env: &Env) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// #272: `Run` threads a failed-cell count (exit 3 on completed-with-failures,
|
||||
/// via `exit_on_campaign_result`), so it is pulled out of the unified
|
||||
/// `Result<(), String>` match the other four subcommands still share.
|
||||
pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
if let CampaignSub::Run { target, parallel_instruments } = &cmd.sub {
|
||||
crate::exit_on_campaign_result(crate::campaign_run::run_campaign(
|
||||
target,
|
||||
env,
|
||||
*parallel_instruments,
|
||||
));
|
||||
return;
|
||||
}
|
||||
let result = match &cmd.sub {
|
||||
CampaignSub::Validate { file } => validate_campaign_file(file, env),
|
||||
CampaignSub::Introspect(i) => {
|
||||
@@ -563,7 +539,6 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
CampaignSub::Register { file } => register_campaign(file, env),
|
||||
CampaignSub::Runs { campaign, run } => campaign_runs(campaign.as_deref(), *run, env),
|
||||
CampaignSub::Show { id } => show_campaign(id, env),
|
||||
CampaignSub::Run { .. } => unreachable!("handled above"),
|
||||
};
|
||||
if let Err(m) = result {
|
||||
eprintln!("aura: {m}");
|
||||
|
||||
@@ -231,12 +231,13 @@ const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data
|
||||
This directory is an aura project: blueprints + research documents over the
|
||||
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
|
||||
- Run: `aura run blueprints/signal.json` (the starter is closed — all
|
||||
params bound; bound values are defaults — any `--axis` may override them
|
||||
(#246))
|
||||
- Sweep: `aura sweep blueprints/signal.json --axis fast.length=2,4,8`
|
||||
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes, RAW
|
||||
`<node>.<param>` names — #328)
|
||||
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
|
||||
— the starter is closed; all params bound; bound values are defaults —
|
||||
`--override NODE.PARAM=VALUE` may override one for this run (#246))
|
||||
- Campaign: `aura exec <campaign.json>` executes a registered campaign
|
||||
document (file or content id) — the multi-cell/axis surface
|
||||
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
|
||||
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
@@ -246,9 +247,9 @@ std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
unit R, and quality metrics are R-based. Entry signals become held state
|
||||
via the signal-side latch/edge-pulse idiom (see
|
||||
`aura graph introspect --vocabulary`).
|
||||
- Data plane: the research verbs are sugar over registered process/campaign
|
||||
documents — author them directly with `aura process` / `aura campaign`,
|
||||
growing one from a bare `{}` via `aura campaign introspect --unwired`.
|
||||
- Data plane: process/campaign documents are authored directly with
|
||||
`aura process` / `aura campaign`, growing one from a bare `{}` via
|
||||
`aura campaign introspect --unwired`.
|
||||
"#;
|
||||
|
||||
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
|
||||
@@ -459,12 +460,12 @@ mod tests {
|
||||
}
|
||||
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
||||
assert!(claude.contains("demo-lab"));
|
||||
// The CLAUDE.md sweep quickstart targets the closed starter itself
|
||||
// with the RAW axis name (#246: a bound param is a default an axis
|
||||
// overrides; #328: the axis namespace is raw, no blueprint-name
|
||||
// prefix).
|
||||
// The CLAUDE.md quickstart targets the closed starter itself through
|
||||
// the surviving surface (#319): exec for both document classes, the
|
||||
// #246 override residue, and raw-namespace axis discovery (#328).
|
||||
assert!(claude.contains("blueprints/signal.json"));
|
||||
assert!(claude.contains("--axis fast.length"));
|
||||
assert!(claude.contains("--override NODE.PARAM=VALUE"));
|
||||
assert!(claude.contains("graph introspect --params"));
|
||||
}
|
||||
|
||||
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,47 +5,136 @@
|
||||
//! handled cleanly, never a panic.
|
||||
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
mod common;
|
||||
use common::{fresh_project_with_data, ScratchGuard, ScratchPath};
|
||||
|
||||
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
|
||||
/// crate; the binary is named `aura` in `Cargo.toml`).
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
||||
|
||||
/// A fresh, unique working directory so the spawned `aura sweep` writes its
|
||||
/// `./runs/runs.jsonl` into a throwaway dir, never dirtying the repo. Unique
|
||||
/// per test + per process; no external tempfile dependency (mirrors `cli_run.rs`).
|
||||
fn temp_cwd(name: &str) -> std::path::PathBuf {
|
||||
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-{name}"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
||||
dir
|
||||
fn run_code_in(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
|
||||
let out = Command::new(BIN).args(args).current_dir(dir).output().expect("binary runs");
|
||||
let text = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
(text, out.status.code())
|
||||
}
|
||||
|
||||
fn write_doc(dir: &Path, name: &str, text: &str) -> std::path::PathBuf {
|
||||
let p = dir.join(name);
|
||||
std::fs::write(&p, text).expect("write doc");
|
||||
p
|
||||
}
|
||||
|
||||
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
|
||||
/// `research_docs.rs`'s constant of the same name.
|
||||
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "sweep-only",
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||
}"#;
|
||||
|
||||
/// Seed one blueprint into the built demo project's store via `aura graph
|
||||
/// register` and return its content id — copied verbatim from
|
||||
/// `research_docs.rs`'s recipe of the same name (#319 — the retired `sweep`
|
||||
/// side-effect seeding this used to ride played no role in the returned
|
||||
/// content id, a pure function of the loaded blueprint's canonical bytes;
|
||||
/// `name` labels the registered id, mirroring the old per-call sweep family
|
||||
/// name).
|
||||
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let (out, code) = run_code_in(dir, &["graph", "register", &closed_bp, "--name", name]);
|
||||
assert_eq!(code, Some(0), "seed register failed: {out}");
|
||||
out.lines()
|
||||
.find(|l| l.starts_with("registered blueprint "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered blueprint ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Register `doc` as a process document in the project store; returns its
|
||||
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
|
||||
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
|
||||
write_doc(dir, file, doc);
|
||||
let (out, code) = run_code_in(dir, &["process", "register", file]);
|
||||
assert_eq!(code, Some(0), "process register failed: {out}");
|
||||
out.lines()
|
||||
.find(|l| l.starts_with("registered process "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered process ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A one-strategy, two-axis (2×2) campaign document over `bp_id`/`proc_id` —
|
||||
/// copied verbatim from `exec.rs`'s `campaign_doc_json_for` of the same
|
||||
/// shape, which yields exactly four family members (the four-line stream
|
||||
/// this test's property needs).
|
||||
fn campaign_doc_json_for(instrument: &str, bp_id: &str, proc_id: &str, window: (i64, i64)) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "broken-pipe",
|
||||
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
||||
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||
"seed": 7,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#,
|
||||
from = window.0,
|
||||
to = window.1,
|
||||
)
|
||||
}
|
||||
|
||||
/// Property: a family-emitting command whose stdout reader closes early exits on
|
||||
/// the EPIPE cleanly — it does NOT panic (`failed printing to stdout: Broken
|
||||
/// pipe`) and does NOT exit 101. `aura sweep` streams four family-member JSON
|
||||
/// lines via `println!`; when the reader closes stdout after the first line, the
|
||||
/// next write hits a closed pipe. Rust's runtime sets SIGPIPE to SIG_IGN at
|
||||
/// startup, so that write returns EPIPE and `println!` panics rather than the
|
||||
/// process terminating quietly — exactly the behaviour a CLI must not have. This
|
||||
/// is the contract: any aura subcommand can be piped into `| head` / `| less` /
|
||||
/// pipe`) and does NOT exit 101. `aura exec <campaign.json>` streams four
|
||||
/// family-member JSON lines via `println!` (a 2×2 axis grid — #319: the
|
||||
/// surviving surface for what a bare `aura sweep` demo grid used to stream);
|
||||
/// when the reader closes stdout after the first line, the next write hits a
|
||||
/// closed pipe. Rust's runtime sets SIGPIPE to SIG_IGN at startup, so that
|
||||
/// write returns EPIPE and `println!` panics rather than the process
|
||||
/// terminating quietly — exactly the behaviour a CLI must not have. This is
|
||||
/// the contract: any aura subcommand can be piped into `| head` / `| less` /
|
||||
/// a UI pane that goes away, and it must finish quietly, not panic.
|
||||
///
|
||||
/// Autonomous: constructs its own early-closing reader inline (no `head`, no
|
||||
/// shell, no fixture files) by spawning the binary with a piped stdout and
|
||||
/// dropping the read handle after a single short read, which closes the pipe's
|
||||
/// read end and makes the writer's next write hit EPIPE.
|
||||
#[test]
|
||||
fn family_emitting_command_survives_early_reader_close() {
|
||||
let cwd = temp_cwd("broken-pipe-sweep");
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir),
|
||||
ScratchPath::File(dir.join("broken-pipe.process.json")),
|
||||
ScratchPath::File(dir.join("broken-pipe.campaign.json")),
|
||||
]);
|
||||
let bp_id = seed_blueprint(&dir, "broken-pipe-seed");
|
||||
let proc_id = register_process_doc(&dir, "broken-pipe.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
||||
write_doc(&dir, "broken-pipe.campaign.json", &doc);
|
||||
|
||||
let mut child = Command::new(BIN)
|
||||
.arg("sweep")
|
||||
.current_dir(&cwd)
|
||||
.args(["exec", "broken-pipe.campaign.json"])
|
||||
.current_dir(&dir)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("spawn aura sweep");
|
||||
.expect("spawn aura exec");
|
||||
|
||||
// Read just enough to guarantee the writer is mid-stream, then drop the read
|
||||
// handle. Dropping it closes the read end of the pipe; the next `println!`
|
||||
@@ -63,9 +152,7 @@ fn family_emitting_command_survives_early_reader_close() {
|
||||
if let Some(mut err) = child.stderr.take() {
|
||||
let _ = err.read_to_string(&mut stderr);
|
||||
}
|
||||
let status = child.wait().expect("reap aura sweep");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
let status = child.wait().expect("reap aura exec");
|
||||
|
||||
// The defect: stderr carries the broken-pipe panic.
|
||||
assert!(
|
||||
|
||||
+2346
-5053
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
//! Shared fixture helpers for the `aura-cli` end-to-end test binaries
|
||||
//! (`research_docs.rs`, `cli_run.rs`, `project_load.rs`), all of which build
|
||||
//! and drive the `tests/fixtures/demo-project` fixture through the real
|
||||
//! `aura` binary (#223).
|
||||
//! (`research_docs.rs`, `cli_run.rs`, `project_load.rs`, `exec.rs`, and —
|
||||
//! for [`synthetic_data`] alone — `project_new.rs`, `cli_broken_pipe.rs`,
|
||||
//! `graph_construct.rs`), all of which build and drive the
|
||||
//! `tests/fixtures/demo-project` fixture through the real `aura` binary
|
||||
//! (#223).
|
||||
//!
|
||||
//! **Per-test project directories, no shared store.** [`fresh_project`] mints
|
||||
//! a unique tempdir per test, wired to the shared, once-built fixture crate
|
||||
@@ -9,10 +11,10 @@
|
||||
//! no two tests ever race on the same `runs/` store — libtest's default
|
||||
//! thread parallelism applies without a serializing lock (#250).
|
||||
//!
|
||||
//! Each of the three test binaries compiles this file as its own `mod
|
||||
//! common`, so an item unused by one binary trips that binary's `-D
|
||||
//! warnings` `dead_code` lint; see the `#[allow(dead_code)]` markers below
|
||||
//! rather than deleting a helper just because one binary doesn't need it.
|
||||
//! Each consuming test binary compiles this file as its own `mod common`, so
|
||||
//! an item unused by one binary trips that binary's `-D warnings` `dead_code`
|
||||
//! lint; see the `#[allow(dead_code)]` markers below rather than deleting a
|
||||
//! helper just because one binary doesn't need it.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
@@ -182,7 +184,7 @@ impl Drop for ScratchGuard {
|
||||
/// the two no-window `generalize` E2E tests get a real, tiny, hostless
|
||||
/// archive instead of streaming the 6.6 GB host archive (#250).
|
||||
#[allow(dead_code)]
|
||||
mod synthetic_data {
|
||||
pub mod synthetic_data {
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,9 @@
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
mod common;
|
||||
use common::{fresh_project_with_data, ScratchGuard, ScratchPath};
|
||||
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
||||
|
||||
/// Run `aura <args>` with `stdin_doc` piped in; return (stdout, stderr, success).
|
||||
@@ -714,26 +717,25 @@ fn graph_params_lists_raw_axis_namespace() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#328, cycle-close tidy fix): `graph introspect --params` and
|
||||
/// `aura sweep --list-axes` are two independently-dispatched discovery
|
||||
/// surfaces over the SAME raw axis namespace — the open pass shares one
|
||||
/// wrap-strip convention, the bound pass shares one `bound_param_space()` +
|
||||
/// `render_value` lexicon (`list_blueprint_axes`, main.rs; `params_lines`,
|
||||
/// this crate). Pinned here as a same-fixture LINE EQUALITY (not just two
|
||||
/// independently-asserted shapes) so the duplicated formatting cannot
|
||||
/// silently drift apart across an edit to either surface.
|
||||
/// Property (#328, #319 retirement residue): `graph introspect --params`
|
||||
/// prints the raw axis namespace — one `{name}:{kind:?}` line per open param,
|
||||
/// then each bound param with its default (`list_blueprint_axes`, main.rs;
|
||||
/// `params_lines`, this crate). This test used to also pin `aura sweep
|
||||
/// --list-axes` as a byte-identical SECOND discovery surface over the same
|
||||
/// namespace (guarding the two independently-dispatched surfaces against
|
||||
/// drift); `sweep` is retired, so only `graph introspect --params`'s own
|
||||
/// literal output survives here — see `graph_params_lists_raw_axis_namespace`
|
||||
/// above for the sibling pin over the same fixture.
|
||||
#[test]
|
||||
fn graph_params_and_sweep_list_axes_are_line_identical() {
|
||||
let dir = temp_cwd("params-vs-list-axes");
|
||||
fn graph_params_prints_raw_axis_names() {
|
||||
let dir = temp_cwd("params-raw-axis-names");
|
||||
let bp = fixture("r_sma_open.json");
|
||||
let (params_out, params_err, params_code) =
|
||||
run_in(&dir, &["graph", "introspect", "--params", &bp]);
|
||||
assert_eq!(params_code, Some(0), "graph introspect --params: {params_out} {params_err}");
|
||||
let (axes_out, axes_err, axes_code) = run_in(&dir, &["sweep", &bp, "--list-axes"]);
|
||||
assert_eq!(axes_code, Some(0), "sweep --list-axes: {axes_out} {axes_err}");
|
||||
assert_eq!(
|
||||
params_out, axes_out,
|
||||
"the two discovery surfaces must print byte-identical lines for the same blueprint"
|
||||
params_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
|
||||
"the raw open params, then the raw bound param with its default"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1262,13 +1264,85 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
|
||||
/// `research_docs.rs`'s constant of the same name.
|
||||
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "sweep-only",
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||
}"#;
|
||||
|
||||
/// Register `doc` as a process document in `dir`'s project store; returns its
|
||||
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
|
||||
fn register_process_doc(dir: &std::path::Path, file: &str, doc: &str) -> String {
|
||||
std::fs::write(dir.join(file), doc).expect("write process doc");
|
||||
let (out, err, code) = run_in(dir, &["process", "register", file]);
|
||||
assert_eq!(code, Some(0), "process register failed: {out} {err}");
|
||||
out.lines()
|
||||
.find(|l| l.starts_with("registered process "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered process ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A one-strategy campaign document over `bp_id`/`proc_id`, with a raw
|
||||
/// `axes_json` object literal spliced in verbatim — a campaign document
|
||||
/// refuses an empty `axes` map ("axes is empty"), so a "closed" comparison
|
||||
/// cell (nothing to vary) passes a single-value axis over one of its OWN
|
||||
/// already-bound params at its own default (a no-op reopen, #246: every
|
||||
/// bound param is an equally re-openable axis). #319: campaigns are `exec`'s
|
||||
/// surviving surface for binding a genuinely OPEN param to a value —
|
||||
/// `--override` only reopens an already-BOUND one
|
||||
/// (`aura_runner::member::override_paths` skips a name already in the open
|
||||
/// space), so the gang-axis parity tests below run both the closed example
|
||||
/// and its open-plus-axis twin through this SAME campaign machinery, over
|
||||
/// the same synthetic archive window, keeping their metrics directly
|
||||
/// comparable.
|
||||
fn one_cell_campaign_doc(bp_id: &str, proc_id: &str, window: (i64, i64), axes_json: &str) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "gang-axis-vehicle",
|
||||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }}, "axes": {{{axes_json}}} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||
"seed": 7,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#,
|
||||
from = window.0,
|
||||
to = window.1,
|
||||
)
|
||||
}
|
||||
|
||||
/// Execs a one-cell campaign document (written to `dir.join(file)`) and
|
||||
/// returns its single member's `report.metrics`.
|
||||
fn one_cell_campaign_metrics(dir: &std::path::Path, file: &str) -> serde_json::Value {
|
||||
let (out, err, code) = run_in(dir, &["exec", file]);
|
||||
assert_eq!(code, Some(0), "campaign exec failed: {out} {err}");
|
||||
let line = out
|
||||
.lines()
|
||||
.find(|l| l.starts_with(r#"{"family_id":"#))
|
||||
.unwrap_or_else(|| panic!("no member line: {out} {err}"));
|
||||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||||
v["report"]["metrics"].clone()
|
||||
}
|
||||
|
||||
/// Property (#61 + #159 cut 2): binding the shipped open example's ganged
|
||||
/// `channel_length` axis through the REAL `aura sweep` CLI pipeline (argv ->
|
||||
/// `parse_axes` -> `compile_with_cells`'s gang expansion) reproduces the exact
|
||||
/// metrics of running the shipped CLOSED example, whose `channel_hi`/
|
||||
/// `channel_lo` are separately hardwired to the same value. The equivalence
|
||||
/// between a gang and its member-bound twin is already pinned at the builder/
|
||||
/// engine level (aura-engine's `gang_e2e.rs`, and this crate's
|
||||
/// `channel_length` axis through a REAL campaign axis (#319: the surviving
|
||||
/// surface — `compile_with_cells`'s gang expansion, same as the retired
|
||||
/// `aura sweep --axis`) reproduces the exact metrics of running the shipped
|
||||
/// CLOSED example, whose `channel_hi`/`channel_lo` are separately hardwired
|
||||
/// to the same value. Both legs run as one-cell campaigns over the same
|
||||
/// synthetic archive window, so the comparison is data-source-agnostic (only
|
||||
/// the axis-vs-hardwire binding differs). The equivalence between a gang and
|
||||
/// its member-bound twin is already pinned at the builder/engine level
|
||||
/// (aura-engine's `gang_e2e.rs`, and this crate's
|
||||
/// `r_breakout_example_loaded_runs_identically_to_the_carved_signal`, which
|
||||
/// calls `run_signal_r` directly); this instead drives the actual public
|
||||
/// binary end-to-end on the actual shipped files, so a regression in the
|
||||
@@ -1278,25 +1352,53 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
|
||||
/// equivalence still holds.
|
||||
#[test]
|
||||
fn open_r_breakout_fixture_gang_axis_matches_the_closed_example() {
|
||||
let closed_dir = temp_cwd("r-breakout-gang-axis-closed");
|
||||
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["run", &example("r_breakout.json")]);
|
||||
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
|
||||
let closed: serde_json::Value =
|
||||
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir),
|
||||
ScratchPath::File(dir.join("breakout-gang.process.json")),
|
||||
ScratchPath::File(dir.join("breakout-gang-closed.campaign.json")),
|
||||
ScratchPath::File(dir.join("breakout-gang-open.campaign.json")),
|
||||
]);
|
||||
|
||||
let sweep_dir = temp_cwd("r-breakout-gang-axis-sweep");
|
||||
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
|
||||
&sweep_dir,
|
||||
&["sweep", &fixture("r_breakout_open.json"), "--axis", "channel_length=3"],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
|
||||
let lines: Vec<&str> = sweep_stdout.lines().collect();
|
||||
assert_eq!(lines.len(), 1, "one grid point for a single-value axis: {sweep_stdout}");
|
||||
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
|
||||
let (reg_closed_out, reg_closed_err, reg_closed_code) =
|
||||
run_in(&dir, &["graph", "register", &example("r_breakout.json")]);
|
||||
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
|
||||
let closed_id = registered_id(®_closed_out);
|
||||
|
||||
let (reg_open_out, reg_open_err, reg_open_code) =
|
||||
run_in(&dir, &["graph", "register", &fixture("r_breakout_open.json")]);
|
||||
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
|
||||
let open_id = registered_id(®_open_out);
|
||||
|
||||
let proc_id = register_process_doc(&dir, "breakout-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
|
||||
// The shared SYMA window used throughout the suite's campaign vehicles
|
||||
// (inside `fresh_project_with_data`'s Jan-Aug 2024 synthetic archive).
|
||||
let window = (1709251200000, 1719791999999);
|
||||
std::fs::write(
|
||||
dir.join("breakout-gang-closed.campaign.json"),
|
||||
// No-op axis: `delay.lag` reopened at its own bound default (1).
|
||||
one_cell_campaign_doc(&closed_id, &proc_id, window, r#""delay.lag": { "kind": "I64", "values": [1] }"#),
|
||||
)
|
||||
.expect("write closed campaign doc");
|
||||
std::fs::write(
|
||||
dir.join("breakout-gang-open.campaign.json"),
|
||||
one_cell_campaign_doc(
|
||||
&open_id,
|
||||
&proc_id,
|
||||
window,
|
||||
r#""channel_length": { "kind": "I64", "values": [3] }"#,
|
||||
),
|
||||
)
|
||||
.expect("write open campaign doc");
|
||||
|
||||
let closed_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-closed.campaign.json");
|
||||
let open_metrics = one_cell_campaign_metrics(&dir, "breakout-gang-open.campaign.json");
|
||||
assert_eq!(
|
||||
member["report"]["metrics"], closed["metrics"],
|
||||
"channel_length=3, bound via the real sweep CLI, must fan out to both channel_hi \
|
||||
open_metrics, closed_metrics,
|
||||
"channel_length=3, bound via the campaign axis, must fan out to both channel_hi \
|
||||
and channel_lo identically to the closed example's hardwired channel=3"
|
||||
);
|
||||
}
|
||||
@@ -1330,39 +1432,69 @@ fn open_r_meanrev_fixture_lists_its_axis_namespace() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#61 + #159 cut 3): a sweep over TWO axes at once — the ganged
|
||||
/// `window` knob (fusing `mean_window.length`/`var_window.length`) alongside
|
||||
/// the independent, un-ganged `band.factor` knob — binds both correctly
|
||||
/// through the real CLI grid, matching the shipped closed example's hardwired
|
||||
/// window=3/band=2.0. This is a distinct risk from the r_breakout gang test
|
||||
/// (a lone gang axis): a mis-scoped gang-expansion map that shifts sibling
|
||||
/// param positions could silently mis-bind the co-present un-ganged axis
|
||||
/// instead (or vice-versa) even though each axis alone still resolves.
|
||||
/// Property (#61 + #159 cut 3): a campaign over TWO axes at once (#319: the
|
||||
/// surviving surface for what `aura sweep --axis` used to do inline) — the
|
||||
/// ganged `window` knob (fusing `mean_window.length`/`var_window.length`)
|
||||
/// alongside the independent, un-ganged `band.factor` knob — binds both
|
||||
/// correctly through the real CLI grid, matching the shipped closed
|
||||
/// example's hardwired window=3/band=2.0. This is a distinct risk from the
|
||||
/// r_breakout gang test (a lone gang axis): a mis-scoped gang-expansion map
|
||||
/// that shifts sibling param positions could silently mis-bind the
|
||||
/// co-present un-ganged axis instead (or vice-versa) even though each axis
|
||||
/// alone still resolves. Both legs run as one-cell campaigns over the same
|
||||
/// synthetic archive window, so the comparison is data-source-agnostic.
|
||||
#[test]
|
||||
fn open_r_meanrev_fixture_gang_plus_plain_axis_matches_the_closed_example() {
|
||||
let closed_dir = temp_cwd("r-meanrev-gang-axis-closed");
|
||||
let (closed_stdout, closed_stderr, closed_code) = run_in(&closed_dir, &["run", &example("r_meanrev.json")]);
|
||||
assert_eq!(closed_code, Some(0), "closed run stderr: {closed_stderr}");
|
||||
let closed: serde_json::Value =
|
||||
serde_json::from_str(closed_stdout.trim()).expect("closed run report parses as JSON");
|
||||
let (dir, _fixture) = fresh_project_with_data();
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let _cleanup = ScratchGuard(vec![
|
||||
ScratchPath::Dir(runs_dir),
|
||||
ScratchPath::File(dir.join("meanrev-gang.process.json")),
|
||||
ScratchPath::File(dir.join("meanrev-gang-closed.campaign.json")),
|
||||
ScratchPath::File(dir.join("meanrev-gang-open.campaign.json")),
|
||||
]);
|
||||
|
||||
let sweep_dir = temp_cwd("r-meanrev-gang-axis-sweep");
|
||||
let (sweep_stdout, sweep_stderr, sweep_code) = run_in(
|
||||
&sweep_dir,
|
||||
&[
|
||||
"sweep", &fixture("r_meanrev_open.json"),
|
||||
"--axis", "window=3",
|
||||
"--axis", "band.factor=2.0",
|
||||
],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
|
||||
let lines: Vec<&str> = sweep_stdout.lines().collect();
|
||||
assert_eq!(lines.len(), 1, "one grid point for two single-value axes: {sweep_stdout}");
|
||||
let member: serde_json::Value = serde_json::from_str(lines[0]).expect("member line parses as JSON");
|
||||
let (reg_closed_out, reg_closed_err, reg_closed_code) =
|
||||
run_in(&dir, &["graph", "register", &example("r_meanrev.json")]);
|
||||
assert_eq!(reg_closed_code, Some(0), "register closed: {reg_closed_out} {reg_closed_err}");
|
||||
let closed_id = registered_id(®_closed_out);
|
||||
|
||||
let (reg_open_out, reg_open_err, reg_open_code) =
|
||||
run_in(&dir, &["graph", "register", &fixture("r_meanrev_open.json")]);
|
||||
assert_eq!(reg_open_code, Some(0), "register open: {reg_open_out} {reg_open_err}");
|
||||
let open_id = registered_id(®_open_out);
|
||||
|
||||
let proc_id = register_process_doc(&dir, "meanrev-gang.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
|
||||
let window = (1709251200000, 1719791999999);
|
||||
std::fs::write(
|
||||
dir.join("meanrev-gang-closed.campaign.json"),
|
||||
// No-op axis: `mean_window.length` reopened at its own bound default (3).
|
||||
one_cell_campaign_doc(
|
||||
&closed_id,
|
||||
&proc_id,
|
||||
window,
|
||||
r#""mean_window.length": { "kind": "I64", "values": [3] }"#,
|
||||
),
|
||||
)
|
||||
.expect("write closed campaign doc");
|
||||
std::fs::write(
|
||||
dir.join("meanrev-gang-open.campaign.json"),
|
||||
one_cell_campaign_doc(
|
||||
&open_id,
|
||||
&proc_id,
|
||||
window,
|
||||
r#""window": { "kind": "I64", "values": [3] }, "band.factor": { "kind": "F64", "values": [2.0] }"#,
|
||||
),
|
||||
)
|
||||
.expect("write open campaign doc");
|
||||
|
||||
let closed_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-closed.campaign.json");
|
||||
let open_metrics = one_cell_campaign_metrics(&dir, "meanrev-gang-open.campaign.json");
|
||||
assert_eq!(
|
||||
member["report"]["metrics"], closed["metrics"],
|
||||
"window=3 (ganged) + band.factor=2.0 (plain), bound via the real sweep CLI, must \
|
||||
open_metrics, closed_metrics,
|
||||
"window=3 (ganged) + band.factor=2.0 (plain), bound via the campaign axes, must \
|
||||
match the closed example's hardwired window=3/band=2.0"
|
||||
);
|
||||
}
|
||||
@@ -1834,7 +1966,7 @@ fn tap_authored_via_op_script_runs_and_persists_the_series() {
|
||||
let bp_path = dir.join("tapped.json");
|
||||
std::fs::write(&bp_path, &blueprint_json).expect("write built blueprint");
|
||||
|
||||
let (_stdout, stderr2, code2) = run_in(&dir, &["run", bp_path.to_str().unwrap()]);
|
||||
let (_stdout, stderr2, code2) = run_in(&dir, &["exec", bp_path.to_str().unwrap()]);
|
||||
assert_eq!(code2, Some(0), "aura run over the op-authored tap: {stderr2}");
|
||||
|
||||
// `aura graph build` always names the root composite "graph" (`composite_from_str`),
|
||||
@@ -2076,10 +2208,10 @@ fn graph_build_use_docless_source_refuses_with_the_c29_shape_exit_1() {
|
||||
|
||||
/// The worked acceptance flow's last leg (spec §Concrete code shapes): a
|
||||
/// pattern registered with an open param splices under an instance, and
|
||||
/// `aura sweep --list-axes` on the CONSUMER shows the path-qualified bare
|
||||
/// (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the existing
|
||||
/// nested-composite param-prefix discipline, reached through `use` this
|
||||
/// time, not a Rust-authored nested composite.
|
||||
/// `aura graph introspect --params` on the CONSUMER shows the path-qualified
|
||||
/// bare (unbound) axis `graph.<instance>.<node>.<param>:<kind>` — the
|
||||
/// existing nested-composite param-prefix discipline, reached through `use`
|
||||
/// this time, not a Rust-authored nested composite.
|
||||
#[test]
|
||||
fn graph_build_use_end_to_end_axes() {
|
||||
let dir = temp_cwd("use-end-to-end-axes");
|
||||
@@ -2114,8 +2246,8 @@ fn graph_build_use_end_to_end_axes() {
|
||||
std::fs::write(&consumer_bp, &build.stdout).expect("write consumer envelope");
|
||||
|
||||
let (axes_out, axes_err, axes_code) =
|
||||
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
|
||||
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
|
||||
run_in(&dir, &["graph", "introspect", "--params", consumer_bp.to_str().unwrap()]);
|
||||
assert_eq!(axes_code, Some(0), "graph introspect --params: {axes_out} {axes_err}");
|
||||
assert_eq!(
|
||||
axes_out, "trend.sma.length:I64\n",
|
||||
"the spliced instance's open param surfaces path-qualified, bare (unbound) RAW form (#328)"
|
||||
@@ -2158,7 +2290,7 @@ fn running_an_open_blueprint_refuses_at_bootstrap() {
|
||||
assert!(built, "an open (Input) root role finishes without root-role boundness (#317)");
|
||||
let bp = dir.join("open.bp.json");
|
||||
std::fs::write(&bp, &build_out).expect("write built blueprint");
|
||||
let (stdout, stderr, code) = run_in(&dir, &["run", bp.to_str().unwrap()]);
|
||||
let (stdout, stderr, code) = run_in(&dir, &["exec", bp.to_str().unwrap()]);
|
||||
assert_eq!(code, Some(1), "an open blueprint refuses standalone at bootstrap, not finish: {stdout} {stderr}");
|
||||
assert!(stdout.is_empty(), "no report emitted on a bootstrap refusal: {stdout}");
|
||||
assert!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! End-to-end pins for the self-description surfaces (#315, #323): the help
|
||||
//! opens with the two-layer concept, the sugar verbs name the document shape
|
||||
//! they desugar to, every introspection roster carries per-entry meanings,
|
||||
//! and `graph build --help` carries the op-list reference. Driven over the
|
||||
//! End-to-end pins for the self-description surfaces (#315, #323, #319): the
|
||||
//! help opens with the two-layer concept, `exec` names both document classes
|
||||
//! it executes, every introspection roster carries per-entry meanings, and
|
||||
//! `graph build --help` carries the op-list reference. Driven over the
|
||||
//! built binary — the zero-setup surface a reader without repo access gets.
|
||||
|
||||
use std::process::Command;
|
||||
@@ -26,7 +26,10 @@ fn run(args: &[&str]) -> (String, String, bool) {
|
||||
fn top_level_help_opens_with_the_two_layer_concept() {
|
||||
let (out, err, ok) = run(&["--help"]);
|
||||
assert!(ok, "aura --help failed: {err}");
|
||||
assert!(out.contains("research verbs"), "names the sugar layer: {out}");
|
||||
assert!(
|
||||
out.contains("the one executor over both document classes"),
|
||||
"names exec + the document classes: {out}"
|
||||
);
|
||||
assert!(out.contains("directly authorable"), "names the document data plane: {out}");
|
||||
assert!(out.contains("bias in [-1,+1]"), "names the bias output: {out}");
|
||||
assert!(out.contains("defines the risk unit"), "names the protective stop / R: {out}");
|
||||
@@ -34,26 +37,16 @@ fn top_level_help_opens_with_the_two_layer_concept() {
|
||||
assert!(out.contains("aura chart"), "names the trace consumers: {out}");
|
||||
}
|
||||
|
||||
/// #315: each document-bridged sugar verb's long help names the process
|
||||
/// shape it desugars to and points at the document layer. `run` (not
|
||||
/// document-bridged) points at the canonical document-first form instead.
|
||||
/// #319: `exec --help` names both document classes it accepts — the sugar
|
||||
/// verbs' retirement leaves `exec` as the one executor over a campaign
|
||||
/// document (file or content id) or a signal blueprint (single run).
|
||||
#[test]
|
||||
fn sugar_verbs_name_their_document_shape() {
|
||||
for (verb, needle) in [
|
||||
("sweep", "std::sweep"),
|
||||
("walkforward", "std::walk_forward"),
|
||||
("mc", "std::monte_carlo"),
|
||||
("generalize", "std::generalize"),
|
||||
] {
|
||||
let (out, err, ok) = run(&[verb, "--help"]);
|
||||
assert!(ok, "aura {verb} --help failed: {err}");
|
||||
assert!(out.contains("Sugar"), "{verb} --help names the sugar relation: {out}");
|
||||
assert!(out.contains(needle), "{verb} --help names {needle}: {out}");
|
||||
assert!(out.contains("aura campaign"), "{verb} --help points at the documents: {out}");
|
||||
}
|
||||
let (out, err, ok) = run(&["run", "--help"]);
|
||||
assert!(ok, "aura run --help failed: {err}");
|
||||
assert!(out.contains("document-first"), "run --help names the canonical form: {out}");
|
||||
fn exec_help_names_both_document_classes() {
|
||||
let (out, err, ok) = run(&["exec", "--help"]);
|
||||
assert!(ok, "aura exec --help failed: {err}");
|
||||
assert!(out.contains("campaign"), "names the campaign document class: {out}");
|
||||
assert!(out.contains("blueprint"), "names the blueprint document class: {out}");
|
||||
assert!(out.contains("single run"), "names the blueprint leg's single-run shape: {out}");
|
||||
}
|
||||
|
||||
/// #323: `graph build --help` carries the op-list reference — the op kinds
|
||||
|
||||
@@ -35,7 +35,7 @@ fn run_measurement(cwd: &Path) {
|
||||
let bp = cwd.join("measurement.json");
|
||||
std::fs::write(&bp, two_tap_blueprint_json()).expect("write blueprint");
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp.to_str().unwrap()])
|
||||
.args(["exec", bp.to_str().unwrap()])
|
||||
.current_dir(cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
|
||||
@@ -38,13 +38,13 @@ fn aura(args: &[&str], cwd: &Path) -> Output {
|
||||
#[test]
|
||||
fn project_run_resolves_demo_node_and_is_bit_identical() {
|
||||
let dir = built_project();
|
||||
let a = aura(&["run", "demo_signal.json"], dir);
|
||||
let a = aura(&["exec", "demo_signal.json"], dir);
|
||||
assert!(
|
||||
a.status.success(),
|
||||
"run failed: {}",
|
||||
String::from_utf8_lossy(&a.stderr)
|
||||
);
|
||||
let b = aura(&["run", "demo_signal.json"], dir);
|
||||
let b = aura(&["exec", "demo_signal.json"], dir);
|
||||
assert!(b.status.success());
|
||||
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
|
||||
}
|
||||
@@ -52,7 +52,7 @@ fn project_run_resolves_demo_node_and_is_bit_identical() {
|
||||
#[test]
|
||||
fn project_run_manifest_carries_provenance() {
|
||||
let dir = built_project();
|
||||
let out = aura(&["run", "demo_signal.json"], dir);
|
||||
let out = aura(&["exec", "demo_signal.json"], dir);
|
||||
assert!(out.status.success());
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_slice(&out.stdout).expect("run report is JSON");
|
||||
@@ -83,7 +83,7 @@ fn outside_a_project_the_demo_blueprint_is_unknown() {
|
||||
// cwd = the aura-cli crate dir: no Aura.toml anywhere up the tree.
|
||||
let cwd = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let bp = fixture_dir().join("demo_signal.json");
|
||||
let out = aura(&["run", bp.to_str().unwrap()], cwd);
|
||||
let out = aura(&["exec", bp.to_str().unwrap()], cwd);
|
||||
assert!(!out.status.success());
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
@@ -104,31 +104,91 @@ fn outside_a_project_the_demo_blueprint_is_unknown() {
|
||||
/// `<project-root>/runs/`, and no stray `runs/` must appear under the
|
||||
/// subdirectory. This is the property that makes `[paths]` project-relative
|
||||
/// rather than shell-relative (C17).
|
||||
///
|
||||
/// #319 vehicle note: the synthetic in-process family builder this test used
|
||||
/// to drive via bare `sweep --axis` (no `--real`) retired with the quintet —
|
||||
/// no surviving CLI surface produces a family record without touching a real
|
||||
/// archive (`exec`'s campaign leg always resolves against `DataServer`), so
|
||||
/// the vehicle is now a one-cell campaign over the `fresh_project_with_data`
|
||||
/// synthetic `SYMA` archive (mirrors `exec.rs`'s `seed_blueprint` +
|
||||
/// `campaign_doc_json_for` recipe), executed from a project subdirectory.
|
||||
#[test]
|
||||
fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
|
||||
let dir = built_project();
|
||||
let sub = dir.join("src");
|
||||
let (dir, _fixture) = common::fresh_project_with_data();
|
||||
let sub = dir.join("sub");
|
||||
std::fs::create_dir_all(&sub).expect("create invocation subdirectory");
|
||||
let runs_dir = dir.join("runs");
|
||||
std::fs::remove_dir_all(&runs_dir).ok();
|
||||
let closed_bp =
|
||||
format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let reg_out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["graph", "register", &closed_bp, "--name", "proj-anchor-seed"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura graph register");
|
||||
assert!(reg_out.status.success(), "register failed: {}", String::from_utf8_lossy(®_out.stderr));
|
||||
let reg_text = String::from_utf8_lossy(®_out.stdout);
|
||||
let bp_id = reg_text
|
||||
.lines()
|
||||
.find(|l| l.starts_with("registered blueprint "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered blueprint ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.to_string();
|
||||
|
||||
let process_doc = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "sweep-only",
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||
}"#;
|
||||
std::fs::write(dir.join("anchor.process.json"), process_doc).expect("write process doc");
|
||||
let proc_out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["process", "register", "anchor.process.json"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura process register");
|
||||
assert!(proc_out.status.success(), "process register failed: {}", String::from_utf8_lossy(&proc_out.stderr));
|
||||
let proc_text = String::from_utf8_lossy(&proc_out.stdout);
|
||||
let proc_id = proc_text
|
||||
.lines()
|
||||
.find(|l| l.starts_with("registered process "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered process ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
|
||||
// The established `SYMA` window (2024-03..06, fully inside its 2024-01..08
|
||||
// span) other #319 vehicle campaigns use.
|
||||
let campaign_doc = format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "run-seam",
|
||||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
||||
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||
"seed": 7,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#
|
||||
);
|
||||
std::fs::write(dir.join("anchor.campaign.json"), &campaign_doc).expect("write campaign doc");
|
||||
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"proj-anchor",
|
||||
])
|
||||
.args(["exec", "../anchor.campaign.json"])
|
||||
.current_dir(&sub)
|
||||
.output()
|
||||
.expect("spawn aura sweep");
|
||||
.expect("spawn aura exec");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"sweep failed: {}",
|
||||
"exec failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
@@ -162,7 +222,7 @@ fn vocabulary_charter_violation_refuses_end_to_end() {
|
||||
"badcharter fixture build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "x.json"], &dir);
|
||||
let run = aura(&["exec", "x.json"], &dir);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
@@ -191,7 +251,7 @@ fn undescribed_vocabulary_entry_refuses_end_to_end() {
|
||||
"undescribed fixture build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "x.json"], &dir);
|
||||
let run = aura(&["exec", "x.json"], &dir);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
@@ -229,7 +289,7 @@ fn restated_name_vocabulary_entry_refuses_end_to_end() {
|
||||
"restated fixture build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "x.json"], &dir);
|
||||
let run = aura(&["exec", "x.json"], &dir);
|
||||
assert_eq!(
|
||||
run.status.code(),
|
||||
Some(1),
|
||||
@@ -287,7 +347,7 @@ fn stale_dylib_warns_naming_both_timestamps_but_still_runs() {
|
||||
set_mtime(&dylib, DYLIB_MTIME);
|
||||
set_mtime(&src, SOURCE_MTIME);
|
||||
|
||||
let out = aura(&["run", "demo_signal.json"], dir);
|
||||
let out = aura(&["exec", "demo_signal.json"], dir);
|
||||
|
||||
// Restore sane mtimes before asserting, so a failed assertion does not
|
||||
// leave the shared fixture wedged "stale" (or with a future mtime that
|
||||
@@ -351,7 +411,7 @@ fn nested_nodes_dir() -> PathBuf {
|
||||
fn data_only_project_runs_and_stamps_commit_only_provenance() {
|
||||
let dir = dataonly_dir();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "signal.json"])
|
||||
.args(["exec", "signal.json"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -387,7 +447,7 @@ fn data_only_project_runs_and_stamps_commit_only_provenance() {
|
||||
fn multi_crate_pointer_refuses_end_to_end() {
|
||||
let dir = multicrate_dir();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "x.json"])
|
||||
.args(["exec", "x.json"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -427,7 +487,7 @@ fn nodes_pointer_crate_resolves_and_stamps_its_namespace() {
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "pointer_signal.json"])
|
||||
.args(["exec", "pointer_signal.json"])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -455,7 +515,7 @@ fn data_only_project_hints_attach_for_namespaced_ids() {
|
||||
let bp = tmp.join("bp.json");
|
||||
std::fs::write(&bp, r#"{"format_version":1,"blueprint":{"name":"x","nodes":[{"primitive":{"type":"nosuch::Node"}}],"edges":[],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":0,"field":0,"name":"bias"}]}}"#).unwrap();
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", bp.to_str().unwrap()])
|
||||
.args(["exec", bp.to_str().unwrap()])
|
||||
.current_dir(&tmp)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
@@ -479,7 +539,7 @@ fn missing_artifact_refuses_with_build_hint() {
|
||||
"[package]\nname = \"nobuild\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[workspace]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let out = aura(&["run", "x.json"], &tmp);
|
||||
let out = aura(&["exec", "x.json"], &tmp);
|
||||
assert_eq!(out.status.code(), Some(1), "runtime refusal");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("cargo build"), "build hint present: {err}");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! E2E: the `aura new` scaffolder (#241 T4). Proves the data-only authoring
|
||||
//! loop — scaffold → `aura run`/`aura sweep`, zero Rust toolchain interaction
|
||||
//! — plus the refusal contract. Scaffolded projects live in per-test temp
|
||||
//! dirs.
|
||||
//! loop — scaffold → `aura exec`, zero Rust toolchain interaction — plus the
|
||||
//! refusal contract. Scaffolded projects live in per-test temp dirs.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
|
||||
mod common;
|
||||
|
||||
fn aura(args: &[&str], cwd: &Path) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(args)
|
||||
@@ -61,17 +62,17 @@ fn init_and_commit(dir: &Path) {
|
||||
///
|
||||
/// Documented deviation from the task text: the plan's wording asks this
|
||||
/// headline to also assert that the run "writes a manifest under `x/runs/`".
|
||||
/// Verified against `dispatch_run`/`run_signal_r` (main.rs, around the
|
||||
/// `tx_req`/`rx_req` channel comment): plain `aura run` never touches the
|
||||
/// trace store — it only prints the `RunReport` to stdout; only
|
||||
/// `sweep`/`mc`/campaign verbs persist (`env.trace_store()`), a pre-#241
|
||||
/// split this task's file list does not touch (giving `run` a store write
|
||||
/// would widen scope beyond `scaffold.rs`/`main.rs`'s `NewCmd`/`dispatch_new`
|
||||
/// and this test file). There is thus no on-disk manifest for THIS verb to
|
||||
/// assert against; the assertion below proves that absence directly instead
|
||||
/// of only asserting it in prose, and the persisted-store property the plan
|
||||
/// wanted is covered by `data_only_project_sweeps_without_any_build` below,
|
||||
/// which does assert `proj.join("runs").exists()`.
|
||||
/// Verified against `dispatch_exec`/`exec_blueprint_leg`/`run_signal_r`
|
||||
/// (main.rs): a single-blueprint `aura exec` never touches the trace store —
|
||||
/// it only prints the `RunReport` to stdout; only exec's campaign leg
|
||||
/// persists (`env.trace_store()`), a pre-#241 split this task's file list
|
||||
/// does not touch (giving the blueprint leg a store write would widen scope
|
||||
/// beyond `scaffold.rs`/`main.rs`'s `NewCmd`/`dispatch_new` and this test
|
||||
/// file). There is thus no on-disk manifest for THIS leg to assert against;
|
||||
/// the assertion below proves that absence directly instead of only
|
||||
/// asserting it in prose, and the persisted-store property the plan wanted
|
||||
/// is covered by `data_only_project_execs_a_campaign_without_any_build`
|
||||
/// below, which does assert `proj.join("runs").exists()`.
|
||||
#[test]
|
||||
fn new_scaffolds_and_runs_the_data_only_loop() {
|
||||
let base = tmp("loop");
|
||||
@@ -102,13 +103,13 @@ fn new_scaffolds_and_runs_the_data_only_loop() {
|
||||
assert!(!proj.join("Cargo.toml").exists(), "data-only project must have no Cargo.toml");
|
||||
assert!(!proj.join("src/lib.rs").exists(), "data-only project must have no src/lib.rs");
|
||||
|
||||
let a = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
let a = aura(&["exec", "blueprints/signal.json"], &proj);
|
||||
assert!(
|
||||
a.status.success(),
|
||||
"run failed: {}",
|
||||
"exec failed: {}",
|
||||
String::from_utf8_lossy(&a.stderr)
|
||||
);
|
||||
let b = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
let b = aura(&["exec", "blueprints/signal.json"], &proj);
|
||||
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
|
||||
let v: serde_json::Value = serde_json::from_slice(&a.stdout).expect("report is JSON");
|
||||
let p = &v["manifest"]["project"];
|
||||
@@ -116,50 +117,140 @@ fn new_scaffolds_and_runs_the_data_only_loop() {
|
||||
assert!(p.get("namespace").is_none(), "no crate was loaded, namespace must be absent: {p}");
|
||||
assert!(
|
||||
!proj.join("runs").exists(),
|
||||
"plain `aura run` must not persist a trace store (only sweep/mc/campaign do) — \
|
||||
the task text's \"writes a manifest under x/runs/\" does not hold for this verb"
|
||||
"a plain single-blueprint `aura exec` must not persist a trace store (only exec's \
|
||||
campaign leg does) — the task text's \"writes a manifest under x/runs/\" does not \
|
||||
hold for this leg"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
/// #241 headline: a fresh data-only project sweeps a dissolved verb (#218's
|
||||
/// gate) with zero build step — using the scaffolded (closed) starter
|
||||
/// blueprint and the exact axis the scaffolded CLAUDE.md advertises.
|
||||
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
|
||||
/// `research_docs.rs`'s constant of the same name.
|
||||
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "sweep-only",
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||||
}"#;
|
||||
|
||||
/// Writes a tiny, self-contained January-2024 `SYMA` archive into
|
||||
/// `<proj>/data` and points the scaffolded `Aura.toml`'s `[paths]` table at
|
||||
/// it. #319: `exec`'s campaign leg (the surviving surface for what `aura
|
||||
/// sweep --axis` used to do inline) needs a registered instrument, unlike a
|
||||
/// plain blueprint run — this keeps the scaffolder's "zero host setup"
|
||||
/// headline intact (no real archive mount) by minting the archive straight
|
||||
/// into the scaffold instead.
|
||||
fn seed_synthetic_archive(proj: &Path) {
|
||||
let data_dir = proj.join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("create synthetic archive dir");
|
||||
common::synthetic_data::write_symbol_archive(&data_dir, "SYMA", (2024, 1), (2024, 1));
|
||||
let toml_path = proj.join("Aura.toml");
|
||||
let toml = std::fs::read_to_string(&toml_path).expect("read scaffolded Aura.toml");
|
||||
let toml = toml.replacen("runs = \"runs\"\n", "runs = \"runs\"\ndata = \"data\"\n", 1);
|
||||
std::fs::write(&toml_path, toml).expect("point Aura.toml at the synthetic archive");
|
||||
}
|
||||
|
||||
/// The id extracted from a `registered <kind> <id> (<path>)` line (#194: bare
|
||||
/// id, no `content:` prefix — tolerant of the pre-#194 prefix all the same).
|
||||
fn registered_id(stdout: &str, kind: &str) -> String {
|
||||
let needle = format!("registered {kind} ");
|
||||
stdout
|
||||
.lines()
|
||||
.find(|l| l.starts_with(&needle))
|
||||
.unwrap_or_else(|| panic!("no \"{needle}\" line: {stdout}"))
|
||||
.trim_start_matches(&needle)
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// A one-strategy, one-axis campaign document over `bp_id`/`proc_id` — the
|
||||
/// surviving surface for what `aura sweep --axis` used to do inline over the
|
||||
/// scaffolded starter's bound `fast.length` (#246: a bound param is a default
|
||||
/// an axis overrides). Windowed fully inside `seed_synthetic_archive`'s
|
||||
/// January-2024 `SYMA` archive.
|
||||
fn one_axis_campaign_doc(bp_id: &str, proc_id: &str, axis_name: &str, axis_values: &str) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "quickstart",
|
||||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1704067200000, "to_ms": 1706745599999 }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||||
"axes": {{ "{axis_name}": {{ "kind": "I64", "values": [{axis_values}] }} }} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||||
"seed": 1,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
/// #241 headline: a fresh data-only project executes a small campaign — #319's
|
||||
/// surviving surface for the retired `aura sweep` — with zero build step,
|
||||
/// using the scaffolded (closed) starter blueprint and the exact axis the
|
||||
/// scaffolded CLAUDE.md advertises. Registering the blueprint straight (`aura
|
||||
/// graph register`, not a sweep side-effect) and seeding a tiny synthetic
|
||||
/// archive (`seed_synthetic_archive`) keeps the loop free of any host data
|
||||
/// mount, matching the pre-retirement headline.
|
||||
///
|
||||
/// #246: the closed starter IS the sweep target — a bound param is a default
|
||||
/// an axis overrides.
|
||||
/// #246: the closed starter IS the campaign axis target — a bound param is a
|
||||
/// default an axis overrides.
|
||||
#[test]
|
||||
fn data_only_project_sweeps_without_any_build() {
|
||||
fn data_only_project_execs_a_campaign_without_any_build() {
|
||||
let base = tmp("sweep");
|
||||
let new = aura(&["new", "scratch"], &base);
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4"],
|
||||
&proj,
|
||||
);
|
||||
seed_synthetic_archive(&proj);
|
||||
|
||||
let reg = aura(&["graph", "register", "blueprints/signal.json"], &proj);
|
||||
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(®.stderr));
|
||||
let bp_id = registered_id(&String::from_utf8_lossy(®.stdout), "blueprint");
|
||||
|
||||
std::fs::write(proj.join("quickstart.process.json"), SWEEP_ONLY_PROCESS_DOC)
|
||||
.expect("write process doc");
|
||||
let preg = aura(&["process", "register", "quickstart.process.json"], &proj);
|
||||
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
|
||||
let proc_id = registered_id(&String::from_utf8_lossy(&preg.stdout), "process");
|
||||
|
||||
let doc = one_axis_campaign_doc(&bp_id, &proc_id, "fast.length", "2, 4");
|
||||
std::fs::write(proj.join("quickstart.campaign.json"), &doc).expect("write campaign doc");
|
||||
let out = aura(&["exec", "quickstart.campaign.json"], &proj);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
assert!(proj.join("runs").exists(), "store must land inside the project");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
/// Property (#246): the scaffold's sweep quickstart genuinely re-opens the
|
||||
/// Property (#246): the scaffold's campaign quickstart genuinely re-opens the
|
||||
/// bound `fast.length` param — the persisted family has exactly one member
|
||||
/// per axis value, not a single collapsed member. A silent regression where
|
||||
/// the bound-param override is dropped (member always built from the
|
||||
/// blueprint's own default) would still exit 0 and still create a `runs`
|
||||
/// store, so `data_only_project_sweeps_without_any_build` above cannot catch
|
||||
/// it; only the member count can.
|
||||
/// store, so `data_only_project_execs_a_campaign_without_any_build` above
|
||||
/// cannot catch it; only the member count can.
|
||||
#[test]
|
||||
fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() {
|
||||
fn data_only_project_campaign_over_the_starter_opens_one_member_per_axis_value() {
|
||||
let base = tmp("sweep-members");
|
||||
let new = aura(&["new", "scratch"], &base);
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4,8"],
|
||||
&proj,
|
||||
);
|
||||
seed_synthetic_archive(&proj);
|
||||
|
||||
let reg = aura(&["graph", "register", "blueprints/signal.json"], &proj);
|
||||
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(®.stderr));
|
||||
let bp_id = registered_id(&String::from_utf8_lossy(®.stdout), "blueprint");
|
||||
|
||||
std::fs::write(proj.join("members.process.json"), SWEEP_ONLY_PROCESS_DOC)
|
||||
.expect("write process doc");
|
||||
let preg = aura(&["process", "register", "members.process.json"], &proj);
|
||||
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
|
||||
let proc_id = registered_id(&String::from_utf8_lossy(&preg.stdout), "process");
|
||||
|
||||
let doc = one_axis_campaign_doc(&bp_id, &proc_id, "fast.length", "2, 4, 8");
|
||||
std::fs::write(proj.join("members.campaign.json"), &doc).expect("write campaign doc");
|
||||
let out = aura(&["exec", "members.campaign.json"], &proj);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
let fams = aura(&["runs", "families"], &proj);
|
||||
assert!(fams.status.success(), "runs families exit: {:?}", fams.status);
|
||||
@@ -301,10 +392,10 @@ fn new_outside_a_work_tree_leaves_a_resolvable_head() {
|
||||
|
||||
// ...so the first quickstart run stamps a commit into the manifest, not
|
||||
// an empty provenance block.
|
||||
let run = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
let run = aura(&["exec", "blueprints/signal.json"], &proj);
|
||||
assert!(
|
||||
run.status.success(),
|
||||
"run failed: {}",
|
||||
"exec failed: {}",
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
let v: serde_json::Value = serde_json::from_slice(&run.stdout).expect("report is JSON");
|
||||
|
||||
@@ -149,23 +149,26 @@ fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>))
|
||||
}
|
||||
|
||||
/// Acceptance box 1 (#235): a project-authored node with an OPEN param is
|
||||
/// discoverable as a sweep axis, sweeps over real data into a persisted family,
|
||||
/// and that family reproduces bit-identically — the defining research loop of a
|
||||
/// genuine external project ("sweep my OWN node's knob"), proven end to end.
|
||||
/// discoverable as an axis, execs over real data (a campaign — #319: the
|
||||
/// surviving surface for what `aura sweep --axis` used to do inline) into a
|
||||
/// persisted family, and that family reproduces bit-identically — the
|
||||
/// defining research loop of a genuine external project ("vary my OWN node's
|
||||
/// knob"), proven end to end.
|
||||
#[test]
|
||||
fn project_node_open_param_sweeps_and_reproduces() {
|
||||
let (dir, _g) = fresh_project();
|
||||
|
||||
// (a) `--list-axes` discovers the PROJECT node's own open knob, followed by
|
||||
// the fixture's three BOUND params as `default=`-lines (#246: every bound
|
||||
// param is an equally re-openable `--axis`, listed regardless of how many
|
||||
// knobs happen to be open alongside it). NOT gated: enumerating axes needs
|
||||
// no data, so the discoverability half runs on every host. The name is RAW
|
||||
// `<node>.<param>` (#328: the blueprint name stays out of axis paths).
|
||||
let axes = aura_in(dir, &["sweep", "blueprints/scaled_open.json", "--list-axes"]);
|
||||
// (a) `graph introspect --params` discovers the PROJECT node's own open
|
||||
// knob, followed by the fixture's three BOUND params as `default=`-lines
|
||||
// (#246: every bound param is an equally re-openable axis, listed
|
||||
// regardless of how many knobs happen to be open alongside it). NOT
|
||||
// gated: enumerating axes needs no data, so the discoverability half runs
|
||||
// on every host. The name is RAW `<node>.<param>` (#328: the blueprint
|
||||
// name stays out of axis paths).
|
||||
let axes = aura_in(dir, &["graph", "introspect", "--params", "blueprints/scaled_open.json"]);
|
||||
assert!(
|
||||
axes.status.success(),
|
||||
"--list-axes stderr: {}",
|
||||
"graph introspect --params stderr: {}",
|
||||
String::from_utf8_lossy(&axes.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -179,37 +182,70 @@ fn project_node_open_param_sweeps_and_reproduces() {
|
||||
String::from_utf8_lossy(&axes.stderr)
|
||||
);
|
||||
|
||||
// The real-data sweep/reproduce legs are archive-gated.
|
||||
// The real-data campaign/reproduce legs are archive-gated.
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
|
||||
// (b) sweep the project node's knob over real GER40 data -> two members.
|
||||
let sweep = aura_in(
|
||||
dir,
|
||||
&[
|
||||
"sweep",
|
||||
"blueprints/scaled_open.json",
|
||||
"--real",
|
||||
"GER40",
|
||||
"--from",
|
||||
GER40_FROM_MS,
|
||||
"--to",
|
||||
GER40_TO_MS,
|
||||
"--axis",
|
||||
"gain.factor=0.5,1.0",
|
||||
"--name",
|
||||
"knob",
|
||||
],
|
||||
// (b) exec a campaign over the project node's knob, real GER40 data ->
|
||||
// two members. Register the blueprint straight (not a sweep side-effect)
|
||||
// and a minimal sweep-only process, then reference both by content id.
|
||||
let reg = aura_in(dir, &["graph", "register", "blueprints/scaled_open.json"]);
|
||||
assert!(reg.status.success(), "graph register stderr: {}", String::from_utf8_lossy(®.stderr));
|
||||
let reg_out = String::from_utf8_lossy(®.stdout).into_owned();
|
||||
let bp_id = reg_out
|
||||
.lines()
|
||||
.find(|l| l.starts_with("registered blueprint "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered blueprint ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("blueprint id")
|
||||
.to_string();
|
||||
|
||||
std::fs::write(dir.join("knob-reproduce.process.json"), SWEEP_ONLY_PROCESS)
|
||||
.expect("write process doc");
|
||||
let preg = aura_in(dir, &["process", "register", "knob-reproduce.process.json"]);
|
||||
assert!(preg.status.success(), "process register stderr: {}", String::from_utf8_lossy(&preg.stderr));
|
||||
let preg_out = String::from_utf8_lossy(&preg.stdout).into_owned();
|
||||
let proc_id = preg_out
|
||||
.lines()
|
||||
.find(|l| l.starts_with("registered process "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered process ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("process id")
|
||||
.to_string();
|
||||
|
||||
let campaign = format!(
|
||||
r#"{{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "knob-reproduce",
|
||||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||||
"strategies": [ {{ "ref": {{ "content_id": "{bp}" }},
|
||||
"axes": {{ "gain.factor": {{ "kind": "F64", "values": [0.5, 1.0] }} }} }} ],
|
||||
"process": {{ "ref": {{ "content_id": "{proc}" }} }},
|
||||
"seed": 7,
|
||||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||||
}}"#,
|
||||
from = GER40_FROM_MS,
|
||||
to = GER40_TO_MS,
|
||||
bp = bp_id,
|
||||
proc = proc_id,
|
||||
);
|
||||
std::fs::write(dir.join("knob-reproduce.campaign.json"), &campaign).expect("write campaign doc");
|
||||
|
||||
let exec = aura_in(dir, &["exec", "knob-reproduce.campaign.json"]);
|
||||
assert!(
|
||||
sweep.status.success(),
|
||||
"sweep stderr: {}",
|
||||
String::from_utf8_lossy(&sweep.stderr)
|
||||
exec.status.success(),
|
||||
"exec stderr: {}",
|
||||
String::from_utf8_lossy(&exec.stderr)
|
||||
);
|
||||
let stdout = String::from_utf8_lossy(&sweep.stdout).into_owned();
|
||||
let lines: Vec<&str> = stdout.lines().collect();
|
||||
let stdout = String::from_utf8_lossy(&exec.stdout).into_owned();
|
||||
let lines: Vec<&str> = stdout.lines().filter(|l| l.starts_with(r#"{"family_id":"#)).collect();
|
||||
assert_eq!(lines.len(), 2, "one member line per factor point: {stdout}");
|
||||
|
||||
// Each member's manifest carries the swept PROJECT-node param binding
|
||||
@@ -324,7 +360,7 @@ fn project_node_open_param_runs_one_campaign_cell() {
|
||||
);
|
||||
std::fs::write(dir.join("knob.campaign.json"), &campaign).expect("write campaign doc");
|
||||
|
||||
let run = aura_in(dir, &["campaign", "run", "knob.campaign.json"]);
|
||||
let run = aura_in(dir, &["exec", "knob.campaign.json"]);
|
||||
let out = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
|
||||
@@ -319,34 +319,9 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
|
||||
]);
|
||||
|
||||
// Seed one real, content-addressed blueprint into the project's own
|
||||
// store via a real sweep over its bound params, which the axes reopen
|
||||
// as overridable defaults (#246) (mirrors `project_load.rs`'s anchor
|
||||
// test).
|
||||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let (sweep_out, sweep_code) = run_code_in(
|
||||
&dir,
|
||||
&[
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"campaign-ref-seed",
|
||||
],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}");
|
||||
let bp_id = std::fs::read_dir(runs_dir.join("blueprints"))
|
||||
.expect("blueprints dir")
|
||||
.next()
|
||||
.expect("one stored blueprint")
|
||||
.expect("dir entry")
|
||||
.path()
|
||||
.file_stem()
|
||||
.expect("stem")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// store via `aura graph register` (#319 — the retired `sweep`
|
||||
// side-effect seeding this used to ride).
|
||||
let bp_id = seed_blueprint(&dir, "campaign-ref-seed");
|
||||
|
||||
// Register a valid AND executable process into the same project store:
|
||||
// this test's OK campaign must pass all three validate tiers, and
|
||||
@@ -441,32 +416,8 @@ fn campaign_validate_resolves_identity_ref_via_index_first_lookup_then_index_hit
|
||||
]);
|
||||
|
||||
// Seed one real, content-addressed blueprint (mirrors the content_id
|
||||
// sibling test above).
|
||||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let (sweep_out, sweep_code) = run_code_in(
|
||||
&dir,
|
||||
&[
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
"campaign-identity-seed",
|
||||
],
|
||||
);
|
||||
assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}");
|
||||
let bp_id = std::fs::read_dir(runs_dir.join("blueprints"))
|
||||
.expect("blueprints dir")
|
||||
.next()
|
||||
.expect("one stored blueprint")
|
||||
.expect("dir entry")
|
||||
.path()
|
||||
.file_stem()
|
||||
.expect("stem")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
// sibling test above; #319 — the retired `sweep` side-effect seeding).
|
||||
let bp_id = seed_blueprint(&dir, "campaign-identity-seed");
|
||||
let bp_path = runs_dir.join("blueprints").join(format!("{bp_id}.json"));
|
||||
|
||||
// The identity id of the SAME stored bytes — `--content-id FILE
|
||||
@@ -1278,35 +1229,26 @@ fn campaign_register_refuses_invalid_cost_section_and_writes_nothing() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Seed one blueprint (its params bound, reopened by the sweep axes per
|
||||
/// #246) into the built demo project's store via a real sweep and return
|
||||
/// its content id (the referential test's recipe).
|
||||
/// Seed one blueprint into the built demo project's store via `aura graph
|
||||
/// register` and return its content id (the referential test's recipe).
|
||||
/// `#319` retired the `sweep`-side-effect seeding this used to ride (its
|
||||
/// `--axis`/reopen mechanics played no role in the returned content id,
|
||||
/// which is a pure function of the loaded blueprint's canonical bytes);
|
||||
/// `name` labels the registered id for readability/uniqueness across call
|
||||
/// sites, mirroring the old per-call sweep family name.
|
||||
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let (out, code) = run_code_in(
|
||||
dir,
|
||||
&[
|
||||
"sweep",
|
||||
&closed_bp,
|
||||
"--axis",
|
||||
"fast.length=2,4",
|
||||
"--axis",
|
||||
"slow.length=8,16",
|
||||
"--name",
|
||||
name,
|
||||
],
|
||||
);
|
||||
assert_eq!(code, Some(0), "seed sweep failed: {out}");
|
||||
std::fs::read_dir(dir.join("runs").join("blueprints"))
|
||||
.expect("blueprints dir")
|
||||
let (out, code) = run_code_in(dir, &["graph", "register", &closed_bp, "--name", name]);
|
||||
assert_eq!(code, Some(0), "seed register failed: {out}");
|
||||
out.lines()
|
||||
.find(|l| l.starts_with("registered blueprint "))
|
||||
.expect("register line")
|
||||
.trim_start_matches("registered blueprint ")
|
||||
.split(' ')
|
||||
.next()
|
||||
.expect("one stored blueprint")
|
||||
.expect("dir entry")
|
||||
.path()
|
||||
.file_stem()
|
||||
.expect("stem")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Register `doc` as a process document in the project store; returns its id.
|
||||
@@ -1494,7 +1436,7 @@ const WF_PROCESS_DOC: &str = r#"{
|
||||
fn campaign_run_outside_project_refuses() {
|
||||
let dir = temp_cwd("campaign-run-outside-project");
|
||||
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "c.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "c.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("campaign run needs a project"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -1508,7 +1450,7 @@ fn campaign_run_outside_project_refuses() {
|
||||
#[test]
|
||||
fn campaign_run_bogus_target_refuses() {
|
||||
let (dir, _fixture) = fresh_project();
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "no-such-target"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "no-such-target"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("'no-such-target' is neither a readable .json file nor a 64-hex content id"),
|
||||
@@ -1521,7 +1463,7 @@ fn campaign_run_bogus_target_refuses() {
|
||||
fn campaign_run_unknown_id_refuses() {
|
||||
let (dir, _fixture) = fresh_project();
|
||||
let id = "0".repeat(64);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", &id]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains(&format!("no campaign {id} in the project store")),
|
||||
@@ -1639,7 +1581,7 @@ fn campaign_run_refuses_mc_before_walk_forward() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-mcwf-seed");
|
||||
let proc_id = register_process_doc(&dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
|
||||
write_doc(&dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mcwf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mcwf.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -1689,7 +1631,7 @@ fn campaign_run_refuses_a_non_terminal_selection_free_sweep() {
|
||||
let proc_id =
|
||||
register_process_doc(&dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC);
|
||||
write_doc(&dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "selfree.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "selfree.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains(
|
||||
@@ -1803,7 +1745,7 @@ fn campaign_run_refuses_a_grid_stage_not_immediately_before_walk_forward() {
|
||||
"gridgate.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridgate.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gridgate.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -1856,7 +1798,7 @@ fn campaign_run_synthetic_e2e_grid_then_wf_persists_no_sweep_family() {
|
||||
"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridwf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gridwf.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
|
||||
let line = out
|
||||
@@ -1918,7 +1860,7 @@ fn campaign_run_synthetic_e2e_cell_order_is_document_order_under_parallel_instru
|
||||
for k in ["1", "2"] {
|
||||
let (out, code) = run_code_in(
|
||||
&dir,
|
||||
&["campaign", "run", "cellorder.campaign.json", "--parallel-instruments", k],
|
||||
&["exec", "cellorder.campaign.json", "--parallel-instruments", k],
|
||||
);
|
||||
assert_eq!(code, Some(0), "K={k}: campaign run failed: {out}");
|
||||
let line = out
|
||||
@@ -1979,7 +1921,7 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul
|
||||
|
||||
let (out, code) = run_code_in(
|
||||
&dir,
|
||||
&["campaign", "run", "parfault.campaign.json", "--parallel-instruments", "2"],
|
||||
&["exec", "parfault.campaign.json", "--parallel-instruments", "2"],
|
||||
);
|
||||
assert_eq!(code, Some(3), "one contained fault, one success: {out}");
|
||||
let line = out
|
||||
@@ -2034,7 +1976,7 @@ fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() {
|
||||
write_doc(&dir, "gateempty.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]);
|
||||
run_code_in(&dir, &["exec", "gateempty.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}");
|
||||
assert!(
|
||||
out.contains("aura: note: cell "),
|
||||
@@ -2066,7 +2008,7 @@ fn campaign_run_refuses_single_instrument_generalize() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-gen1-seed");
|
||||
let proc_id = register_process_doc(&dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
|
||||
write_doc(&dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gen1.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gen1.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("std::generalize needs at least 2"),
|
||||
@@ -2127,7 +2069,7 @@ fn campaign_run_refuses_generalize_non_r_metric() {
|
||||
}}"#
|
||||
);
|
||||
write_doc(&dir, "genpip.campaign.json", &campaign_doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "genpip.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "genpip.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
// #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable
|
||||
// R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is
|
||||
@@ -2191,7 +2133,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-wfmc-seed");
|
||||
let proc_id = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
|
||||
write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "wfmc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wfmc.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("std::walk_forward cannot follow std::monte_carlo"),
|
||||
@@ -2400,7 +2342,7 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-zeromc-seed");
|
||||
let proc_id = register_process_doc(&dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC);
|
||||
write_doc(&dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "zeromc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "zeromc.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("process stage 1: monte_carlo resamples must be > 0"),
|
||||
@@ -2435,7 +2377,7 @@ fn campaign_run_refuses_unknown_tap_at_validate() {
|
||||
"badtap.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "badtap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "badtap.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -2478,7 +2420,7 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() {
|
||||
"taps.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "taps.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
!out.contains("unknown tap"),
|
||||
@@ -2548,7 +2490,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
||||
"\"family_table\", \"selection_report\"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "wf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wf.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -2725,7 +2667,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_pooled_oos_bootstrap() {
|
||||
// Both runs share the store (the cost-e2e sibling pattern) — the record
|
||||
// line is read from each run's own stdout, so no isolation is needed.
|
||||
let pooled = |doc: &str| -> serde_json::Value {
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", doc]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
let line = out
|
||||
.lines()
|
||||
@@ -2802,7 +2744,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap() {
|
||||
write_doc(&dir, "net_ps.campaign.json", &with_cost);
|
||||
|
||||
let per_survivor = |doc: &str| -> Vec<(u64, serde_json::Value)> {
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", doc]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
let line = out
|
||||
.lines()
|
||||
@@ -2864,7 +2806,7 @@ fn campaign_run_synthetic_e2e_per_instrument_cost_resolves_per_cell() {
|
||||
(1709251200000, 1717199999999),
|
||||
);
|
||||
write_doc(&dir, "percost.campaign.json", &doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "percost.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "percost.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
|
||||
// Member emit lines carry report.manifest; group the stamped resolved
|
||||
@@ -2925,7 +2867,7 @@ fn campaign_run_synthetic_e2e_vol_tf_regime_realizes_and_stamps() {
|
||||
);
|
||||
assert_ne!(with_risk, base, "replacen must actually match the seed field");
|
||||
write_doc(&dir, "voltf.campaign.json", &with_risk);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "voltf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "voltf.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
|
||||
let record_line = out
|
||||
@@ -3050,7 +2992,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
|
||||
write_doc(&dir, "close.campaign.json", &base);
|
||||
write_doc(&dir, "open.campaign.json", &rebound);
|
||||
|
||||
let (out_close, code_close) = run_code_in(&dir, &["campaign", "run", "close.campaign.json"]);
|
||||
let (out_close, code_close) = run_code_in(&dir, &["exec", "close.campaign.json"]);
|
||||
if code_close == Some(3)
|
||||
&& (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument"))
|
||||
{
|
||||
@@ -3058,7 +3000,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
|
||||
return;
|
||||
}
|
||||
assert_eq!(code_close, Some(0), "stdout/stderr: {out_close}");
|
||||
let (out_open, code_open) = run_code_in(&dir, &["campaign", "run", "open.campaign.json"]);
|
||||
let (out_open, code_open) = run_code_in(&dir, &["exec", "open.campaign.json"]);
|
||||
assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}");
|
||||
|
||||
let first_member = |out: &str| -> serde_json::Value {
|
||||
@@ -3113,7 +3055,7 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
|
||||
);
|
||||
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "regime.campaign.json", &with_regime);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "regime.campaign.json"]);
|
||||
|
||||
if code == Some(3)
|
||||
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
|
||||
@@ -3183,7 +3125,7 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
|
||||
);
|
||||
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "tworegimes.campaign.json", &with_regimes);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "tworegimes.campaign.json"]);
|
||||
|
||||
if code == Some(3)
|
||||
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
|
||||
@@ -3270,7 +3212,7 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
|
||||
);
|
||||
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "regime-persist.campaign.json", &with_regime);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "regime-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3350,7 +3292,7 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "cost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "cost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "cost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3445,7 +3387,7 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "net-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "net-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "net-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3559,7 +3501,7 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "volcost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "volcost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "volcost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3648,7 +3590,7 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
|
||||
);
|
||||
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "tworegimes-persist.campaign.json", &with_regimes);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "tworegimes-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3726,7 +3668,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
||||
"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mixedtap.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3842,7 +3784,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
|
||||
// (a) the cost-less baseline: net == gross, and net_r_equity skips with
|
||||
// the remedy notice while equity still persists.
|
||||
let (out_gross, code_gross) = run_code_in(&dir, &["campaign", "run", "costless.campaign.json"]);
|
||||
let (out_gross, code_gross) = run_code_in(&dir, &["exec", "costless.campaign.json"]);
|
||||
if code_gross == Some(3)
|
||||
&& (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument"))
|
||||
{
|
||||
@@ -3864,7 +3806,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
|
||||
// (b) the costed run: gross identical member-for-member, net strictly
|
||||
// below gross, no skip notice, net_r_equity persisted on disk.
|
||||
let (out_net, code_net) = run_code_in(&dir, &["campaign", "run", "costnet.campaign.json"]);
|
||||
let (out_net, code_net) = run_code_in(&dir, &["exec", "costnet.campaign.json"]);
|
||||
assert_eq!(code_net, Some(0), "costed run: {out_net}");
|
||||
assert!(
|
||||
!out_net.contains("skipped"),
|
||||
@@ -3963,7 +3905,7 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "carrycost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "carrycost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "carrycost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -4035,7 +3977,7 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
|
||||
"mc.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mc.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -4106,7 +4048,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
||||
let proc_id = register_process_doc(&dir, "mc2.process.json", MC_PROCESS_DOC);
|
||||
write_doc(&dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
|
||||
let (file_out, file_code) = run_code_in(&dir, &["campaign", "run", "mc2.campaign.json"]);
|
||||
let (file_out, file_code) = run_code_in(&dir, &["exec", "mc2.campaign.json"]);
|
||||
assert_eq!(file_code, Some(3), "stdout/stderr: {file_out}");
|
||||
|
||||
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]);
|
||||
@@ -4121,7 +4063,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (id_out, id_code) = run_code_in(&dir, &["exec", &id]);
|
||||
assert_eq!(id_code, Some(3), "stdout/stderr: {id_out}");
|
||||
|
||||
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
|
||||
@@ -4169,8 +4111,8 @@ fn campaign_run_tolerates_content_prefix_on_target() {
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
|
||||
let (bare_out, _) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (pfx_out, _) = run_code_in(&dir, &["campaign", "run", &format!("content:{id}")]);
|
||||
let (bare_out, _) = run_code_in(&dir, &["exec", &id]);
|
||||
let (pfx_out, _) = run_code_in(&dir, &["exec", &format!("content:{id}")]);
|
||||
|
||||
let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line");
|
||||
assert!(
|
||||
@@ -4222,7 +4164,7 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
|
||||
|
||||
let proc_id = register_process_doc(&dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
write_doc(&dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "onramp.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "onramp.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("no recorded geometry") || out.contains("no data for instrument"),
|
||||
@@ -4251,7 +4193,7 @@ fn campaign_run_invalid_file_refuses_before_touching_store() {
|
||||
let bad_path = write_doc(&dir, "bad.campaign.json", &bad);
|
||||
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone()), ScratchPath::File(bad_path)]);
|
||||
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "bad.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "bad.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("aura: campaign document invalid:"),
|
||||
@@ -4304,7 +4246,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
|
||||
}}"#,
|
||||
);
|
||||
write_doc(&dir, "gap.campaign.json", &doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gap.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
|
||||
let record_line = out
|
||||
@@ -4346,7 +4288,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
|
||||
/// clap refuses it as a usage error (exit 2) before any target resolution.
|
||||
#[test]
|
||||
fn campaign_run_rejects_a_zero_parallel_instruments_bound() {
|
||||
let (out, code) = run_code(&["campaign", "run", "unused", "--parallel-instruments", "0"]);
|
||||
let (out, code) = run_code(&["exec", "unused", "--parallel-instruments", "0"]);
|
||||
assert_eq!(code, Some(2), "clap usage error expected, got: {out}");
|
||||
// Domain prose, not clap's raw NonZeroUsize wording ("number would be
|
||||
// zero for non-zero type") — the fieldtest friction finding.
|
||||
|
||||
@@ -34,7 +34,7 @@ fn measurement_blueprint_runs_bare_and_emits_its_tap() {
|
||||
std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -65,7 +65,7 @@ fn measurement_run_honours_the_tap_selector() {
|
||||
let bp_path = cwd.join("measurement.json");
|
||||
std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint");
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=last"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=last"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -104,7 +104,7 @@ fn run_refuses_a_hand_crafted_envelope_with_a_bad_root_name_before_any_trace_wri
|
||||
std::fs::write(&bp_path, &bad).expect("write bad-root-name blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=record"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=record"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
|
||||
@@ -38,7 +38,7 @@ fn assert_clean_refusal(bp_json: &str, tag: &str, what: &str) {
|
||||
std::fs::write(&bp_path, bp_json).expect("write blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
|
||||
@@ -53,7 +53,7 @@ fn single_run_persists_a_declared_tap_series() {
|
||||
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -100,7 +100,7 @@ fn tap_free_run_writes_no_trace_store() {
|
||||
let cwd = temp_cwd("tap-free-writes-nothing");
|
||||
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", &bp])
|
||||
.args(["exec", &bp])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -152,7 +152,7 @@ fn single_run_refuses_a_duplicate_tap_name_before_persisting_anything() {
|
||||
std::fs::write(&bp_path, duplicate_tap_blueprint_json()).expect("write dup-tap blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -178,7 +178,7 @@ fn an_unwritable_store_root_exits_through_the_tap_trace_register() {
|
||||
std::fs::write(cwd.join("runs"), b"not a directory").expect("occupy runs as a file");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -210,7 +210,7 @@ fn a_read_only_run_dir_surfaces_terminally_through_the_tap_trace_register() {
|
||||
.expect("make run dir read-only");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -239,7 +239,7 @@ fn run_tap_selector_persists_a_fold_summary_row() {
|
||||
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -276,7 +276,7 @@ fn run_explicit_record_plan_matches_the_record_all_default() {
|
||||
.expect("write two-tap blueprint");
|
||||
}
|
||||
let run = |cwd: &std::path::Path, extra: &[&str]| {
|
||||
let mut args = vec!["run", "two_tap.json"];
|
||||
let mut args = vec!["exec", "two_tap.json"];
|
||||
args.extend_from_slice(extra);
|
||||
let out = Command::new(BIN)
|
||||
.args(&args)
|
||||
@@ -309,7 +309,7 @@ fn run_tap_selector_refuses_an_unknown_label_with_the_roster() {
|
||||
let bp_path = cwd.join("tapped_r_sma.json");
|
||||
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=medain"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=medain"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -334,7 +334,7 @@ fn run_tap_selector_refuses_an_undeclared_tap() {
|
||||
let bp_path = cwd.join("two_tap.json");
|
||||
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "nope=mean"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -359,7 +359,7 @@ fn run_tap_selector_notes_unbound_declared_taps_on_stderr() {
|
||||
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -384,7 +384,7 @@ fn run_with_no_tap_flag_emits_no_unbound_note() {
|
||||
std::fs::write(&bp_path, two_tap_blueprint_json()).expect("write two-tap blueprint");
|
||||
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", bp_path.to_str().expect("utf-8 path")])
|
||||
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura run");
|
||||
@@ -407,7 +407,7 @@ fn run_tap_selector_refuses_malformed_and_duplicate_pairs() {
|
||||
vec!["--tap", "fast_tapmean"],
|
||||
vec!["--tap", "fast_tap=mean", "--tap", "fast_tap=last"],
|
||||
] {
|
||||
let mut full = vec!["run", bp_path.to_str().expect("utf-8 path")];
|
||||
let mut full = vec!["exec", bp_path.to_str().expect("utf-8 path")];
|
||||
full.extend(args);
|
||||
let out = Command::new(BIN)
|
||||
.args(&full)
|
||||
|
||||
@@ -574,7 +574,8 @@ pub enum ReopenError {
|
||||
|
||||
/// One entry of the aggregated BOUND param surface (#246): the path-qualified
|
||||
/// twin of [`ParamSpec`] carrying the bound value — the default a sweep axis
|
||||
/// may override, and what `--list-axes` renders as `default=`.
|
||||
/// may override, and what `aura graph introspect --params` renders as
|
||||
/// `default=`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BoundSpec {
|
||||
pub name: String,
|
||||
@@ -3376,7 +3377,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// #246: bound_param_space mirrors param_space's path qualification and
|
||||
/// carries the bound value (the default the CLI renders in --list-axes).
|
||||
/// carries the bound value (the default the CLI renders in
|
||||
/// `aura graph introspect --params`).
|
||||
#[test]
|
||||
fn bound_param_space_is_path_qualified_with_values() {
|
||||
use aura_strategy::Bias;
|
||||
|
||||
@@ -31,6 +31,11 @@ pub enum FamilyKind {
|
||||
Sweep,
|
||||
MonteCarlo,
|
||||
WalkForward,
|
||||
/// Dead machinery since the #319 sugar retirement (C18): no producer
|
||||
/// mints a merged cross-instrument family anymore — the campaign-run
|
||||
/// record's `generalizations[]` carries the data. The variant stays for
|
||||
/// store read-back of historically minted families (never retroactively
|
||||
/// invalidated, C29).
|
||||
CrossInstrument,
|
||||
}
|
||||
|
||||
|
||||
@@ -1309,14 +1309,31 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
||||
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
||||
slots.push(open("seed", "required, non-negative integer"));
|
||||
}
|
||||
if v.get("presentation").is_none() {
|
||||
slots.push(open(
|
||||
match v.get("presentation") {
|
||||
None => slots.push(open(
|
||||
"presentation",
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
));
|
||||
)),
|
||||
Some(p) => {
|
||||
if p.get("persist_taps").and_then(|x| x.as_array()).is_none() {
|
||||
slots.push(open(
|
||||
"presentation.persist_taps",
|
||||
format!(
|
||||
"required, list of: {}",
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
));
|
||||
}
|
||||
if p.get("emit").and_then(|x| x.as_array()).is_none() {
|
||||
slots.push(open(
|
||||
"presentation.emit",
|
||||
format!("required, list of: {}", emit_vocabulary().join(" | ")),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(slots)
|
||||
}
|
||||
@@ -2294,6 +2311,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #319 fieldtest bug 1: a present-but-empty `presentation: {}` must drill
|
||||
/// down into its two required sub-slots exactly as a present-but-empty
|
||||
/// `data: {}` drills into `instruments`/`windows` — the walker must not
|
||||
/// stop at "the section exists" without checking what's inside it.
|
||||
#[test]
|
||||
fn presentation_drills_into_an_empty_section_like_data_does() {
|
||||
let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft",
|
||||
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
|
||||
"strategies": [ { "ref": { "content_id": "9f3a" },
|
||||
"axes": { "slow": { "kind": "I64", "values": [10] } } } ],
|
||||
"process": { "ref": { "content_id": "4e2d" } },
|
||||
"seed": 1,
|
||||
"presentation": {} }"#;
|
||||
let slots = open_slots_campaign(draft).unwrap();
|
||||
assert!(
|
||||
!slots.iter().any(|s| s.path == "presentation"),
|
||||
"an empty-but-present presentation is not the same open slot as an absent one: {slots:?}"
|
||||
);
|
||||
assert!(
|
||||
slots.iter().any(|s| s.path == "presentation.persist_taps"
|
||||
&& s.hint == "required, list of: equity | exposure | r_equity | net_r_equity"),
|
||||
"presentation.persist_taps must be named as its own open slot: {slots:?}"
|
||||
);
|
||||
assert!(
|
||||
slots.iter().any(|s| s.path == "presentation.emit"
|
||||
&& s.hint == "required, list of: family_table | selection_report"),
|
||||
"presentation.emit must be named as its own open slot: {slots:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #256 fork B: the fieldless enumerate-only stage round-trips through
|
||||
/// the schema-strict parser, and any slot on it is refused (its slot
|
||||
/// list is empty, so the generic unknown-slot check covers every key).
|
||||
|
||||
@@ -64,7 +64,8 @@ pub fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool {
|
||||
/// instead of refusing it.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum AxisIntake {
|
||||
/// A legal RAW name (`--list-axes`'s own namespace, #328) — accept as-is.
|
||||
/// A legal RAW name (`aura graph introspect --params`'s own namespace,
|
||||
/// #328) — accept as-is.
|
||||
Raw,
|
||||
/// A retired WRAPPED name; the carried string is its translated RAW
|
||||
/// candidate — never applied silently, only ever surfaced in a refusal.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Coverage reporting: the single interior-gap-month walk, shared by a
|
||||
//! campaign cell's coverage annotation
|
||||
//! (`DefaultMemberRunner::window_coverage`) and `aura data coverage`'s
|
||||
//! archive-wide report — two independent walk implementations before this
|
||||
//! module existed (#295 dedup).
|
||||
//! The interior-gap-month walk behind a campaign cell's coverage annotation
|
||||
//! (`DefaultMemberRunner::window_coverage`) — originally shared with `aura
|
||||
//! data coverage`'s archive-wide report (#295 dedup: two independent walk
|
||||
//! implementations before this module existed) before that verb retired in
|
||||
//! favor of per-cell fault isolation (#272, #273).
|
||||
|
||||
/// `"YYYY-MM"` rendering of a `(year, month)` pair.
|
||||
pub fn fmt_year_month((y, m): (u16, u8)) -> String {
|
||||
@@ -18,11 +18,11 @@ pub fn next_year_month((y, m): (u16, u8)) -> (u16, u8) {
|
||||
/// fall inside `window_ms` (Unix-ms) — one `"YYYY-MM"` entry per missing
|
||||
/// month, never a collapsed range: the registry's `CellCoverage::gap_months`
|
||||
/// is a flat list an aggregate counts directly. `months` is assumed sorted
|
||||
/// ([`aura_ingest::list_m1_months`]'s own contract). The single gap-walk
|
||||
/// implementation (#295): both `DefaultMemberRunner::window_coverage`
|
||||
/// (a swept cell's own window) and [`data_coverage_report`] (the full
|
||||
/// archive span, via [`FULL_ARCHIVE_WINDOW_MS`]) call this one walk instead
|
||||
/// of maintaining independent copies.
|
||||
/// ([`aura_ingest::list_m1_months`]'s own contract). `DefaultMemberRunner::
|
||||
/// window_coverage` (a swept cell's own window) is the sole caller since
|
||||
/// `aura data coverage`'s archive-wide report retired (#273); the walk stays
|
||||
/// its own function because it was single-sourced with that verb before then
|
||||
/// (#295).
|
||||
pub fn interior_gap_months(months: &[(u16, u8)], window_ms: (i64, i64)) -> Vec<String> {
|
||||
let from_ym = data_server::records::unix_ms_to_year_month(window_ms.0);
|
||||
let to_ym = data_server::records::unix_ms_to_year_month(window_ms.1);
|
||||
@@ -40,68 +40,6 @@ pub fn interior_gap_months(months: &[(u16, u8)], window_ms: (i64, i64)) -> Vec<S
|
||||
out
|
||||
}
|
||||
|
||||
/// A Unix-ms window that decodes (via `unix_ms_to_year_month`) to year/month
|
||||
/// bounds — 0001-01 and 9999-01, exact epoch-ms for those UTC instants, not
|
||||
/// an approximation — comfortably outside any realistic archive month, so an
|
||||
/// unbounded, archive-wide [`interior_gap_months`] walk never ms-filters out
|
||||
/// a real gap. Safely inside `chrono`'s valid calendar range, so the
|
||||
/// conversion inside `interior_gap_months` never panics.
|
||||
pub const FULL_ARCHIVE_WINDOW_MS: (i64, i64) = (-62_135_596_800_000, 253_370_764_800_000);
|
||||
|
||||
/// `aura data coverage <SYMBOL>`'s pure report body (#264): render `symbol`'s
|
||||
/// coverage report from its sorted `(year, month)` file list — one `span:`
|
||||
/// line framing the first/last present month, then either a `no gaps` line
|
||||
/// or one `missing: YYYY-MM..YYYY-MM` line per interior contiguous gap (a
|
||||
/// ten-month hole is one line, not ten). `Err` exactly when `months` is
|
||||
/// empty — no archive file exists for `symbol` at all (the caller's "unknown
|
||||
/// symbol" refusal, stderr + exit 1). The gap detection itself is
|
||||
/// [`interior_gap_months`] over the full archive span
|
||||
/// ([`FULL_ARCHIVE_WINDOW_MS`]); collapsing its flat per-month list into
|
||||
/// contiguous ranges is presentation-only regrouping, done here since it
|
||||
/// stays byte-identical to the pre-dedup report.
|
||||
pub fn data_coverage_report(symbol: &str, months: &[(u16, u8)]) -> Result<Vec<String>, String> {
|
||||
let Some(&first) = months.first() else {
|
||||
return Err(format!("no archive files found for symbol \"{symbol}\""));
|
||||
};
|
||||
let last = *months.last().expect("non-empty checked above");
|
||||
let mut lines =
|
||||
vec![format!("{symbol} span: {}..{}", fmt_year_month(first), fmt_year_month(last))];
|
||||
let gap_months = interior_gap_months(months, FULL_ARCHIVE_WINDOW_MS);
|
||||
if gap_months.is_empty() {
|
||||
lines.push(format!("{symbol} no gaps"));
|
||||
} else {
|
||||
for (from, to) in collapse_contiguous_year_months(&gap_months) {
|
||||
lines.push(format!("{symbol} missing: {from}..{to}"));
|
||||
}
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
/// Collapse [`interior_gap_months`]' flat, ascending `"YYYY-MM"` list into
|
||||
/// contiguous inclusive `(from, to)` ranges — a ten-month hole collapses to
|
||||
/// one pair, not ten. Presentation-only regrouping: the gap WALK itself
|
||||
/// already ran in `interior_gap_months`.
|
||||
fn collapse_contiguous_year_months(months: &[String]) -> Vec<(String, String)> {
|
||||
let mut out: Vec<(String, String)> = Vec::new();
|
||||
for m in months {
|
||||
let ym = parse_year_month(m);
|
||||
match out.last_mut() {
|
||||
Some((_, to)) if next_year_month(parse_year_month(to)) == ym => {
|
||||
*to = m.clone();
|
||||
}
|
||||
_ => out.push((m.clone(), m.clone())),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Inverse of [`fmt_year_month`] — parses back the `"YYYY-MM"` shape
|
||||
/// [`interior_gap_months`] always emits.
|
||||
fn parse_year_month(s: &str) -> (u16, u8) {
|
||||
let (y, m) = s.split_once('-').expect("interior_gap_months emits YYYY-MM");
|
||||
(y.parse().expect("YYYY digits"), m.parse().expect("MM digits"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -125,37 +63,4 @@ mod tests {
|
||||
"both interior gap months must be named, in order"
|
||||
);
|
||||
}
|
||||
|
||||
/// An unknown symbol (no archive files at all — an empty month list)
|
||||
/// refuses rather than reporting a bogus empty-span coverage (#264): the
|
||||
/// caller eprintln's the message and exits 1.
|
||||
#[test]
|
||||
fn data_coverage_report_refuses_an_unknown_symbol() {
|
||||
let err = data_coverage_report("GHOST", &[]).unwrap_err();
|
||||
assert!(err.contains("GHOST"), "names the unknown symbol: {err}");
|
||||
}
|
||||
|
||||
/// A symbol with a fully contiguous file index reports its span plus an
|
||||
/// explicit `no gaps` line — never a bare span with no gap-status line at
|
||||
/// all, which would leave "no gaps" indistinguishable from "gaps not yet
|
||||
/// checked" (#264).
|
||||
#[test]
|
||||
fn data_coverage_report_of_a_gapless_symbol_is_span_plus_no_gaps() {
|
||||
let months = [(2024, 1), (2024, 2), (2024, 3)];
|
||||
let lines = data_coverage_report("SYMA", &months).expect("known symbol");
|
||||
assert_eq!(lines, vec!["SYMA span: 2024-01..2024-03", "SYMA no gaps"]);
|
||||
}
|
||||
|
||||
/// The Copper failure shape itself: one interior gap collapses to a single
|
||||
/// `missing: YYYY-MM..YYYY-MM` line naming the whole contiguous hole, not
|
||||
/// one line per missing month (#264).
|
||||
#[test]
|
||||
fn data_coverage_report_collapses_an_interior_gap_to_one_range_line() {
|
||||
let months = [(2024, 1), (2024, 2), (2024, 5), (2024, 6)];
|
||||
let lines = data_coverage_report("GAPSYM", &months).expect("known symbol");
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec!["GAPSYM span: 2024-01..2024-06", "GAPSYM missing: 2024-03..2024-04"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,23 @@
|
||||
//! Family assembly and orchestration (#295).
|
||||
//!
|
||||
//! The blueprint sweep / walk-forward / Monte-Carlo family builders — pure
|
||||
//! member-run recipes driven off a [`DataSource`] (the shared synthetic/real
|
||||
//! data provider every family builder threads, plus the `--select` objective
|
||||
//! [`Selection`] walk-forward resolves its winner under) — together with the
|
||||
//! winner-selection (`select_winner`) and axis-grid validation
|
||||
//! (`validate_axis_grid`) machinery they share. No `aura` binary is needed to
|
||||
//! drive a family: the shell (`aura-cli`) wraps these builders with
|
||||
//! persistence + stdout rendering (`run_blueprint_sweep`,
|
||||
//! `run_blueprint_walkforward`, `run_blueprint_mc`), which stay in the shell,
|
||||
//! along with the `--select`/`--real` argv grammar (`parse_select`,
|
||||
//! `select_rule_of`) and the `topology_hash`/`content_id` naming primitive
|
||||
//! (inlined here instead, mirroring `member::run_signal_r`'s own copy — the
|
||||
//! CLI shell still needs its own for call sites outside any family builder).
|
||||
//! The blueprint sweep / walk-forward / Monte-Carlo family builders that used
|
||||
//! to live here are retired (#319 — the research-verb quintet's own
|
||||
//! machinery; the campaign document path, `aura-campaign::exec`, is the
|
||||
//! surviving family-execution surface, with its own independent axis/window/
|
||||
//! winner-selection machinery). What remains is the shared [`DataSource`]
|
||||
//! data provider — the synthetic/real price-source abstraction
|
||||
//! `aura_runner::reproduce` drives its own bit-identical re-derivation
|
||||
//! over — and its synthetic stream primitives (`showcase_prices`,
|
||||
//! `walkforward_prices`, `walkforward_window_source`, `synthetic_walk_sources`).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use aura_campaign::{catch_member_panic, member_fault_prose, MemberFault};
|
||||
use aura_composites::StopRule;
|
||||
use aura_core::{Cell, ParamSpec, Scalar, Timestamp};
|
||||
use aura_engine::{
|
||||
blueprint_from_json, walk_forward, window_of, BindError, Composite, FamilySelection,
|
||||
RollMode, RunManifest, SyntheticSpec, VecSource, WindowBounds, WindowRoller,
|
||||
};
|
||||
use aura_registry::{
|
||||
optimize_deflated, optimize_plateau, PlateauMode, DEFLATION_BLOCK_LEN, DEFLATION_N_RESAMPLES,
|
||||
};
|
||||
use aura_backtest::{
|
||||
monte_carlo, McFamily, RunMetrics, RunReport, SweepFamily, SweepPoint, WalkForwardResult,
|
||||
WindowRun, WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS,
|
||||
};
|
||||
use aura_core::{Scalar, Timestamp};
|
||||
use aura_engine::{window_of, SyntheticSpec, VecSource};
|
||||
use aura_backtest::{WF_REAL_IS_NS, WF_REAL_OOS_NS, WF_REAL_STEP_NS};
|
||||
|
||||
use crate::binding::ResolvedBinding;
|
||||
use crate::member::{
|
||||
blueprint_axis_probe, blueprint_axis_probe_reopened, no_real_data, override_paths,
|
||||
pip_or_refuse, probe_window, reopen_all, run_blueprint_member, wrapped_bound_overrides_of,
|
||||
SYNTHETIC_PIP_SIZE,
|
||||
};
|
||||
use crate::member::{no_real_data, pip_or_refuse, probe_window, SYNTHETIC_PIP_SIZE};
|
||||
use crate::project::Env;
|
||||
use crate::translate::{R_SMA_STOP_K, R_SMA_STOP_LENGTH};
|
||||
|
||||
/// The demo default stop regime, shared by every family builder in this module.
|
||||
const DEFAULT_STOP: StopRule = StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K };
|
||||
|
||||
/// The in-sample winner-selection objective for walk-forward's per-window IS
|
||||
/// refit (cycle 0077). `Argmax` is the bare-best pick deflated for trials
|
||||
/// (#144, the default); `Plateau` argmaxes the neighbourhood-smoothed surface
|
||||
/// instead (opt-in via `--select`). The CLI shell's `--select` grammar
|
||||
/// (`parse_select`) and campaign-rule mapping (`select_rule_of`) build this
|
||||
/// value; they stay in the shell since only this type crosses the boundary.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Selection {
|
||||
Argmax,
|
||||
Plateau(PlateauMode),
|
||||
}
|
||||
|
||||
/// A warm-up-adequate synthetic stream (~18 ticks rising, falling, then rising
|
||||
/// again) used as `DataSource::Synthetic`'s full-window stream for the built-in
|
||||
@@ -103,19 +66,18 @@ pub enum DataChoice {
|
||||
Real { symbol: String, from_ms: Option<i64>, to_ms: Option<i64> },
|
||||
}
|
||||
|
||||
/// The source provider threaded into the family builders: synthetic built-in
|
||||
/// streams, or real M1 close bars from the data-server archive. Replaces the
|
||||
/// hardcoded `VecSource` so a member's source, pip, window, and roller sizes come
|
||||
/// from one place (Fork B/D/F).
|
||||
/// The source provider `aura_runner::reproduce` threads through its
|
||||
/// re-derivation: synthetic built-in streams, or real M1 close bars from the
|
||||
/// data-server archive. Replaces the hardcoded `VecSource` so a member's
|
||||
/// source, pip, window, and roller sizes come from one place (Fork B/D/F).
|
||||
///
|
||||
/// `Synthetic` denotes a *consumer-dependent* built-in stream, not one fixed
|
||||
/// series: the full-window consumers (`full_window` / `run_sources`, used by
|
||||
/// sweep / MC) draw the 18-bar `showcase_prices()`, while the windowed consumers
|
||||
/// (`windowed_sources` / `wf_window_sizes`, used by walk-forward) draw the 60-bar
|
||||
/// series: the full-window consumers (`full_window` / `run_sources`) draw the
|
||||
/// 18-bar `showcase_prices()`, while the windowed consumers
|
||||
/// (`windowed_sources` / `wf_window_sizes`) draw the 60-bar
|
||||
/// `walkforward_prices()` so the `(24,12,12)`-bar roller fits its span. The two
|
||||
/// faces never reach one consumer (a family is either full-window or windowed), so
|
||||
/// the split is invisible per call site but real across the type — read both
|
||||
/// family builders to see it whole.
|
||||
/// faces never reach one consumer (full-window vs. windowed reproduction), so
|
||||
/// the split is invisible per call site but real across the type.
|
||||
pub enum DataSource {
|
||||
Synthetic,
|
||||
Real {
|
||||
@@ -223,666 +185,14 @@ impl DataSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// #260: the r-sma sugar/MC paths below run with either no cost model (empty
|
||||
/// `cost` slice) or CLI-flag cost specs (scalar-only — `cost_specs_from_params`
|
||||
/// only ever wraps `CostValue::Scalar`), so an instrument-keyed map can never
|
||||
/// originate on these paths and the instrument context is genuinely inert.
|
||||
/// Named once so every such call site states its intent by reference instead
|
||||
/// of repeating the justifying comment.
|
||||
const NO_INSTRUMENT_CONTEXT: &str = "";
|
||||
|
||||
/// The winner-selection objective for walk-forward's per-window IS refit — the
|
||||
/// deflation-aware SQN variant (#144 default). Named once so `select_winner`
|
||||
/// and `blueprint_walkforward_family` (the only two family-builder-side call
|
||||
/// sites) cannot drift apart on the token; the CLI shell's campaign-sugar
|
||||
/// bridge keeps its own copy of this token (main.rs `WINNER_SELECTION_METRIC`)
|
||||
/// since it is not itself a family builder.
|
||||
const WINNER_SELECTION_METRIC: &str = "sqn_normalized";
|
||||
|
||||
/// Fixed RNG seed for the trials-deflation reality-check bootstrap in walk-forward
|
||||
/// winner selection. Recorded on each winner's manifest (so `overfit_probability`
|
||||
/// is reproducible by re-run); a CLI flag for it is a deferred refinement. The
|
||||
/// resample count and block length are the shared `aura_registry::DEFLATION_*`.
|
||||
const DEFLATION_SEED: u64 = 0xDEF1_A7ED;
|
||||
|
||||
/// Renders a [`BindError`] as one-line prose in `member::override_paths`' sibling
|
||||
/// register above (#247) — never the raw Rust `Debug` struct name
|
||||
/// (`KindMismatch { .. }` / `MissingKnob("..")`), the two variants the sweep
|
||||
/// terminal actually raises past `override_paths`' own pre-flight (which
|
||||
/// already rejects an unresolvable axis name as prose before either call
|
||||
/// site below ever reaches the terminal). `UnknownKnob` already carries a
|
||||
/// fully-prosed message string (wrapped from `override_paths`' own
|
||||
/// `Result<_, String>`) — unwrapped here rather than Debug-framed (#269), so
|
||||
/// the walkforward path's rejection reaches stderr as bare prose too.
|
||||
///
|
||||
/// #328 render-seam fix: `BindError::MissingKnob`/`KindMismatch` carry the
|
||||
/// WRAPPED knob name (the terminal's own bind-time frame — untouched by this
|
||||
/// cycle, per the minuted deviation on acceptance-criterion 2). No
|
||||
/// user-facing surface may print a wrapped axis name, so this renderer strips
|
||||
/// the one leading node segment (`wrapped_to_raw_axis`, the same convention
|
||||
/// `axes.rs` defines) before the name ever reaches prose — the fix lives at
|
||||
/// the RENDER seam only, not in the error type or the resolution machinery.
|
||||
pub fn render_bind_error(e: &BindError) -> String {
|
||||
match e {
|
||||
BindError::MissingKnob(name) => {
|
||||
let name = crate::axes::wrapped_to_raw_axis(name);
|
||||
format!(
|
||||
"axis {name}: an open param with no axis and no bound default — \
|
||||
bind it with `--axis {name}=<value>` — see `aura sweep <bp> --list-axes`"
|
||||
)
|
||||
}
|
||||
BindError::KindMismatch { knob, expected, got } => {
|
||||
let knob = crate::axes::wrapped_to_raw_axis(knob);
|
||||
format!(
|
||||
"axis {knob}: expected {expected:?}, supplied {got:?} — \
|
||||
see `aura sweep <bp> --list-axes`"
|
||||
)
|
||||
}
|
||||
BindError::UnknownKnob(msg) => msg.clone(),
|
||||
BindError::DuplicateBinding(name) => format!(
|
||||
"axis {name}: bound twice — each param takes exactly one axis — \
|
||||
see `aura sweep <bp> --list-axes`"
|
||||
),
|
||||
BindError::EmptyAxis(name) => format!(
|
||||
"axis {name}: supplies no values — give at least one, e.g. `--axis {name}=2,4`"
|
||||
),
|
||||
BindError::EmptyRange(name) => format!(
|
||||
"axis {name}: the named range is empty — give it at least one value"
|
||||
),
|
||||
// A blueprint defect, not an axis usage error: the point passed name
|
||||
// resolution but its bootstrap failed. Prose frame with the compile
|
||||
// detail explicitly labelled as internal — per-variant prose for
|
||||
// CompileError belongs to the graph-build surface, not this boundary.
|
||||
BindError::Compile(e) => format!(
|
||||
"the resolved axis point failed to bootstrap — the blueprint is \
|
||||
defective at this point, re-validate it with `aura graph build` \
|
||||
(internal detail: {e:?})"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the in-sample winner under the chosen selection objective. `Argmax`
|
||||
/// defers to the trials-deflation pick (#144). `Plateau` argmaxes the smoothed grid
|
||||
/// surface — it needs the grid lattice, so a sweep with no lattice (a future random
|
||||
/// walk-forward producer) is refused rather than silently argmaxed. The metric is
|
||||
/// always known at the call sites, so a metric error is unreachable (`expect`); the
|
||||
/// only fallible outcome is the plateau-without-lattice refusal, returned as
|
||||
/// `Err(message)` for the caller to print and exit 2.
|
||||
pub fn select_winner(
|
||||
family: &SweepFamily, metric: &str, select: Selection, lattice: Option<&[usize]>,
|
||||
) -> Result<(SweepPoint, FamilySelection), String> {
|
||||
match select {
|
||||
Selection::Argmax => Ok(optimize_deflated(
|
||||
family, metric, DEFLATION_N_RESAMPLES, DEFLATION_BLOCK_LEN, DEFLATION_SEED,
|
||||
).expect("walk-forward metrics are known")),
|
||||
Selection::Plateau(mode) => match lattice {
|
||||
Some(lens) => Ok(optimize_plateau(family, lens, metric, mode)
|
||||
.expect("walk-forward metrics are known")),
|
||||
None => Err(
|
||||
"--select plateau requires a grid sweep; a random sweep has no parameter lattice"
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal
|
||||
/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all
|
||||
/// run BEFORE this closure is invoked per grid point, so its body never influences the
|
||||
/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to
|
||||
/// satisfy the terminal, at O(1) cost per point instead of a full member run. Reused
|
||||
/// (#278) as the discarded slot value for a member run [`catch_member_panic`]
|
||||
/// contains: the caller always resolves the captured fault and exits before this
|
||||
/// value is ever rendered or persisted, so its content is equally irrelevant there.
|
||||
fn axis_grid_probe_report() -> RunReport {
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
commit: String::new(),
|
||||
params: Vec::new(),
|
||||
defaults: Vec::new(),
|
||||
window: (Timestamp(0), Timestamp(0)),
|
||||
seed: 0,
|
||||
broker: "wf-axis-preflight-placeholder".to_string(),
|
||||
selection: None,
|
||||
instrument: None,
|
||||
topology_hash: None,
|
||||
project: None,
|
||||
},
|
||||
metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None },
|
||||
}
|
||||
}
|
||||
|
||||
/// The LOWEST enumeration index's captured member-panic message, found by
|
||||
/// walking `family.points` — guaranteed by `assemble_sweep`'s own
|
||||
/// `collect()` to be in grid-enumeration order regardless of which worker
|
||||
/// finished first (C1) — for the first point whose params match a captured
|
||||
/// fault. Thread-order-independent by construction (mirrors the lowest-
|
||||
/// index-fault convention `aura-campaign::exec::run_members` establishes for
|
||||
/// the identical #272 seam), without needing a separate numeric-index
|
||||
/// side-channel: the returned family already carries the points in order.
|
||||
fn lowest_point_fault(family: &SweepFamily, faults: Mutex<Vec<(Vec<Cell>, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
family
|
||||
.points
|
||||
.iter()
|
||||
.find_map(|pt| captured.iter().find(|(p, _)| *p == pt.params).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// The LOWEST seed's captured member-panic message (#278), found by walking
|
||||
/// `family.draws` — `monte_carlo`'s own contract guarantees seed-**input**
|
||||
/// order regardless of thread completion (C1) — for the first draw whose
|
||||
/// seed matches a captured fault. Thread-order-independent by construction,
|
||||
/// mirroring [`lowest_point_fault`]'s and `lowest_window_fault`'s identical
|
||||
/// convention one level over (per-point sweep / per-window walk-forward).
|
||||
fn lowest_seed_fault(family: &McFamily, faults: Mutex<Vec<(u64, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
family
|
||||
.draws
|
||||
.iter()
|
||||
.find_map(|d| captured.iter().find(|(s, _)| *s == d.seed).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// Render + exit(3) on a contained member panic (#278: the deliberate
|
||||
/// failed-cells exit, C14, never the raw 101 an uncontained panic would
|
||||
/// yield) — reuses `aura_campaign::member_fault_prose`'s established
|
||||
/// "a member panicked: …" wording rather than re-wording it, and the
|
||||
/// `aura: warning: ` class marker (#278 decision) the CLI's own `diag`
|
||||
/// macro would emit, hand-written here since that macro is presentation-
|
||||
/// crate-private (#295 boundary) and this fires from `aura-runner`.
|
||||
fn exit_on_member_panic(msg: &str) -> ! {
|
||||
eprintln!("aura: warning: {}", member_fault_prose(&MemberFault::Panic(msg.to_string())));
|
||||
std::process::exit(3);
|
||||
}
|
||||
|
||||
/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any
|
||||
/// member (#253). Reuses the SAME strict, erroring axis-name check
|
||||
/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two
|
||||
/// paths cannot drift to differently-worded rejections) to derive the #246 override
|
||||
/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks
|
||||
/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for
|
||||
/// the run closure — no data access, no sim engine tick. Axis resolution is
|
||||
/// window-agnostic, so the caller derives or passes no window at all.
|
||||
fn validate_axis_grid(
|
||||
doc: &str, axes: &[(String, Vec<Scalar>)], raw_space: &[ParamSpec], probe_signal: &Composite,
|
||||
env: &Env,
|
||||
) -> Result<(), BindError> {
|
||||
let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?;
|
||||
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
|
||||
let space = probe.param_space();
|
||||
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
|
||||
// preflight ahead of this call, mirroring the plain sweep verb) — translate
|
||||
// each onto the ONE wrapped `space` slot it suffix-matches
|
||||
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds its
|
||||
// own `SweepBinder`) before this terminal's exact-name resolution.
|
||||
let mut iter = axes.iter();
|
||||
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
|
||||
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
|
||||
}
|
||||
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
|
||||
}
|
||||
|
||||
/// Sweep a serialized signal `doc` over user-named param-space axes. Structurally it
|
||||
/// keeps the shape of the retired `r_sma_sweep_family` demo builder (#159), with three
|
||||
/// deviations. (1) The signal source is
|
||||
/// `wrap_r(blueprint_from_json(doc))` — a loaded blueprint, not the Rust-built
|
||||
/// r-sma graph. (2) The signal is RE-loaded from `doc` per member (a `Composite` is
|
||||
/// `!Clone`, so the throwaway param-space probe and each grid point each reload). (3)
|
||||
/// The axes are taken verbatim BY NAME (not the four suffix-resolved r-sma knobs):
|
||||
/// each `(name, vals)` is fed straight to the `SweepBinder`, so an unknown name or a
|
||||
/// kind mismatch surfaces as the sweep terminal's [`BindError`], rendered to a message
|
||||
/// string — a named error, never a panic. An axis naming a bound param re-opens it
|
||||
/// (#246: bound value = default); an axis matching neither space is refused by
|
||||
/// `override_paths` before any run. Every member manifest carries the shared
|
||||
/// `topology_hash` of the loaded signal; reduce-mode fold, identical to the retired
|
||||
/// mirror's default (no-trace) arm.
|
||||
pub fn blueprint_sweep_family(
|
||||
doc: &str,
|
||||
axes: &[(String, Vec<Scalar>)],
|
||||
data: &DataSource,
|
||||
env: &Env,
|
||||
) -> Result<SweepFamily, String> {
|
||||
// Identity + binding read the AUTHORED doc, raw (no override re-open):
|
||||
// topology and the resolved role plan are properties of the document, not
|
||||
// of any one sweep's axis choices.
|
||||
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||
// topology_hash's own two-line body, inlined (mirrors member::run_signal_r):
|
||||
// `content_id_of` over the canonical (#164) blueprint JSON — the CLI shell's
|
||||
// `topology_hash` helper is the same primitive, kept single-sourced at
|
||||
// `aura_research`.
|
||||
let topo = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
|
||||
);
|
||||
// Strict binding resolution (name defaults — the verb path carries no
|
||||
// campaign overrides): the family's open plan and wrap plan in one value.
|
||||
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
|
||||
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
|
||||
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
|
||||
}
|
||||
let pip = data.pip_size();
|
||||
let window = data.full_window(env);
|
||||
// The un-reopened wrapped OPEN space (#246): derives the override set (which
|
||||
// named axes re-open a bound param) before the real, reopened probe is built —
|
||||
// probe and per-member reloads must re-open identically so points resolve
|
||||
// against one space. A name matching neither space is the error here.
|
||||
let raw_space = blueprint_axis_probe(doc, env).param_space();
|
||||
let overrides = override_paths(axes, &raw_space, &probe_signal)?;
|
||||
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
|
||||
let space = probe.param_space();
|
||||
// The doc is parse-validated at the dispatch boundary (with file-path context),
|
||||
// so every reload here is infallible: the builder has a single error contract —
|
||||
// the `BindError` returned by the sweep terminal — and no hidden process exit.
|
||||
// Member reloads re-open the SAME override set derived above, so every member
|
||||
// resolves its axes against the identical (reopened) param space the probe used.
|
||||
let reload = |d: &str| {
|
||||
reopen_all(
|
||||
blueprint_from_json(d, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
|
||||
&overrides,
|
||||
)
|
||||
};
|
||||
// seed the named axes: `axes` names are RAW (#328 — refused at intake before
|
||||
// this fn ever runs), so each is translated to the ONE wrapped `space` slot
|
||||
// it suffix-matches (`raw_matches_wrapped`, the same convention
|
||||
// `override_paths` just resolved the override set through) before feeding
|
||||
// `Composite::axis`/`SweepBinder::axis`, which still key by the exact
|
||||
// wrapped `param_space()` name. `resolve_axes` name- and kind-checks the
|
||||
// translated axes at the sweep terminal, so an UnknownKnob / KindMismatch
|
||||
// is returned, not panicked (translation cannot itself introduce one: every
|
||||
// incoming name was already validated raw-or-bound at the intake / override
|
||||
// preflight above).
|
||||
let wrapped_name_of = |raw: &str| -> String {
|
||||
space
|
||||
.iter()
|
||||
.find(|p| crate::axes::raw_matches_wrapped(raw, &p.name))
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_else(|| raw.to_string())
|
||||
};
|
||||
let mut iter = axes.iter();
|
||||
let (first_name, first_vals) = iter.next().expect("a blueprint sweep declares >= 1 axis");
|
||||
let mut binder = probe.axis(&wrapped_name_of(first_name), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(&wrapped_name_of(n), vals.clone());
|
||||
}
|
||||
// manifest.params records RAW names (#328): a name-only reshaping of `space`
|
||||
// (order/kind untouched) fed to `run_blueprint_member`, which only zips it
|
||||
// against `point` BY POSITION (`zip_params`) — never re-resolves by name —
|
||||
// so renaming here is purely the manifest's own namespace, with no effect
|
||||
// on member resolution.
|
||||
let manifest_space: Vec<ParamSpec> = space
|
||||
.iter()
|
||||
.map(|p| ParamSpec { name: crate::axes::wrapped_to_raw_axis(&p.name).to_string(), kind: p.kind })
|
||||
.collect();
|
||||
// #278: `run_blueprint_member` runs bare here (no #272 fault boundary of its
|
||||
// own), so a member-compile panic (e.g. an `Sma::new` length assert) would
|
||||
// otherwise unwind straight through this sweep to an uncaught exit 101 —
|
||||
// contained the same way the real/campaign path contains it, via
|
||||
// `catch_member_panic` + a per-point capture, resolved to the lowest
|
||||
// enumeration index's message after the sweep joins (thread-order-
|
||||
// independent, C1).
|
||||
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||
let family = binder
|
||||
.sweep(|point| {
|
||||
// fresh per-member graph (Composite is !Clone, reload per member) run through
|
||||
// the shared reduce-mode member path — the same fn reproduction re-runs.
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reload(doc), point, &manifest_space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
})
|
||||
// render the sweep terminal's BindError to prose (#247), the fn's String error
|
||||
// contract — never the raw Debug struct.
|
||||
.map_err(|e| render_bind_error(&e))?;
|
||||
if let Some(msg) = lowest_point_fault(&family, faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
Ok(family)
|
||||
}
|
||||
|
||||
/// Sweep the LOADED blueprint over the user `--axis` grid on an in-sample window
|
||||
/// `[from,to]` — the windowed, lattice-carrying twin of
|
||||
/// `blueprint_sweep_family`. `sweep_with_lattice` gives the grid lattice `--select
|
||||
/// plateau` needs. An unknown/kind-mismatched axis surfaces as `BindError` at the
|
||||
/// sweep terminal (no panic, no hidden exit) for the caller to render. An axis
|
||||
/// naming a bound param re-opens it (#246: bound value = default, same
|
||||
/// `override_paths`/`reopen_all` recipe as `blueprint_sweep_family` — this is
|
||||
/// its walk-forward in-sample twin); an axis matching neither space is refused
|
||||
/// (wrapped as `BindError::UnknownKnob`, the honest replacement for the retired
|
||||
/// "fully bound; nothing to sweep" refusal) before any member runs.
|
||||
///
|
||||
/// The returned `Option<String>` (#278) is this fn's OWN resolved member-panic
|
||||
/// capture: this fn's nested sweep runs inside `blueprint_walkforward_family`'s
|
||||
/// per-window parallel closure, so a contained member panic here must NOT
|
||||
/// `eprintln!`+`exit` on the spot (that would race across windows, the exact
|
||||
/// `#177` duplicated-rejection class the dispatch-boundary axis pre-flight
|
||||
/// above already avoids) — captured internally, resolved to the lowest-index
|
||||
/// message via [`lowest_point_fault`] before returning, for the caller to push
|
||||
/// onto its own per-window fault list only after every window has joined.
|
||||
pub fn blueprint_sweep_over(
|
||||
doc: &str, axes: &[(String, Vec<Scalar>)], from: Timestamp, to: Timestamp, data: &DataSource,
|
||||
env: &Env, binding: &ResolvedBinding,
|
||||
) -> Result<(SweepFamily, Vec<usize>, Option<String>), BindError> {
|
||||
let reload = |d: &str| {
|
||||
blueprint_from_json(d, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||
};
|
||||
let pip = data.pip_size();
|
||||
let probe_signal = reload(doc);
|
||||
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
|
||||
let topo = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
|
||||
);
|
||||
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
|
||||
// load: derives the override set (which named axes re-open a bound param) before
|
||||
// the reopened probe is built — probe and per-member reloads must re-open
|
||||
// identically so points resolve against one space, exactly like
|
||||
// `blueprint_sweep_family`.
|
||||
let raw_space = blueprint_axis_probe(doc, env).param_space();
|
||||
let overrides = override_paths(axes, &raw_space, &probe_signal).map_err(BindError::UnknownKnob)?;
|
||||
let probe = blueprint_axis_probe_reopened(doc, env, &overrides);
|
||||
let space = probe.param_space();
|
||||
// #328: `axes` names are RAW (refused at the walkforward dispatch-boundary
|
||||
// preflight ahead of the caller, mirroring the plain sweep verb) —
|
||||
// translate each onto the ONE wrapped `space` slot it suffix-matches
|
||||
// (`wrapped_name_of`, the same convention `blueprint_sweep_family` feeds
|
||||
// its own `SweepBinder`) before this terminal's exact-name resolution.
|
||||
let mut iter = axes.iter();
|
||||
let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis");
|
||||
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
|
||||
for (n, vals) in iter {
|
||||
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
|
||||
}
|
||||
let faults: Mutex<Vec<(Vec<Cell>, String)>> = Mutex::new(Vec::new());
|
||||
let (family, lattice) = binder.sweep_with_lattice(|point| {
|
||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||
let window = window_of(&sources).expect("non-empty in-sample window");
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reopen_all(reload(doc), &overrides), point, &space, sources, window, 0, pip, &topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((point.to_vec(), msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
})?;
|
||||
let fault = lowest_point_fault(&family, faults);
|
||||
Ok((family, lattice, fault))
|
||||
}
|
||||
|
||||
/// Run the winner params over an out-of-sample window `[from,to]` on the loaded
|
||||
/// blueprint. The reduce-mode member
|
||||
/// (`run_blueprint_member`) retains R-metrics, not a raw pip curve, so the stitching
|
||||
/// segment is empty (an empty segment leaves the stitched curve unbroken). `overrides`
|
||||
/// (#246) is the SAME family-wide set `blueprint_walkforward_family` derived once and
|
||||
/// resolved `space`/`params` against — the OOS reload must re-open it too, or a
|
||||
/// bound-param axis's winner point (kind-checked against the REOPENED space) fails
|
||||
/// `bootstrap_with_cells`'s arity check against this still-closed reload.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn run_oos_blueprint(
|
||||
doc: &str, params: &[Cell], space: &[ParamSpec], from: Timestamp, to: Timestamp,
|
||||
topo: &str, data: &DataSource, env: &Env, binding: &ResolvedBinding,
|
||||
overrides: &[String],
|
||||
) -> (Vec<(Timestamp, f64)>, RunReport) {
|
||||
let reload = reopen_all(
|
||||
blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible"),
|
||||
overrides,
|
||||
);
|
||||
let pip = data.pip_size();
|
||||
let sources = data.windowed_sources(from, to, env, &binding.columns());
|
||||
let window = window_of(&sources).expect("non-empty out-of-sample window");
|
||||
let report = run_blueprint_member(reload, params, space, sources, window, 0, pip, topo, env, DEFAULT_STOP, binding, &[], NO_INSTRUMENT_CONTEXT);
|
||||
(Vec::new(), report)
|
||||
}
|
||||
|
||||
/// A discarded placeholder [`WindowRun`] for a window whose IS refit or OOS
|
||||
/// run was contained after a member panic (#278): `chosen_params` matches
|
||||
/// `space`'s arity with inert zero cells (the `assemble_walk_forward` arity
|
||||
/// invariant), `oos_equity` is empty (an empty segment leaves the stitched
|
||||
/// curve unbroken, same convention as a genuinely equity-less window), and
|
||||
/// the report reuses [`axis_grid_probe_report`]'s zero-cost placeholder.
|
||||
/// Never rendered: the caller always resolves the captured window fault and
|
||||
/// exits before touching a faulted window's run.
|
||||
fn placeholder_window_run(space: &[ParamSpec]) -> WindowRun {
|
||||
WindowRun {
|
||||
chosen_params: space.iter().map(|_| Cell::from_i64(0)).collect(),
|
||||
oos_equity: Vec::new(),
|
||||
oos_report: axis_grid_probe_report(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The LOWEST roll-order window's captured member-panic message (#278),
|
||||
/// found by walking `result.windows` — guaranteed by `assemble_walk_forward`
|
||||
/// to be in roll order regardless of thread completion (C1) — for the first
|
||||
/// window whose bounds match a captured fault. Thread-order-independent by
|
||||
/// construction, mirroring [`lowest_point_fault`]'s identical convention one
|
||||
/// level down (per-point within one window's IS sweep) and
|
||||
/// `aura-campaign::exec`'s own lowest-index walk-forward fault attribution.
|
||||
fn lowest_window_fault(result: &WalkForwardResult, faults: Mutex<Vec<(WindowBounds, String)>>) -> Option<String> {
|
||||
let captured = faults.into_inner().expect("fault capture lock");
|
||||
result
|
||||
.windows
|
||||
.iter()
|
||||
.find_map(|w| captured.iter().find(|(b, _)| *b == w.bounds).map(|(_, m)| m.clone()))
|
||||
}
|
||||
|
||||
/// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the
|
||||
/// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the
|
||||
/// winner OOS, reusing the generic `walk_forward` driver + `select_winner`;
|
||||
/// only the per-window sweep/OOS source the loaded blueprint. In-closure errors
|
||||
/// (a bad `--axis`) `exit(2)` with the sweep terminal's message.
|
||||
pub fn blueprint_walkforward_family(
|
||||
doc: &str, axes: &[(String, Vec<Scalar>)], data: &DataSource, select: Selection,
|
||||
env: &Env,
|
||||
) -> WalkForwardResult {
|
||||
let span = data.wf_full_span(env);
|
||||
let (is_len, oos_len, step) = data.wf_window_sizes();
|
||||
let roller = match WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("aura: walk-forward window too short for one IS+OOS span: {e:?}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
let probe_signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
|
||||
let topo = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
|
||||
);
|
||||
// The un-reopened wrapped OPEN space (#246), against a raw probe + raw strategy
|
||||
// load: derives the override set ONCE for the whole family — every per-window
|
||||
// sweep AND the OOS reload re-open the SAME set, mirroring
|
||||
// `blueprint_sweep_family`/`blueprint_sweep_over`. The SILENT variant
|
||||
// (`wrapped_bound_overrides_of`, not the validating `override_paths`): an axis
|
||||
// matching neither space is simply not an override here — the strict check + its
|
||||
// established error message stay single-sourced in `blueprint_sweep_over`'s own
|
||||
// pre-flight call below, so this derivation cannot double-validate with a
|
||||
// differently-worded rejection.
|
||||
let axis_names: Vec<String> = axes.iter().map(|(n, _)| n.clone()).collect();
|
||||
let raw_space = blueprint_axis_probe(doc, env).param_space();
|
||||
let overrides = wrapped_bound_overrides_of(&axis_names, &raw_space, &probe_signal);
|
||||
let space = blueprint_axis_probe_reopened(doc, env, &overrides).param_space();
|
||||
// Strict binding resolution, once per family; refusal is the established
|
||||
// `aura: ` + exit-1 register (the roller's usage refusals stay exit 2).
|
||||
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())
|
||||
.unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
if matches!(data, DataSource::Synthetic) && !binding.close_only() {
|
||||
eprintln!("aura: {}", crate::binding::synthetic_refusal(probe_signal.name(), &binding));
|
||||
std::process::exit(1);
|
||||
}
|
||||
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
|
||||
// (which resolves its axes a single time before any member runs). `walk_forward` fans
|
||||
// the per-window closure out across the windows in parallel, so a `BindError` raised
|
||||
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
|
||||
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic
|
||||
// (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without
|
||||
// running a single member — no IS window (or a second roller) is needed here at all.
|
||||
if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) {
|
||||
eprintln!("aura: {}", render_bind_error(&e));
|
||||
std::process::exit(2);
|
||||
}
|
||||
// #278: `blueprint_sweep_over`'s inner sweep and `run_oos_blueprint`'s single
|
||||
// member run both drive `run_blueprint_member` bare, so either could
|
||||
// otherwise unwind an uncaught member-compile panic straight through this
|
||||
// window closure. Both are contained (`blueprint_sweep_over` resolves its
|
||||
// own capture and returns it; `run_oos_blueprint` via `catch_member_panic`
|
||||
// directly) and captured PER WINDOW here rather than printed on the spot —
|
||||
// this closure runs across windows in parallel (`walk_forward`), so an
|
||||
// inline `eprintln!`+`exit` would race the same way the axis pre-flight
|
||||
// above was written to avoid (#177). The lowest roll-order window's fault
|
||||
// is resolved once, after every window has joined.
|
||||
let window_faults: Mutex<Vec<(WindowBounds, String)>> = Mutex::new(Vec::new());
|
||||
let result = walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||
let (is_family, lattice, is_fault) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data, env, &binding)
|
||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||
if let Some(msg) = is_fault {
|
||||
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||
return placeholder_window_run(&space);
|
||||
}
|
||||
let (best, selection) = match select_winner(&is_family, WINNER_SELECTION_METRIC, select, Some(&lattice)) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||
};
|
||||
match catch_member_panic(|| {
|
||||
run_oos_blueprint(doc, &best.params, &space, w.oos.0, w.oos.1, &topo, data, env, &binding, &overrides)
|
||||
}) {
|
||||
Ok((oos_equity, mut oos_report)) => {
|
||||
oos_report.manifest.selection = Some(selection);
|
||||
WindowRun { chosen_params: best.params, oos_equity, oos_report }
|
||||
}
|
||||
Err(msg) => {
|
||||
window_faults.lock().expect("fault capture lock").push((w, msg));
|
||||
placeholder_window_run(&space)
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(msg) = lowest_window_fault(&result, window_faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// A fresh seeded synthetic price walk for one Monte-Carlo draw — `blueprint_mc_family`'s
|
||||
/// pattern (a distinct realization per seed). A FIXED `SyntheticSpec` shared by the
|
||||
/// `aura mc <blueprint.json>` persist path AND the reproduce MonteCarlo branch, so the
|
||||
/// seed->walk reconstruction is bit-exact (C1). Length 60 comfortably warms the loaded
|
||||
/// r-sma graph (SMA slow=4 + the len-3 vol stop) so draws produce differing trades.
|
||||
/// A fresh seeded synthetic price walk for one Monte-Carlo draw (a distinct
|
||||
/// realization per seed). A FIXED `SyntheticSpec` shared by the reproduce
|
||||
/// MonteCarlo branch (`aura_runner::reproduce`, its only remaining
|
||||
/// production caller — #319 retired the family builder that used to mint
|
||||
/// such a family), so the seed->walk reconstruction is bit-exact (C1).
|
||||
/// Length 60 comfortably warms the loaded r-sma graph (SMA slow=4 + the
|
||||
/// len-3 vol stop) so draws produce differing trades.
|
||||
pub fn synthetic_walk_sources(seed: u64) -> Vec<Box<dyn aura_engine::Source>> {
|
||||
let spec = SyntheticSpec { start: 1.0, len: 60, step: 1 };
|
||||
vec![Box::new(spec.source(seed))]
|
||||
}
|
||||
|
||||
/// Build a Monte-Carlo family from a loaded CLOSED signal blueprint: run the fixed
|
||||
/// blueprint across `n_seeds` seeds, each seed drawing a distinct synthetic walk. The
|
||||
/// blueprint must be CLOSED (empty wrapped `param_space`) — MC binds no axis, so a free
|
||||
/// knob has no binder; an OPEN blueprint yields a named `Err` (exit-free like the sibling
|
||||
/// [`blueprint_sweep_family`]: the IO wrapper `run_blueprint_mc` renders it to stderr +
|
||||
/// exit 2) before any run, pre-empting the `compile_with_params` arity panic. Each draw
|
||||
/// runs the shared reduce-mode member path (`run_blueprint_member`, the same fn reproduce
|
||||
/// re-runs), so reproduction is bit-identical (C1); every member carries the shared
|
||||
/// `topology_hash`.
|
||||
pub fn blueprint_mc_family(
|
||||
doc: &str, n_seeds: u64, data: &DataSource, env: &Env,
|
||||
) -> Result<McFamily, String> {
|
||||
let reload = |d: &str| {
|
||||
blueprint_from_json(d, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible")
|
||||
};
|
||||
let probe_signal = reload(doc);
|
||||
// topology_hash's own two-line body, inlined (see `blueprint_sweep_family`).
|
||||
let topo = aura_research::content_id_of(
|
||||
&aura_engine::blueprint_to_json(&probe_signal).expect("a buildable signal serializes"),
|
||||
);
|
||||
// Strict binding resolution (name defaults — mc's synthetic family binds
|
||||
// no campaign overrides); the exit-free Err contract of this builder.
|
||||
let binding = crate::binding::resolve_binding(probe_signal.name(), probe_signal.input_roles(), &BTreeMap::new())?;
|
||||
if !binding.close_only() {
|
||||
// MC draws ALWAYS run the seeded synthetic close walk (real-data mc
|
||||
// routes through the campaign sugar and never reaches this builder).
|
||||
return Err(crate::binding::synthetic_refusal(probe_signal.name(), &binding));
|
||||
}
|
||||
let pip = data.pip_size();
|
||||
// probe the wrapped param_space (the same probe the sweep resolves against);
|
||||
// MC needs it empty. `blueprint_axis_probe` is the single source of that wrap.
|
||||
let space = blueprint_axis_probe(doc, env).param_space();
|
||||
if !space.is_empty() {
|
||||
// Exit-free like blueprint_sweep_family: the builder's single error contract is this
|
||||
// returned message (no hidden process exit), so the rejection is unit-testable; the IO
|
||||
// wrapper run_blueprint_mc renders it to stderr + exit 2 at the boundary.
|
||||
return Err(format!(
|
||||
"mc requires a closed blueprint (no free parameters); {} free knob(s) — \
|
||||
bind them or use `aura sweep --axis`",
|
||||
space.len()
|
||||
));
|
||||
}
|
||||
// Closed blueprint -> an empty base point (as `aura run <blueprint.json>`); the MC
|
||||
// draws vary the SEED, not a tuning param (C12 axis 4). Delegate the disjoint C1 draws
|
||||
// to the shared `monte_carlo` helper — it runs them in parallel across sims (invariant 1),
|
||||
// deterministic in seed-input order. Each draw
|
||||
// re-runs the shared reduce-mode member path over its own seeded synthetic walk.
|
||||
let seeds: Vec<u64> = (1..=n_seeds).collect();
|
||||
let base_point: Vec<Scalar> = Vec::new();
|
||||
// #278: `run_blueprint_member` ran bare here, the one family builder without
|
||||
// the #272 fault boundary its sweep/walk-forward siblings gained in 51096a3
|
||||
// — a member-compile panic (e.g. an `Sma::new` length assert) would otherwise
|
||||
// unwind straight through `monte_carlo`'s `run_indexed` to an uncaught exit
|
||||
// 101. Contained the same way: `catch_member_panic` + a per-seed capture,
|
||||
// resolved to the LOWEST seed's message after the join (thread-order-
|
||||
// independent, C1) via `lowest_seed_fault`.
|
||||
let faults: Mutex<Vec<(u64, String)>> = Mutex::new(Vec::new());
|
||||
let family = monte_carlo(&base_point, &seeds, |seed, _base| {
|
||||
let sources = synthetic_walk_sources(seed);
|
||||
let window = window_of(&sources).expect("non-empty synthetic walk");
|
||||
match catch_member_panic(|| {
|
||||
run_blueprint_member(reload(doc), &[], &space, sources, window, seed, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
|
||||
}) {
|
||||
Ok(report) => report,
|
||||
Err(msg) => {
|
||||
faults.lock().expect("fault capture lock").push((seed, msg));
|
||||
axis_grid_probe_report()
|
||||
}
|
||||
}
|
||||
});
|
||||
// The captured fault must win BEFORE the vacuous-mc guard below: a faulted
|
||||
// draw's placeholder report is metrics-identical across every faulted seed,
|
||||
// so a run with >= 2 faulted seeds (or one faulted + one real draw sharing
|
||||
// its placeholder's zero metrics) could otherwise trip the vacuous refusal
|
||||
// instead of surfacing the real member fault.
|
||||
if let Some(msg) = lowest_seed_fault(&family, faults) {
|
||||
exit_on_member_panic(&msg);
|
||||
}
|
||||
// Silent-vacuous MC guard (refuse-don't-guess, C10): with >= 2 seeds, if every draw's
|
||||
// metrics are bit-identical to the first, no seed reached a distinguishable realization —
|
||||
// the strategy never warmed over the fixed synthetic walk (e.g. a lookback as deep as the
|
||||
// walk is long), so the "distribution" is a single point masquerading as a family: a wrong
|
||||
// result with no error. Compare `metrics`, not the whole `RunReport` — the manifest's
|
||||
// `seed` differs per draw by construction, so a whole-report compare could never detect the
|
||||
// collapse; the metrics are the realization the seed is meant to move. A single-draw MC
|
||||
// (n == 1) is trivially "all identical" and is NOT this cross-seed condition, so it passes.
|
||||
if family.draws.len() >= 2
|
||||
&& family
|
||||
.draws
|
||||
.iter()
|
||||
.all(|d| d.report.metrics == family.draws[0].report.metrics)
|
||||
{
|
||||
return Err(
|
||||
"mc is vacuous: every seed produced an identical result — the strategy never warmed \
|
||||
over the synthetic walk, so no seed reached a distinguishable realization; use a \
|
||||
shallower-lookback blueprint or a longer walk"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(family)
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ use std::sync::{mpsc, Arc, LazyLock};
|
||||
use aura_composites::{cost_graph, risk_executor, StopRule};
|
||||
use aura_core::{zip_params, Cell, Firing, ParamSpec, PrimitiveBuilder, Scalar, ScalarKind, Timestamp};
|
||||
use aura_engine::{
|
||||
blueprint_from_json, window_of, BlueprintNode, Composite, GraphBuilder, Harness, RunManifest,
|
||||
VecSource,
|
||||
blueprint_from_json, window_of, BlueprintNode, CompileError, Composite, GraphBuilder, Harness,
|
||||
RunManifest, VecSource,
|
||||
};
|
||||
use aura_backtest::{
|
||||
summarize, summarize_r, RunMetrics, RunReport, SimBroker, PM_FIELD_NAMES, PM_RECORD_KINDS,
|
||||
@@ -471,6 +471,49 @@ pub fn resolve_run_data(
|
||||
}
|
||||
}
|
||||
|
||||
/// The variant-name string for a `ScalarKind`, matching the wire/CLI spelling
|
||||
/// used across the codebase (`F64`/`I64`/`Bool`/`Timestamp`).
|
||||
fn scalar_kind_name(kind: ScalarKind) -> &'static str {
|
||||
match kind {
|
||||
ScalarKind::F64 => "F64",
|
||||
ScalarKind::I64 => "I64",
|
||||
ScalarKind::Bool => "Bool",
|
||||
ScalarKind::Timestamp => "Timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
/// Bug 3 (#319 fieldtest cycle 2): an `--override` value whose lexed kind
|
||||
/// does not match the param's declared kind (e.g. the bare literal `2` for
|
||||
/// an F64 param) reaches this compile boundary as a raw
|
||||
/// `CompileError::ParamKindMismatch { slot, expected, got }` — `slot` is a
|
||||
/// flat param-space index, meaningless to a caller. `names`/`params` are the
|
||||
/// SAME order `compile_with_params` was called with (built from `signal`
|
||||
/// before it was consumed by `wrap_r`, `C11`: composites inline, preserving
|
||||
/// order), so `slot` indexes both. This names the param path and both kinds,
|
||||
/// mirroring the campaign leg's own kind-fault prose
|
||||
/// (`ref_fault_prose`'s `AxisKindMismatch` arm, `research_docs.rs`). Every
|
||||
/// other `CompileError` variant keeps the existing Debug fallback — out of
|
||||
/// this bug's scope, and `names`/`params` only reconstruct the one variant
|
||||
/// that carries an injected value.
|
||||
fn compile_error_prose(e: &CompileError, names: &[String], params: &[Scalar]) -> String {
|
||||
let CompileError::ParamKindMismatch { slot, expected, got } = e else {
|
||||
return format!("this blueprint does not compile to a runnable harness: {e:?}");
|
||||
};
|
||||
let path = names.get(*slot).map(String::as_str).unwrap_or("<unknown>");
|
||||
let example = match (expected, params.get(*slot)) {
|
||||
(ScalarKind::F64, Some(Scalar::I64(v))) => format!("{v}.0"),
|
||||
(ScalarKind::F64, _) => "a decimal literal, e.g. 2.0".to_string(),
|
||||
(ScalarKind::I64, _) => "a bare integer literal, e.g. 2".to_string(),
|
||||
(ScalarKind::Bool, _) => "true or false".to_string(),
|
||||
(ScalarKind::Timestamp, _) => "an integer epoch-ns literal".to_string(),
|
||||
};
|
||||
format!(
|
||||
"--override {path}: expects {}, got {} — write {example}",
|
||||
scalar_kind_name(*expected),
|
||||
scalar_kind_name(*got)
|
||||
)
|
||||
}
|
||||
|
||||
/// Run a signal blueprint through the R scaffolding: hash the signal,
|
||||
/// wrap it (broker + equity/exposure/R sinks), compile with `params`, bootstrap,
|
||||
/// run over `data`, and build the RunReport (manifest carries topology_hash).
|
||||
@@ -514,7 +557,7 @@ pub fn run_signal_r(
|
||||
let (sources, window, pip_size) = resolve_run_data(&data, env, &binding);
|
||||
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding, None);
|
||||
let mut flat = wrapped.compile_with_params(params).unwrap_or_else(|e| {
|
||||
eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}");
|
||||
eprintln!("aura: {}", compile_error_prose(&e, &names, params));
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Bind each declared tap per the plan's subscription, before bootstrap
|
||||
@@ -659,8 +702,9 @@ pub fn run_blueprint_member(
|
||||
/// against: the loaded signal wrapped in the r-sma scaffolding (stop bound,
|
||||
/// reduce, no cost), taps discarded. `param_space()` on it is the axis
|
||||
/// namespace `--axis` binds; `.axis()` consumes it to seed a sweep. Single
|
||||
/// source for the sweep terminal, the MC closed-check, AND `--list-axes`, so
|
||||
/// the listed names track the swept names by construction (incl. across #159's
|
||||
/// source for the sweep terminal, the MC closed-check, AND
|
||||
/// `aura graph introspect --params`, so the listed names track the swept
|
||||
/// names by construction (incl. across #159's
|
||||
/// harness retirement). The reload is infallible under the SAME
|
||||
/// dispatch-boundary contract the callers already rely on: the doc is
|
||||
/// `blueprint_from_json`-validated at the `["sweep", ..]` / `["mc", ..]`
|
||||
@@ -673,7 +717,8 @@ pub fn blueprint_axis_probe(doc: &str, env: &Env) -> Composite {
|
||||
/// The axis probe with a #246 override set re-opened on the strategy BEFORE
|
||||
/// wrapping — probe and per-member reloads must re-open identically so points
|
||||
/// resolve against one space. The empty-set form is byte-equal to the old
|
||||
/// probe (mc/run closed-checks and the open `--list-axes` lines read that).
|
||||
/// probe (mc/run closed-checks and the open `aura graph introspect --params`
|
||||
/// lines read that).
|
||||
pub fn blueprint_axis_probe_reopened(doc: &str, env: &Env, overrides: &[String]) -> Composite {
|
||||
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible");
|
||||
@@ -767,7 +812,7 @@ pub fn override_paths(
|
||||
None => {
|
||||
return Err(format!(
|
||||
"axis {name}: names no param of this blueprint (open or bound) — \
|
||||
see `aura sweep <bp> --list-axes`"
|
||||
see `aura graph introspect --params <bp>`"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ fn the_shell_defines_no_domain_modules_and_no_lib_target() {
|
||||
"render.rs",
|
||||
"research_docs.rs",
|
||||
"scaffold.rs",
|
||||
"verb_sugar.rs",
|
||||
],
|
||||
"C28 (#295): the shell holds argv/dispatch, argv->document translation, \
|
||||
and presentation only — a new module needs a library home"
|
||||
|
||||
+45
-37
@@ -10,7 +10,7 @@ JSON artifact kinds you author headlessly along that arc:
|
||||
through `aura process validate|introspect|register|show`;
|
||||
3. a **campaign document** (role 6b) — experiment intent (instruments ×
|
||||
windows × strategy × axes × process) through `aura campaign
|
||||
validate|introspect|register|show|run|runs`.
|
||||
validate|introspect|register|runs|show`, executed by `aura exec` (#319).
|
||||
|
||||
Each section below is a worked, verified example — every command shown was
|
||||
run against this repo and the output is transcribed, not invented. The *why*
|
||||
@@ -39,9 +39,11 @@ the same shape every node in `aura-std` already follows.
|
||||
scaffolds a **data-only** project (`docs/project-layout.md`, "A project
|
||||
repo (two tiers)") — a strategy over the std vocabulary is a
|
||||
blueprint/campaign document, not Rust: the scaffold ships **one** closed
|
||||
`signal.json` starter that serves both verbs — `aura run` uses its bound
|
||||
values as-is, `aura sweep --axis fast.length=2,4,8` overrides them
|
||||
(bound = default, not fixed); `--list-axes` lists the open knobs and the
|
||||
`signal.json` starter that serves both legs of `exec` (#319) — `aura exec
|
||||
signal.json` uses its bound values as-is, `aura exec signal.json --override
|
||||
fast.length=8` reopens one for that run, and a campaign document's
|
||||
`strategies[].axes` reopens one across a family (bound = default, not
|
||||
fixed); `aura graph introspect --params` lists the open knobs and the
|
||||
bound defaults alike, so every override target is discoverable.
|
||||
- **A project-specific *native* node type** — the moment §0's three-part
|
||||
pattern is actually needed — lives in an attached **node crate**:
|
||||
@@ -240,7 +242,7 @@ are dotted `<identifier>.<port>` on both sides of a wire.
|
||||
| `feed` | `{"op":"feed","role":<str>,"into":[<port>, …]}` | fan a previously-declared role into one or more interior input slots, all-or-nothing (a failing target leaves none of the batch wired). |
|
||||
| `connect` | `{"op":"connect","from":<port>,"to":<port>}` | wire one interior output field to one interior input slot. A `connect` that would close a dataflow cycle is rejected immediately — the only legal feedback path is an explicit delay/state node (domain invariant 5). |
|
||||
| `expose` | `{"op":"expose","from":<port>,"as":<str>}` | promote an interior output field to a boundary output under the alias `as` — a real *alias* (a terminal boundary name, not a referenceable identifier like `add`'s `name`). Together with `tap`, one of the two ops whose `as` key is a terminal name rather than an identifier. |
|
||||
| `tap` | `{"op":"tap","from":<port>,"as":<str>}` | declare a **measurement tap** on an interior output field under the name `as` — the output-side twin of `expose` (a recorded observation point, not a boundary output; a `Composite.taps` entry, C27). A single `aura run` constructs a recorder at each declared tap and persists its per-cycle series as a `ColumnarTrace`; a sweep leaves it inert. Tap names are their own namespace and must be unique (a second `tap` under one name refuses: `duplicate tap name`). |
|
||||
| `tap` | `{"op":"tap","from":<port>,"as":<str>}` | declare a **measurement tap** on an interior output field under the name `as` — the output-side twin of `expose` (a recorded observation point, not a boundary output; a `Composite.taps` entry, C27). A single `aura exec` constructs a recorder at each declared tap and persists its per-cycle series as a `ColumnarTrace`; a campaign member run leaves it inert. Tap names are their own namespace and must be unique (a second `tap` under one name refuses: `duplicate tap name`). |
|
||||
| `gang` | `{"op":"gang","as":"channel_length","into":["channel_hi.length","channel_lo.length"]}` | Fuse two or more sibling params into ONE public knob: the member addresses leave the sweepable param space and `as` replaces them; the bound or swept value fans out to every member at bootstrap. Members must share one scalar kind and stay open (un-bound). |
|
||||
|
||||
Omitting `name` is fine for a single strategy, but it has three live
|
||||
@@ -283,9 +285,10 @@ corpus's own example, verified below; byte-identical to the on-disk
|
||||
|
||||
A `bind` in an `add` op pins that param to a value and removes it from the
|
||||
**open** param space (`--params`): a bound param is a **default** (#246) — a
|
||||
run uses it as-is, while any campaign axis or `aura sweep --axis` naming it
|
||||
re-opens it for that family and binds it per cell. `--list-axes` lists it
|
||||
after the open knobs as `<name>:<KIND> default=<value>`. `bind` is for a
|
||||
plain `exec` uses it as-is, while a campaign axis naming it re-opens it for
|
||||
that family (bound per cell) and `exec`'s own `--override NODE.PARAM=VALUE`
|
||||
(#319) re-opens it for that one execution. `graph introspect --params` lists
|
||||
it after the open knobs as `<name>:<KIND> default=<value>`. `bind` is for a
|
||||
value the strategy carries by default; leave a param unbound, as all three
|
||||
are here, to make binding it mandatory for every sweep.
|
||||
|
||||
@@ -293,7 +296,7 @@ A param therefore has three states: **open** (an axis every sweep MUST bind),
|
||||
**bound** (a default any axis MAY override, #246), and **ganged** (open, but
|
||||
fused with its siblings under ONE public knob declared by a `gang` op; the
|
||||
member addresses are unbindable and only the gang's own single-segment name —
|
||||
e.g. `channel_length` — appears in `--list-axes`).
|
||||
e.g. `channel_length` — appears in `graph introspect --params`).
|
||||
|
||||
### Worked example: declaring a measurement tap
|
||||
|
||||
@@ -322,10 +325,12 @@ bias scaling) under the name `spread`:
|
||||
|
||||
The built blueprint carries a `taps` array naming the resolved wire
|
||||
(`{"name":"spread","from":{"node":<sub's index>,"field":0}}` — the name is
|
||||
addressed, the index is resolved *for* you). A single `aura run <blueprint>`
|
||||
addressed, the index is resolved *for* you). A single `aura exec <blueprint>`
|
||||
then constructs a recorder at that point and persists the `spread` series as a
|
||||
`ColumnarTrace` in the run's trace store, chartable by its name. A tap is inert
|
||||
in a sweep (no per-cell recorder) — it is a single-run observation surface.
|
||||
in a campaign member run (no per-cell recorder) — it is a single-run
|
||||
observation surface (or, per campaign, what a nominee's `persist_taps`
|
||||
re-run records, §3).
|
||||
(`sub.value` is tapped here purely to illustrate; any interior output field is
|
||||
a legal tap source, and a producer needs no other consumer to be tapped.)
|
||||
|
||||
@@ -422,29 +427,32 @@ slow.length:I64
|
||||
bias.scale:F64
|
||||
```
|
||||
|
||||
These printed names are the **one** axis namespace (#328): op-script params, a
|
||||
campaign document's `strategies[].axes` keys (§3), and `aura sweep <blueprint>
|
||||
--list-axes` / `--axis` (glossary `sweep`) all speak the same raw
|
||||
`<node>.<param>` form — `--params` and `--list-axes` are line-identical (open
|
||||
params bare, bound params trailing `default=<value>`), and every discovered
|
||||
name is verbatim legal as a document axis key, bound params included (#246's
|
||||
re-open contract):
|
||||
These printed names are the **one** axis namespace (#328): op-script params
|
||||
and a campaign document's `strategies[].axes` keys (§3) speak the same raw
|
||||
`<node>.<param>` form as `graph introspect --params` prints (open params
|
||||
bare, bound params trailing `default=<value>`) — every discovered name is
|
||||
verbatim legal as a document axis key, bound params included (#246's
|
||||
re-open contract), and as the token `exec --override NODE.PARAM=VALUE` (#319)
|
||||
takes on the single-run leg:
|
||||
|
||||
```json
|
||||
"strategies": [ { "ref": { "content_id": "…" },
|
||||
"axes": { "fast.length": { "kind": "I64", "values": [3, 5, 7, 9] },
|
||||
"bias.scale": { "kind": "F64", "values": [0.25, 0.5] } } } ]
|
||||
```
|
||||
|
||||
A single execution reopens the same raw name directly, with no document at all:
|
||||
|
||||
```
|
||||
$ aura sweep smacross.json --axis fast.length=3:9:2 # synthetic: raw works
|
||||
$ aura sweep smacross.json --axis bias.scale=0.25,0.5 --real ... # real: raw works
|
||||
$ aura exec smacross.json --override fast.length=5 # single run: raw works
|
||||
```
|
||||
|
||||
The older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`,
|
||||
what pre-#328 transcripts and `--list-axes` output used to print) is retired
|
||||
from the surface; naming it refuses with a translation pointer to the raw
|
||||
candidate, on both intake seams:
|
||||
what pre-#328 transcripts and the retired `--list-axes` output used to print)
|
||||
is retired from the surface; naming it in a campaign document refuses with a
|
||||
translation pointer to the raw candidate, the one remaining intake seam:
|
||||
|
||||
```
|
||||
$ aura sweep smacross.json --axis graph.fast.length=...
|
||||
aura: axis "graph.fast.length": axis names are raw node.param paths —
|
||||
use "fast.length" (the wrapped --list-axes form was retired, #328)
|
||||
|
||||
$ aura campaign validate wrapped.json # a document quoting an old transcript
|
||||
aura: campaign references do not resolve:
|
||||
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240:
|
||||
@@ -704,7 +712,7 @@ campaign document valid (executable): pipeline shape and static guards pass
|
||||
```
|
||||
$ aura campaign register mra_3_campaign_full_v2.json
|
||||
registered campaign 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd (…/runs/campaigns/42ed…json)
|
||||
$ aura campaign run 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd
|
||||
$ aura exec 42edebd2159de708009ba21e1ed4aea2cffabc373bf761c5765f79c190b677bd
|
||||
{"family_id":"42edebd2-0-GER40-w0-s0-0","report":{...}}
|
||||
{"family_id":"42edebd2-0-GER40-w0-s0-0-r1","report":{...}}
|
||||
…
|
||||
@@ -730,19 +738,19 @@ regime's family ids and trace dirs carry the `-r1` ordinal suffix (the
|
||||
default/first regime stays unsuffixed), each cell record names its regime,
|
||||
and generalization is keyed per regime — regimes are compared, never pooled.
|
||||
|
||||
`aura campaign run` is register-then-run sugar for a `.json` file, but the
|
||||
`aura exec` (#319) is register-then-run sugar for a `.json` file, but the
|
||||
canonical address is always the content id — running a bare file the first
|
||||
time registers it implicitly. `aura campaign runs` lists stored
|
||||
realizations; `aura campaign runs <id>` dumps the bare stored record(s) (not
|
||||
the `{"campaign_run": …}` emit wrapper above). If `presentation.persist_taps`
|
||||
is non-empty, the run also persists the named taps under
|
||||
`runs/traces/<trace_name>/…`, chartable with `aura chart`. The
|
||||
`sweep`/`walkforward --trace` analog persists every member's taps as a
|
||||
family charted the same way, by the handle the run prints (members keyed
|
||||
`<cell>/<member>`) — also by the `--trace <NAME>` you chose, when that name
|
||||
uniquely names one recorded run (`aura chart <NAME>` resolves it against the
|
||||
stored campaign documents; a name reused across runs refuses rather than
|
||||
guessing which one you mean).
|
||||
is non-empty, the run also persists every member's taps under
|
||||
`runs/traces/<trace_name>/…` — the deterministic `{campaign8}-{run}` handle
|
||||
the `campaign_run` record's `trace_name` field prints, never a user-chosen
|
||||
name — chartable with `aura chart <trace_name>` (members keyed
|
||||
`<cell>/<member>`); `aura chart` also resolves a bare campaign id/name
|
||||
against the stored campaign documents when no literal trace directory
|
||||
matches, refusing rather than guessing when the name is ambiguous across
|
||||
recorded runs.
|
||||
|
||||
Exit codes: a clean run exits 0; a usage error exits 2; a refusal before any
|
||||
cell runs (invalid document, missing project, unresolvable strategy) exits 1;
|
||||
|
||||
@@ -23,3 +23,7 @@ never line-ordered. Duplicate campaign instruments are refused at both the
|
||||
validate tier and the executor's preflight: the per-cell family name embeds
|
||||
the raw instrument string, so uniqueness is what keeps concurrent appends from
|
||||
racing one name's run-index assignment.
|
||||
|
||||
**Current-state cross-command-path example (superseded by the #319 sugar
|
||||
retirement, 2026-07-25):** "e.g. a swept member's `sqn` vs the same cell
|
||||
re-run under `aura generalize`".
|
||||
|
||||
@@ -5,8 +5,9 @@ loop that reaches a unique state after each input tick. Same input (incl. seed)
|
||||
→ bit-identical run. Two backtests are fully disjoint and run concurrently
|
||||
without locking. The bit-identity is *per run*: one backtest of given (inputs,
|
||||
seed) reproduces byte-for-byte. It does **not** extend to a *derived metric*
|
||||
recomputed for the same params by two *different command paths* (e.g. a swept
|
||||
member's `sqn` vs the same cell re-run under `aura generalize`), which may differ
|
||||
recomputed for the same params by two *different command paths* (e.g. a
|
||||
campaign cell's `sqn` within its sweep stage vs the same params re-run
|
||||
through `exec`'s blueprint leg with `--override`, #319), which may differ
|
||||
by floating-point reassociation (≤1 ULP) because the two paths accumulate the
|
||||
same logical reduction in a different operation order (IEEE-754 non-associativity).
|
||||
C1 governs the determinism of a single run, not the cross-command bit-identity of
|
||||
|
||||
@@ -34,3 +34,15 @@ walk-forward families (axes 2–4 above) build their members over **one** shared
|
||||
`Arc<DataServer>` (one `FileCache`), so a window is parsed once and every member's
|
||||
`M1FieldSource` borrows the same cached `Arc<[M1Parsed]>` chunks
|
||||
(`crates/aura-ingest/src/lib.rs:316`). The single-pass *parse* cost stays tracked as #95.
|
||||
|
||||
**Current-state bound-override + identity-anchor clause (superseded by the
|
||||
#319 sugar retirement, 2026-07-25):** "so axis 1 (param-sweep) may name a
|
||||
bound param — the family boundary re-opens it on the probe and on every
|
||||
member reload, and the axis binds it per cell; `run`/`mc` still require
|
||||
every param resolved (a truly open param refuses). Identity is untouched:
|
||||
`content_id_of` (`crates/aura-research/src/lib.rs`) and `topology_hash`
|
||||
(`crates/aura-cli/src/main.rs`) read the authored document, never a
|
||||
re-opened probe, and each member manifest records its per-cell bindings
|
||||
(#246, ratified 2026-07-12; the restriction this amends — axes bind only
|
||||
open knobs — was an implementation consequence of `bind()` shrinking the
|
||||
param surface, not a recorded decision)."
|
||||
|
||||
@@ -5,12 +5,17 @@ RNG-seed) → deterministic run → metrics`. Parameters are typed, ranged, runt
|
||||
values injected at graph build — no recompile per param-set; the optimizer sees a
|
||||
generic vector of typed ranges. A **bound blueprint param is that param's
|
||||
default**: "open" means *must be bound by an axis*, "bound" means *default,
|
||||
overridable by an axis*, so axis 1 (param-sweep) may name a bound param — the
|
||||
family boundary re-opens it on the probe and on every member reload, and the axis
|
||||
binds it per cell; `run`/`mc` still require every param resolved (a truly open
|
||||
param refuses). Identity is untouched: `content_id_of`
|
||||
(`crates/aura-research/src/lib.rs`) and `topology_hash`
|
||||
(`crates/aura-cli/src/main.rs`) read the authored document, never a re-opened
|
||||
overridable by an axis*, so a campaign axis (param-sweep) may name a bound
|
||||
param — the family boundary re-opens it on the probe and on every member
|
||||
reload, and the axis binds it per cell; `exec`'s blueprint leg still requires
|
||||
every param resolved (a truly open param refuses), and its own `--override
|
||||
NODE.PARAM=VALUE` (#246 residue, #319) reopens exactly the named bound param
|
||||
for that one execution — the single-member analogue of a campaign axis.
|
||||
Identity is untouched: `content_id_of` (`crates/aura-research/src/lib.rs`) is
|
||||
the one hashing primitive, read directly off the authored document by
|
||||
`aura_runner::member`'s `run_signal_r`/`run_blueprint_member` — where the
|
||||
manifest's `topology_hash` is computed inline (#319 Task 9 retired the
|
||||
CLI-side wrapper that used to duplicate this one call) — never a re-opened
|
||||
probe, and each member manifest records its per-cell bindings (#246, ratified
|
||||
2026-07-12; the restriction this amends — axes bind only open knobs — was an
|
||||
implementation consequence of `bind()` shrinking the param surface, not a recorded
|
||||
|
||||
@@ -27,3 +27,27 @@ every hand-rolled usage line reads `Usage: aura <verb> …` (#179, cycle 0101);
|
||||
refusal diagnostics stay unprefixed (diagnostics are not usage lines). The
|
||||
machine-first help surface (JSON/manifest help, stdin op-scripts) stays on the
|
||||
#157/C21 track, not this cycle (settled as human/GNU convention compliance).
|
||||
|
||||
**Current-state "Dual grammar" section (superseded by the #319 sugar
|
||||
retirement, 2026-07-25):** "The four dual-grammar subcommands
|
||||
(run/sweep/walkforward/mc) keep both grammars under one token via an
|
||||
optional `[blueprint]` positional plus a post-parse `is_file()` dispatch; the
|
||||
execution layer is unchanged (arg-plumbing via thin `*_from` adapters). The
|
||||
machine-first help surface (JSON/manifest help, stdin op-scripts) is
|
||||
deferred to the #157 / C21 track, distinct from this human/GNU-convention
|
||||
compliance."
|
||||
|
||||
**Current-state "Single document grammar" section's first-draft routing
|
||||
sentence (superseded by the #319 sugar retirement, 2026-07-25):** "one verb,
|
||||
`exec <target>`, dispatches on `is_blueprint_file` (a first-positional
|
||||
naming an existing `.json` file selects the loaded-blueprint leg; anything
|
||||
else — a campaign file or a registered content id — the campaign leg)." This
|
||||
undersold the routing by one layer: the code always also peeked the file's
|
||||
top-level `"kind"` field (`is_campaign_document_file`) to tell a campaign
|
||||
document ending in `.json` from a blueprint file — both being ordinary
|
||||
readable `.json` files `is_blueprint_file` alone cannot distinguish — and,
|
||||
after an independent review found a malformed campaign file was silently
|
||||
misrouted to the blueprint leg's own JSON-parse error prose, gained a
|
||||
neutral not-valid-JSON refusal ahead of the leg choice (review Minor-3,
|
||||
2026-07-25's fix cycle). The current contract's "Single document grammar"
|
||||
section names this real routing.
|
||||
|
||||
@@ -59,12 +59,21 @@ caller branches on the failure class without parsing stderr.
|
||||
in `crates/aura-cli/src/main.rs`, threaded from the run registry —
|
||||
[C18](c18-registry.md)).
|
||||
|
||||
**Dual grammar.** The four dual-grammar subcommands (run/sweep/walkforward/mc)
|
||||
keep both grammars under one token via an optional `[blueprint]` positional plus
|
||||
a post-parse `is_file()` dispatch; the execution layer is unchanged (arg-plumbing
|
||||
via thin `*_from` adapters). The machine-first help surface (JSON/manifest help,
|
||||
stdin op-scripts) is deferred to the #157 / [C21](c21-world.md) track, distinct
|
||||
from this human/GNU-convention compliance.
|
||||
**Single document grammar (#319, 2026-07-25).** The five dual-grammar
|
||||
subcommands (run/sweep/walkforward/mc/generalize) that used to keep two
|
||||
grammars under one token are retired; one verb, `exec <target>`, dispatches
|
||||
on `is_blueprint_file` layered with a "kind"-field peek
|
||||
(`classify_exec_document_file`): a first-positional naming an existing
|
||||
`.json` file that parses as JSON and carries no top-level `"kind"` selects
|
||||
the loaded-blueprint leg; a file carrying `"kind"`, or a target that is not a
|
||||
readable `.json` file at all (a registered content id), selects the campaign
|
||||
leg. A file that does not parse as JSON at all refuses neutrally (exit 2)
|
||||
before either leg's own document-shape validation runs, rather than
|
||||
silently choosing a leg (review Minor-3, 2026-07-25). The execution layer
|
||||
this collapses onto is unchanged in kind (arg-plumbing via thin adapters).
|
||||
The machine-first help surface (JSON/manifest help, stdin op-scripts) is
|
||||
deferred to the #157 / [C21](c21-world.md) track, distinct from this
|
||||
human/GNU-convention compliance.
|
||||
|
||||
**Two artifact classes, two redundancy budgets** (#249, ratified 2026-07-13).
|
||||
The data layer the programmatic face emits is a public interface read raw (by
|
||||
|
||||
@@ -271,3 +271,27 @@ and both callers are untouched; maintenance is lazy-only — put-time indexing
|
||||
was rejected because it would need a roster-free doc-level identity function
|
||||
whose equivalence to the loaded-composite path no green test ratifies (decision
|
||||
log: #191 comments).
|
||||
|
||||
**Current-state `runs.jsonl`/`families.jsonl` CLI clauses (superseded by the
|
||||
#319 sugar retirement, 2026-07-25):** "No live producer writes it today —
|
||||
sweep / walk-forward / mc persist to the family store and `aura run` does not
|
||||
persist — so the flat lib API is retained but selectively live: `rank_by`
|
||||
backs `aura runs family … rank`, `optimize` backs walk-forward's in-sample
|
||||
step (`aura-campaign` and the registry's own selection helper both call it),
|
||||
while `append`/`load` remain public API with no in-tree caller (a latent
|
||||
surface for external consumers)." … "CLI: `aura runs families`, `aura runs
|
||||
family <id> [rank <metric>]`; `aura sweep`/`walkforward`/`mc` persist via
|
||||
`append_family` with an optional `--name`."
|
||||
|
||||
**Current-state "Cross-instrument generalization" paragraph (superseded by
|
||||
the #319 sugar retirement, 2026-07-25):** "`FamilyKind::CrossInstrument`:
|
||||
`aura generalize` runs one candidate across an instrument list and persists
|
||||
the M per-instrument runs via `append_family`, each member self-identifying
|
||||
through `RunManifest.instrument`. The generalization score (worst-case R
|
||||
floor + sign-agreement + per-instrument breakdown) is a **recomputable
|
||||
aggregate** over those members, not a persisted family-level record."
|
||||
|
||||
**Current-state "The campaign executor" opening sentence (superseded by the
|
||||
#319 sugar retirement, 2026-07-25):** "`aura campaign run <file|content-id>`
|
||||
executes a campaign (a file is register-then-run sugar; the content id is
|
||||
canonical): a zero-fault referential gate, then the process pipeline."
|
||||
|
||||
@@ -39,9 +39,10 @@ by aura. The experiments-&-results plane is the run registry in `aura-registry`
|
||||
|
||||
- `runs.jsonl` — the append-only flat store, one `RunReport` per line
|
||||
(`RunManifest` + `RunMetrics`), with a typed read-path (`load`) and best-first
|
||||
ranking (`rank_by`/`optimize`). No live producer writes it today — sweep /
|
||||
walk-forward / mc persist to the family store and `aura run` does not persist —
|
||||
so the flat lib API is retained but selectively live: `rank_by` backs
|
||||
ranking (`rank_by`/`optimize`). No live producer writes it today — the campaign
|
||||
executor (`exec`'s campaign leg, `aura-campaign`, #319) persists to the family
|
||||
store instead, and `exec`'s blueprint leg (single run) does not persist — so
|
||||
the flat lib API is retained but selectively live: `rank_by` backs
|
||||
`aura runs family … rank`, `optimize` backs walk-forward's in-sample step
|
||||
(`aura-campaign` and the registry's own selection helper both call it), while
|
||||
`append`/`load` remain public API with no in-tree caller (a latent surface for
|
||||
@@ -49,14 +50,16 @@ by aura. The experiments-&-results plane is the run registry in `aura-registry`
|
||||
- `families.jsonl` — the family store. A sweep / Monte-Carlo / walk-forward /
|
||||
cross-instrument run persists as a *set of related records*, each a
|
||||
`FamilyRunRecord` (a `RunReport` stamped with `family` + `run` + `kind` +
|
||||
`ordinal`; `FamilyKind ∈ {Sweep, MonteCarlo, WalkForward, CrossInstrument}`).
|
||||
`group_families` re-derives a family from the stored links (re-listable /
|
||||
rankable as a unit — C21). The user-facing `family_id = "{family}-{run}"` handle
|
||||
is **derived** from the stored `family` name plus a per-name `run` index
|
||||
(numeric max+1 — not a content hash, so re-running the same family mints a fresh
|
||||
id). CLI: `aura runs families`, `aura runs family <id> [rank <metric>]`;
|
||||
`aura sweep`/`walkforward`/`mc` persist via `append_family` with an optional
|
||||
`--name`.
|
||||
`ordinal`; `FamilyKind ∈ {Sweep, MonteCarlo, WalkForward, CrossInstrument}`,
|
||||
the last now dead — below). `group_families` re-derives a family from the
|
||||
stored links (re-listable / rankable as a unit — C21). The user-facing
|
||||
`family_id = "{family}-{run}"` handle is **derived** from the stored `family`
|
||||
name plus a per-name `run` index (numeric max+1 — not a content hash, so
|
||||
re-running the same family mints a fresh id). CLI: `aura runs families`,
|
||||
`aura runs family <id> [rank <metric>]`; the campaign executor persists per-cell
|
||||
sweep/walk-forward families via `append_family` under a deterministic,
|
||||
campaign-derived name — no user-supplied `--name` (the retired quintet's
|
||||
naming flag, #319).
|
||||
- `campaign_runs.jsonl` — one thin `CampaignRunRecord` per campaign run (below),
|
||||
over untouched family records.
|
||||
- `blueprints/<hash>.json`, `processes/`, `campaigns/` — the content-addressed
|
||||
@@ -109,12 +112,13 @@ the byte-exact `topology_hash` keeps every debug role untouched (introspection-o
|
||||
the blueprint does not use leaves the id byte-stable. `--content-id` and
|
||||
`--identity-id` are combinable.
|
||||
|
||||
**Cross-instrument generalization.** `FamilyKind::CrossInstrument`: `aura generalize`
|
||||
runs one candidate across an instrument list and persists the M per-instrument runs
|
||||
via `append_family`, each member self-identifying through `RunManifest.instrument`.
|
||||
The generalization score (worst-case R floor + sign-agreement + per-instrument
|
||||
breakdown) is a **recomputable aggregate** over those members, not a persisted
|
||||
family-level record.
|
||||
**Cross-instrument generalization (retired standalone family, #319).** The
|
||||
`FamilyKind::CrossInstrument` variant is now dead: the standalone `aura
|
||||
generalize` verb that used to run one candidate across an instrument list and
|
||||
persist the M per-instrument runs via `append_family` is retired. The
|
||||
surviving cross-instrument grading is `std::generalize`'s campaign-scope
|
||||
computation (below, "Annotators are terminal") — a **recomputable aggregate**
|
||||
over the cells' nominees, not a persisted family-level record.
|
||||
|
||||
**Research-artifact document stores.** `processes/` and `campaigns/` hold two
|
||||
document types (C25 roles 5 / 6b): the **process document** (a named
|
||||
@@ -145,7 +149,7 @@ falls in. Pinned by
|
||||
`referential_tier_accepts_a_kind_correct_axis_over_a_bound_param`
|
||||
(`aura-registry/src/lib.rs`).
|
||||
|
||||
**The campaign executor.** `aura campaign run <file|content-id>` executes a
|
||||
**The campaign executor.** `aura exec <file|content-id>` (#319) executes a
|
||||
campaign (a file is register-then-run sugar; the content id is canonical): a
|
||||
zero-fault referential gate, then the process pipeline. The executable shape is
|
||||
`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`
|
||||
|
||||
@@ -34,3 +34,16 @@ scaffolding's last data weld — `wrap_r`'s hard-wired `price`←close role and
|
||||
`M1Field::Close`-only open sites — is retired; a strategy's input roles now bind
|
||||
archive columns by name (C26). `wrap_r`'s remaining R-scaffolding retirement
|
||||
stays #159.]
|
||||
|
||||
**Current-state "Pre-C24 scaffolding retired" paragraph's family-builder
|
||||
naming (superseded by the #319 sugar retirement, 2026-07-25):** "The family
|
||||
builders are now generic and blueprint-driven: `blueprint_sweep_family`,
|
||||
`blueprint_walkforward_family`, `blueprint_mc_family` (and
|
||||
`blueprint_sweep_over`) in `crates/aura-runner/src/family.rs`." Those four
|
||||
functions are themselves retired by #319 (2026-07-25): the campaign executor
|
||||
(`aura_campaign::exec::execute`) builds every family now, driven by the
|
||||
process document's stage pipeline; `family.rs` keeps only the synthetic-
|
||||
stream helpers `aura_runner::reproduce` calls to re-derive a recorded family
|
||||
(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`,
|
||||
`synthetic_walk_sources`, `DataSource`/`DataChoice`). The current contract's
|
||||
"Pre-C24 scaffolding retired" paragraph names this real state.
|
||||
|
||||
@@ -71,13 +71,20 @@ rather than a hand-enumerated menu. Persisted experiment *intent* is the campaig
|
||||
document (C25/C18).
|
||||
|
||||
**Pre-C24 scaffolding retired.** The pre-C24 scaffolding — `HarnessKind` and the
|
||||
per-strategy `*_sweep_family` functions — is **gone**. The family builders are now
|
||||
generic and blueprint-driven: `blueprint_sweep_family`, `blueprint_walkforward_family`,
|
||||
`blueprint_mc_family` (and `blueprint_sweep_over`) in
|
||||
`crates/aura-runner/src/family.rs`. The R-evaluator scaffold `wrap_r`
|
||||
(defined in `crates/aura-runner/src/member.rs`; imported and called from
|
||||
`runner.rs`) survives; its last hard data weld — the
|
||||
hard-wired `price`←close role and the `M1Field::Close`-only open sites — is retired
|
||||
per-strategy `*_sweep_family` functions — is **gone**. The generic blueprint-driven
|
||||
family builders that first replaced it (`blueprint_sweep_family`,
|
||||
`blueprint_walkforward_family`, `blueprint_mc_family`, `blueprint_sweep_over`) are,
|
||||
in turn, retired by the #319 sugar retirement (2026-07-25): the **campaign
|
||||
executor** (`aura_campaign::exec::execute`) now builds every family, driven by
|
||||
the process document's stage pipeline (`std::sweep`/`std::grid`,
|
||||
`std::walk_forward`, `std::monte_carlo`). `crates/aura-runner/src/family.rs`
|
||||
retains only the synthetic-stream helpers serving `aura_runner::reproduce`
|
||||
(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`,
|
||||
`synthetic_walk_sources`, `DataSource`/`DataChoice`) — reproduction's own
|
||||
re-derivation path, not a family-minting path. The R-evaluator scaffold
|
||||
`wrap_r` (defined in `crates/aura-runner/src/member.rs`; imported and called
|
||||
from `runner.rs`) survives; its last hard data weld — the hard-wired
|
||||
`price`←close role and the `M1Field::Close`-only open sites — is retired
|
||||
(a strategy's input roles now bind archive columns by name, C26), and `wrap_r`'s
|
||||
remaining R-scaffolding retirement is tracked at #159.
|
||||
|
||||
|
||||
@@ -135,3 +135,36 @@ the mean shows the net/duty-cycle level. Deferred refinements: rendering the min
|
||||
envelope honestly as range bars / OHLC rather than a polyline (#112); a `--width`
|
||||
budget flag and true intra-bucket min/max ordering for the non-default continuous
|
||||
x-mode (#110).
|
||||
|
||||
**Current-state "Single run" + "Families" + "Newcomer" clauses (superseded by
|
||||
the #319 sugar retirement, 2026-07-25):** "**Single run.** `aura run
|
||||
<blueprint.json>` persists every tap the blueprint declares to the trace
|
||||
store under the run's own name, on both shapes: a `bias`-output strategy
|
||||
(`aura-runner::member::run_signal_r`, R-wrapped) and a bare measurement
|
||||
blueprint with ≥1 declared tap but no `bias`
|
||||
(`aura-runner::measure::run_measurement`). A tap-free run writes nothing to
|
||||
`runs/` and its stdout stays byte-identical. The CLI `--trace <NAME>` flag is
|
||||
**retired** on `run` (and on `mc`): it parses but is refused at dispatch
|
||||
(exit 2) — naming a trace is the family / campaign path's job, not a per-run
|
||||
flag.
|
||||
|
||||
**Families.** `aura sweep|walkforward --real <SYM> … --trace <NAME>`
|
||||
persists *each member* via the campaign path: the dissolved verb is
|
||||
translated into a content-addressed process + campaign document whose
|
||||
`presentation.persist_taps` requests the tap vocabulary, run through the one
|
||||
campaign executor; `aura-runner::runner::persist_campaign_traces` writes
|
||||
each member under a depth-2 fan-out `runs/traces/<name>/<cell>/<member>/`.
|
||||
Every written member is independently re-run once in non-reduce trace mode
|
||||
over its own recorded window, and its re-derived metrics are asserted equal
|
||||
to the recorded member's — a **C1 drift alarm** that refuses (exit 1) rather
|
||||
than persist a silently-wrong trace. The **synthetic** sweep / walk-forward
|
||||
path refuses `--trace` (exit 2); **Monte-Carlo is excluded from trace
|
||||
persistence** (`mc --real` refuses `--name`/`--trace`, exit 2 — the
|
||||
real-data R-bootstrap campaign itself runs, but records no per-member
|
||||
family traces; the synthetic `--seeds` family's realization argument, C12,
|
||||
does not carry over to one real series). `TraceStore::ensure_name_free`
|
||||
makes name resolution a total function, refusing cross-kind reuse of one
|
||||
name by both a run and a family." … (Newcomer, tail sentence) "so `aura run`
|
||||
prints summary R-metrics to stdout but writes no on-disk trace — a
|
||||
**chartable** trace today comes from a blueprint that declares taps (single
|
||||
run) or from a `--real … --trace` family campaign."
|
||||
|
||||
@@ -60,31 +60,31 @@ columnar (SoA, C7) `ColumnarTrace` — struct→JSON only — in `aura-engine`
|
||||
`aura-cli` (`render_chart_html` + the vendored `chart-viewer.js`). The engine stays
|
||||
headless.
|
||||
|
||||
**Single run.** `aura run <blueprint.json>` persists every tap the blueprint
|
||||
declares to the trace store under the run's own name, on both shapes: a
|
||||
`bias`-output strategy (`aura-runner::member::run_signal_r`, R-wrapped) and a bare
|
||||
measurement blueprint with ≥1 declared tap but no `bias`
|
||||
(`aura-runner::measure::run_measurement`). A tap-free run writes nothing to `runs/`
|
||||
and its stdout stays byte-identical. The CLI `--trace <NAME>` flag is **retired** on
|
||||
`run` (and on `mc`): it parses but is refused at dispatch (exit 2) — naming a trace
|
||||
is the family / campaign path's job, not a per-run flag.
|
||||
**Single run.** `aura exec <blueprint.json>` (#319) persists every tap the
|
||||
blueprint declares to the trace store under the run's own name, on both
|
||||
shapes: a `bias`-output strategy (`aura-runner::member::run_signal_r`,
|
||||
R-wrapped) and a bare measurement blueprint with ≥1 declared tap but no
|
||||
`bias` (`aura-runner::measure::run_measurement`). A tap-free run writes
|
||||
nothing to `runs/` and its stdout stays byte-identical. `exec` carries no
|
||||
free-standing `--trace <NAME>` flag — naming a trace is the family / campaign
|
||||
path's job, not a per-run flag (the repeatable `--tap TAP=FOLD` selector,
|
||||
#310, only chooses which of the blueprint's own declared taps to subscribe,
|
||||
never names the trace).
|
||||
|
||||
**Families.** `aura sweep|walkforward --real <SYM> … --trace <NAME>` persists *each
|
||||
member* via the campaign path: the dissolved verb is translated into a
|
||||
content-addressed process + campaign document whose `presentation.persist_taps`
|
||||
requests the tap vocabulary, run through the one campaign executor;
|
||||
`aura-runner::runner::persist_campaign_traces` writes each member under a depth-2
|
||||
fan-out `runs/traces/<name>/<cell>/<member>/`. Every written member is
|
||||
independently re-run once in non-reduce trace mode over its own recorded window,
|
||||
and its re-derived metrics are asserted equal to the recorded member's — a **C1
|
||||
drift alarm** that refuses (exit 1) rather than persist a silently-wrong trace. The
|
||||
**synthetic** sweep / walk-forward path refuses `--trace` (exit 2);
|
||||
**Monte-Carlo is excluded from trace persistence** (`mc --real` refuses
|
||||
`--name`/`--trace`, exit 2 — the real-data R-bootstrap campaign itself runs, but
|
||||
records no per-member family traces; the synthetic `--seeds` family's realization
|
||||
argument, C12, does not carry over to one real series).
|
||||
`TraceStore::ensure_name_free` makes name resolution a total function, refusing
|
||||
cross-kind reuse of one name by both a run and a family.
|
||||
**Families.** A campaign document's `presentation.persist_taps` (the closed
|
||||
tap vocabulary, C18/C27) requests traces for each cell's nominee, run through
|
||||
`aura exec <campaign.json|id>` (#319 — a document's `axes` replace the
|
||||
retired `sweep|walkforward --real <SYM> --trace <NAME>` grammar entirely;
|
||||
there is no per-invocation `--trace`/`--name` flag left on any surface):
|
||||
`aura-runner::runner::persist_campaign_traces` writes each member under a
|
||||
depth-2 fan-out `runs/traces/{campaign8}-{run}/<cell>/<member>/`, the name
|
||||
derived deterministically (`derive_trace_name`), never user-chosen. Every
|
||||
written member is independently re-run once in non-reduce trace mode over
|
||||
its own recorded window, and its re-derived metrics are asserted equal to
|
||||
the recorded member's — a **C1 drift alarm** that refuses (exit 1) rather
|
||||
than persist a silently-wrong trace. `TraceStore::ensure_name_free` makes
|
||||
name resolution a total function, refusing cross-kind reuse of one name by
|
||||
both a run and a family.
|
||||
|
||||
**Viewer.** `aura chart <name> [--tap <t>] [--panels]` classifies the name on disk
|
||||
(`TraceStore::name_kind`: top-level `index.json` → a single Run; member subdirs → a
|
||||
@@ -117,10 +117,10 @@ vocabulary (an SMA-cross → `Bias` strategy), `.gitignore`, and a project `CLAU
|
||||
The engine also ships example blueprints under
|
||||
`crates/aura-cli/examples/r_{sma,breakout,channel,meanrev}.json`. Honest gap against
|
||||
the Guarantee's "populated trace immediately": these shipped blueprints are
|
||||
*strategies* (a `bias` output, no declared taps), so `aura run` prints summary
|
||||
*strategies* (a `bias` output, no declared taps), so `aura exec` prints summary
|
||||
R-metrics to stdout but writes no on-disk trace — a **chartable** trace today comes
|
||||
from a blueprint that declares taps (single run) or from a `--real … --trace` family
|
||||
campaign.
|
||||
from a blueprint that declares taps (single run) or from a campaign document
|
||||
requesting `presentation.persist_taps`.
|
||||
|
||||
**Deferred.** Live sink streams *during* a run are not built — taps are
|
||||
buffer-then-drain (collected, then written after the run), and there is no local
|
||||
|
||||
@@ -157,4 +157,92 @@ convert every deliberate template improvement into forced churn of the frozen
|
||||
fixture — the same cross-purpose coupling that rules out regenerating the
|
||||
fixture from the scaffolder.
|
||||
|
||||
**Current-state "Runs and families are built FROM blueprint-data" section
|
||||
(superseded by the #319 sugar retirement, 2026-07-25):** "`aura run
|
||||
<blueprint.json>` loads a serialized **signal** blueprint and emits a
|
||||
`RunReport` **bit-identical** (C1) to its Rust-built twin; the run scaffolding
|
||||
(sinks / broker / data) is supplied **at run**, not serialized. Beyond a
|
||||
single run, the World constructs and orchestrates **families** of harnesses
|
||||
from topology-data:
|
||||
|
||||
- `aura sweep <blueprint.json> --axis <name>=<csv>` → `FamilyKind::Sweep`. A
|
||||
sweep needs an **open** blueprint (a fully-bound one has an empty
|
||||
`param_space`).
|
||||
- `aura mc <blueprint.json> --seeds N` → `FamilyKind::MonteCarlo`, each seed a
|
||||
distinct synthetic price walk drawn disjoint-parallel through the engine
|
||||
`monte_carlo` seam (invariant 1). MC binds no axis, so it needs a **closed**
|
||||
blueprint (the sweep's distinction inverted); an open one returns a named
|
||||
`Err`, rendered exit-2 at the builder boundary — no hidden exit in the pure
|
||||
builder.
|
||||
- `aura walkforward <blueprint.json> --axis <name>=<csv> [--select
|
||||
argmax|plateau:mean|plateau:worst]` → `FamilyKind::WalkForward`:
|
||||
re-optimizes the loaded blueprint's params over the `--axis` grid on each
|
||||
24/12/12 IS window, selects the winner by `sqn_normalized`, runs it
|
||||
out-of-sample. Reduce-mode members are R-measured (`oos_r` the meaningful
|
||||
summary; stitched pip-equity empty, C10).
|
||||
|
||||
The family builders live in `aura-runner::family` (`blueprint_sweep_family` /
|
||||
`blueprint_mc_family` / `blueprint_walkforward_family`). Every member manifest
|
||||
carries the **shared** `topology_hash` (one signal topology, only params
|
||||
vary; `member_key` distinguishes members), and each family stores its
|
||||
blueprint(s) content-addressed so `aura reproduce` re-derives every member
|
||||
bit-identically (`aura-runner::reproduce`, C18). The synthetic-walk DGP is
|
||||
the MC machinery, not trader-grade statistics; a real-data block-bootstrap —
|
||||
and retiring the `synthetic_walk_sources` `len:60`↔warm-up coupling — rides
|
||||
#172."
|
||||
|
||||
**Current-state "Axis discovery" section (superseded by the #319 sugar
|
||||
retirement, 2026-07-25):** "`aura sweep <blueprint.json> --list-axes` lists
|
||||
a loaded blueprint's open sweepable knobs (one `<name>:<kind>` per line,
|
||||
`param_space()` order) and exits; the printed names are exactly what
|
||||
`--axis` binds. Every listed name is **mandatory** on `sweep` /
|
||||
`walkforward` — the blueprint must be fully bound before it runs — so a
|
||||
subset grid is refused with the missing knob named (`BindError::MissingKnob`,
|
||||
`aura-engine`) and there is no default; pin a knob you do not want to vary
|
||||
with a single-value axis. A single `blueprint_axis_probe` (`aura-runner`)
|
||||
single-sources the wrapped probe for the sweep terminal, the MC closed-check,
|
||||
and the listing, so **listed == swept by construction** (and stays so across
|
||||
the harness retirement — the listing tracks whatever the sweep actually
|
||||
resolves). One raw namespace, `<node>.<param>` (a splice path keeps its
|
||||
interior path, e.g. `anchor.sess.period_minutes`), is the only user-facing
|
||||
axis name (#328): `graph introspect --params` and `--list-axes` are
|
||||
line-identical (open params bare, bound params with `default=`) and both
|
||||
print exactly what `--axis` binds, on both sweep routes. The wrapped
|
||||
`<blueprint>.<node>.<param>` form the probe still resolves against
|
||||
internally is retired from the surface, with a translation refusal naming
|
||||
the raw candidate on both intake seams (`--axis` and `campaign validate`).
|
||||
`--trace` on `sweep` / `walkforward` writes per-member traces on the
|
||||
real-data campaign path (depth-2 fan-out, chartable by the printed family
|
||||
handle, #224); the synthetic path still refuses (`run` / `mc` refuse
|
||||
`--trace` outright). The live trace-writer is the campaign
|
||||
`presentation.persist_taps` (`persist_campaign_traces`,
|
||||
`aura-runner::runner`)."
|
||||
|
||||
**Current-state `name`-op gated-intake clause (superseded by the #319 sugar
|
||||
retirement, 2026-07-25):** "Gated by a shared shape check (non-empty, single
|
||||
path segment, no `/`, `\`, `.` or `..`) applied at this op intake **plus
|
||||
every CLI intake that reads an authored blueprint envelope from a file** —
|
||||
`register`, `introspect --content-id <FILE>`, the bare graph-file viewer,
|
||||
`run`, `introspect --params <FILE>`, `sweep --list-axes`, and each of
|
||||
`sweep`/`walkforward`/`mc`/`generalize`'s file-reading entry points
|
||||
(synthetic and `--real` alike) — one class of intake, one gate. The
|
||||
refusal's core sentence (`name_gate_fault_prose`) is byte-uniform across
|
||||
every one of these sites; only the leading context prefix varies — `run`
|
||||
and the bare graph-file viewer prepend `aura: <path>:`, while `register`,
|
||||
the sweep-axis family (`validate_and_register_axes`), and `introspect`
|
||||
prepend bare `aura: `. Store read-back (`reproduce`, `use` resolution,
|
||||
`introspect`/`--params` by content id) stays deliberately ungated — C29: a
|
||||
registered artifact is never retroactively invalidated."
|
||||
|
||||
**[C26, 2026-07-10 (#231): the single-price data weld inside the surviving `wrap_r` scaffolding is retired — input roles bind archive columns by name; the wrapper's remaining R-scaffolding retirement stays #159.]** (From the C24 Forbids clause as of that date; the single-price weld was retired at #231/C26, but `wrap_r` itself survives — its full R-scaffolding retirement stays deferred, #159.)
|
||||
|
||||
---
|
||||
|
||||
> The disjoint-parallel synthetic
|
||||
> price walks a sweep/MC family draws (`aura_runner::family`) are the family
|
||||
> machinery, not trader-grade statistics; a real-data block-bootstrap — and
|
||||
> retiring the `synthetic_walk_sources` `len:60`↔warm-up coupling — rides #172.
|
||||
|
||||
(superseded by the #319 sugar retirement, 2026-07-25 — audit-close correction:
|
||||
no family-minting path draws synthetic walks after the family-builder
|
||||
retirement; only `reproduce` consumes them)
|
||||
|
||||
@@ -75,34 +75,36 @@ channel). `LinComb` / `CostSum` / `Session` — formerly listed here — entered
|
||||
round-trippable set with #271's typed construction args (see the add-op `args`
|
||||
clause and the data-driven `format_version` below).
|
||||
|
||||
### Runs and families are built FROM blueprint-data
|
||||
### Runs and families are built FROM documents, via `exec` (#319, 2026-07-25)
|
||||
|
||||
`aura run <blueprint.json>` loads a serialized **signal** blueprint and emits a
|
||||
`RunReport` **bit-identical** (C1) to its Rust-built twin; the run scaffolding (sinks
|
||||
/ broker / data) is supplied **at run**, not serialized. Beyond a single run, the
|
||||
World constructs and orchestrates **families** of harnesses from topology-data:
|
||||
`aura exec <blueprint.json>` loads a serialized **signal** blueprint and runs it
|
||||
once, emitting a `RunReport` **bit-identical** (C1) to its Rust-built twin (a
|
||||
no-bias blueprint with ≥1 declared tap runs the measurement leg instead); the
|
||||
run scaffolding (sinks / broker / data) is supplied **at run**, not serialized.
|
||||
Beyond a single run, the World constructs and orchestrates **families** from a
|
||||
**campaign document** (role 6b, [C18](c18-registry.md)/[C25](c25-role-model.md)):
|
||||
`aura exec <campaign.json|id>` drives the document's `strategies[].axes` through
|
||||
the `aura-campaign` executor (`aura_campaign::exec::execute`) — one member per
|
||||
(strategy, instrument, window) cell, the process document's stage pipeline
|
||||
(`std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]?
|
||||
[std::generalize]?`) deciding what each cell realizes (grid family, gated
|
||||
survivors, walk-forward roll, MC bootstrap, cross-instrument generalization —
|
||||
C18's cycle-0107/0108 realizations). The one deliberate argv residue,
|
||||
`--override NODE.PARAM=VALUE`, reopens a bound param for the execution without
|
||||
touching the document (blueprint leg: `override_paths` + `reopen_all`; campaign
|
||||
leg: injected as a single-value axis ahead of `validate_campaign_refs`, refusing
|
||||
a collision with a document-declared axis).
|
||||
|
||||
- `aura sweep <blueprint.json> --axis <name>=<csv>` → `FamilyKind::Sweep`. A sweep
|
||||
needs an **open** blueprint (a fully-bound one has an empty `param_space`).
|
||||
- `aura mc <blueprint.json> --seeds N` → `FamilyKind::MonteCarlo`, each seed a
|
||||
distinct synthetic price walk drawn disjoint-parallel through the engine
|
||||
`monte_carlo` seam (invariant 1). MC binds no axis, so it needs a **closed**
|
||||
blueprint (the sweep's distinction inverted); an open one returns a named `Err`,
|
||||
rendered exit-2 at the builder boundary — no hidden exit in the pure builder.
|
||||
- `aura walkforward <blueprint.json> --axis <name>=<csv> [--select
|
||||
argmax|plateau:mean|plateau:worst]` → `FamilyKind::WalkForward`: re-optimizes the
|
||||
loaded blueprint's params over the `--axis` grid on each 24/12/12 IS window,
|
||||
selects the winner by `sqn_normalized`, runs it out-of-sample. Reduce-mode members
|
||||
are R-measured (`oos_r` the meaningful summary; stitched pip-equity empty, C10).
|
||||
|
||||
The family builders live in `aura-runner::family` (`blueprint_sweep_family` /
|
||||
`blueprint_mc_family` / `blueprint_walkforward_family`). Every member manifest carries
|
||||
the **shared** `topology_hash` (one signal topology, only params vary; `member_key`
|
||||
distinguishes members), and each family stores its blueprint(s) content-addressed so
|
||||
`aura reproduce` re-derives every member bit-identically (`aura-runner::reproduce`,
|
||||
C18). The synthetic-walk DGP is the MC machinery, not trader-grade statistics; a
|
||||
real-data block-bootstrap — and retiring the `synthetic_walk_sources` `len:60`↔warm-up
|
||||
coupling — rides #172.
|
||||
Every member manifest carries the **shared** `topology_hash` (one signal
|
||||
topology, only params vary; `member_key` distinguishes members), and the
|
||||
executor stores its blueprint(s) content-addressed so `aura reproduce`
|
||||
re-derives every member bit-identically (C18). The synthetic price walks
|
||||
(`aura_runner::family::synthetic_walk_sources`) survive the #319 retirement
|
||||
only as `reproduce`'s re-derivation inputs for historically minted synthetic
|
||||
families — no family-minting path draws them anymore (the campaign executor
|
||||
is the one family builder, C20) — and they are not trader-grade statistics; a
|
||||
real-data block-bootstrap — and retiring the `len:60`↔warm-up coupling —
|
||||
rides #172.
|
||||
|
||||
### Reproduction identity
|
||||
|
||||
@@ -119,27 +121,24 @@ identity id is introspection-only until a dedup consumer exists.
|
||||
|
||||
### Axis discovery
|
||||
|
||||
`aura sweep <blueprint.json> --list-axes` lists a loaded blueprint's open sweepable
|
||||
knobs (one `<name>:<kind>` per line, `param_space()` order) and exits; the printed
|
||||
names are exactly what `--axis` binds. Every listed name is **mandatory** on `sweep` /
|
||||
`walkforward` — the blueprint must be fully bound before it runs — so a subset grid is
|
||||
refused with the missing knob named (`BindError::MissingKnob`, `aura-engine`) and
|
||||
there is no default; pin a knob you do not want to vary with a single-value axis. A
|
||||
single `blueprint_axis_probe` (`aura-runner`) single-sources the wrapped probe for the
|
||||
sweep terminal, the MC closed-check, and the listing, so **listed == swept by
|
||||
construction** (and stays so across the harness retirement — the listing tracks
|
||||
whatever the sweep actually resolves). One raw namespace, `<node>.<param>` (a splice
|
||||
path keeps its interior path, e.g. `anchor.sess.period_minutes`), is the only
|
||||
user-facing axis name (#328): `graph introspect --params` and `--list-axes` are
|
||||
line-identical (open params bare, bound params with `default=`) and both print
|
||||
exactly what `--axis` binds, on both sweep routes. The wrapped
|
||||
`<blueprint>.<node>.<param>` form the probe still resolves against internally is
|
||||
retired from the surface, with a translation refusal naming the raw candidate on
|
||||
both intake seams (`--axis` and `campaign validate`). `--trace` on
|
||||
`sweep` / `walkforward` writes per-member traces on the real-data campaign path
|
||||
(depth-2 fan-out, chartable by the printed family handle, #224); the synthetic path
|
||||
still refuses (`run` / `mc` refuse `--trace` outright). The live trace-writer is the
|
||||
campaign `presentation.persist_taps` (`persist_campaign_traces`, `aura-runner::runner`).
|
||||
`aura graph introspect --params <blueprint.json|id>` (#328) is the one
|
||||
axis-discovery surface: it lists a loaded blueprint's open knobs (one
|
||||
`<name>:<kind>` per line, `param_space()` order) followed by its bound knobs
|
||||
(`<name>:<kind> default=<value>`) — the campaign-axis namespace
|
||||
`validate_campaign_refs` checks a campaign document's `strategies[].axes`
|
||||
against. Every open name a campaign document's `axes` varies is **mandatory**:
|
||||
a subset is refused with the missing knob named (`BindError::MissingKnob`,
|
||||
`aura-engine`) and there is no default; pin a knob you do not want to vary with
|
||||
a single-value axis. One raw namespace, `<node>.<param>` (a splice path keeps
|
||||
its interior path, e.g. `anchor.sess.period_minutes`), is the only user-facing
|
||||
axis name (#328) — the wrapped `<blueprint>.<node>.<param>` form is retired
|
||||
from the surface, with a did-you-mean refusal naming the raw candidate at the
|
||||
one remaining intake seam, `campaign validate` (#319 retired the standalone
|
||||
`--axis`/`--list-axes` CLI flags this used to also gate). Per-member traces
|
||||
are the campaign document's `presentation.persist_taps` (closed vocabulary,
|
||||
C18 cycle-0109) or, on `exec`'s blueprint leg, the repeatable `--tap
|
||||
TAP=FOLD` selector (#310) over a blueprint's own declared taps (C27); the
|
||||
live trace-writer is `persist_campaign_traces` (`aura-runner::runner`).
|
||||
|
||||
### The construction service (op-script)
|
||||
|
||||
@@ -203,15 +202,15 @@ replayed in order; nodes are referenced **by identifier**, ports as dotted
|
||||
built composite keeps the CLI's own seed default (`"graph"`). Gated by a
|
||||
shared shape check (non-empty, single path segment, no `/`, `\`, `.` or
|
||||
`..`) applied at this op intake **plus every CLI intake that reads an
|
||||
authored blueprint envelope from a file** — `register`, `introspect
|
||||
--content-id <FILE>`, the bare graph-file viewer, `run`, `introspect
|
||||
--params <FILE>`, `sweep --list-axes`, and each of
|
||||
`sweep`/`walkforward`/`mc`/`generalize`'s file-reading entry points
|
||||
(synthetic and `--real` alike) — one class of intake, one gate. The
|
||||
refusal's core sentence (`name_gate_fault_prose`) is byte-uniform across
|
||||
every one of these sites; only the leading context prefix varies —
|
||||
`run` and the bare graph-file viewer prepend `aura: <path>:`, while
|
||||
`register`, the sweep-axis family (`validate_and_register_axes`), and
|
||||
authored blueprint envelope from a file** — `graph register`, `introspect
|
||||
--content-id <FILE>`, the bare graph-file viewer, `introspect --params
|
||||
<FILE>`'s file branch, and `exec`'s blueprint leg (#319; its narrower,
|
||||
envelope-only grammar shares the same `gate_authored_root_name` call
|
||||
directly rather than through the shape-discriminating wrapper) — one
|
||||
class of intake, one gate. The refusal's core sentence
|
||||
(`name_gate_fault_prose`) is byte-uniform across every one of these
|
||||
sites; only the leading context prefix varies — `exec` and the bare
|
||||
graph-file viewer prepend `aura: <path>:`, while `register` and
|
||||
`introspect` prepend bare `aura: `. Store read-back (`reproduce`, `use`
|
||||
resolution, `introspect`/`--params` by content id) stays deliberately
|
||||
ungated — C29: a registered artifact is never retroactively invalidated.
|
||||
@@ -245,8 +244,8 @@ replayed in order; nodes are referenced **by identifier**, ports as dotted
|
||||
recorded observation point (a `Composite.taps` entry, C27), not a boundary output.
|
||||
Name-addressed like every other op (no raw index); tap names are their own
|
||||
namespace (a duplicate refuses). The `finish` gate threads op-declared taps into
|
||||
the built `Composite` (`.with_taps`); a single `aura run` records each, a sweep
|
||||
leaves them inert.
|
||||
the built `Composite` (`.with_taps`); a single `aura exec` (#319) records
|
||||
each, a campaign member run leaves them inert.
|
||||
- `gang` — `{"op":"gang","as":"channel_length","into":["channel_hi.length","channel_lo.length"]}`
|
||||
— fuse two or more sibling params into ONE public knob: the member addresses
|
||||
leave the sweepable param space and `as` replaces them; the bound or swept
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# C25 — The role model: nine authoring roles, cut by artifact + surface + iteration cost: history
|
||||
|
||||
> FROZEN HISTORICAL RECORD. Each block below was true as of its cycle/date stamp and may be superseded; this file is NOT current truth and NOT a grounding surface. Current contract: [c25-role-model.md](c25-role-model.md).
|
||||
|
||||
**Current-state executor-verb-set sentence (owner-minuted 2026-07-21 on #295,
|
||||
delivered by #300; superseded by the #319 sugar retirement, 2026-07-25):**
|
||||
"The executor verb set is settled: `run`, the four thin per-verb generators,
|
||||
and the document verbs validate/introspect/register/show/run/runs — `show`
|
||||
being #300's read-back addition. The verbs' per-verb identity is re-ratified
|
||||
(#300 F8 — reduction, not grammar collapse)."
|
||||
@@ -74,12 +74,14 @@ present but faceless: no addressed verb families yet. Role homes in the project
|
||||
layout and docs-by-role are open (#192).
|
||||
|
||||
**Document-first completion** is the resolved direction on the control-surface
|
||||
amendment (owner-minuted 2026-07-21 on #295), delivered by #300. The executor verb
|
||||
set is settled: `run`, the four thin per-verb generators, and the document verbs
|
||||
validate/introspect/register/show/run/runs — `show` being #300's read-back
|
||||
addition. The verbs' per-verb identity is re-ratified (#300 F8 — reduction, not
|
||||
grammar collapse). A typed-protocol host or MCP face remains demand-driven and
|
||||
unbuilt.
|
||||
amendment (owner-minuted 2026-07-21 on #295), delivered by #300. The executor
|
||||
surface is settled again, one reduction further (#319, 2026-07-25): a single
|
||||
`exec <target>` verb runs both document classes — a campaign (file or content
|
||||
id) and a signal blueprint (single run) — over the document verbs
|
||||
validate/introspect/register/show/runs (`show` #300's read-back addition); the
|
||||
one argv residue is `--override NODE.PARAM=VALUE`, a per-execution bound-param
|
||||
reopen threading through both `exec` legs. A typed-protocol host or MCP face
|
||||
remains demand-driven and unbuilt.
|
||||
|
||||
## See also
|
||||
- [C16](c16-engine-project-split.md) — the game-engine analogy this extends to people
|
||||
@@ -88,3 +90,5 @@ unbuilt.
|
||||
- [C20](c20-strategy-harness.md), [C21](c21-world.md) — the node/harness/World tiers behind the 6a/6b split
|
||||
- [C24](c24-blueprint-data.md) — topology-as-data, the strategy designer's artifact
|
||||
- [C26](c26-input-binding.md) — a role's input contract carried in blueprint data
|
||||
|
||||
> History: [c25-role-model.history.md](c25-role-model.history.md)
|
||||
|
||||
@@ -9,3 +9,8 @@ the #310 `--tap` selector):** "…and the shared `bind_tap_plan`/`BoundTaps`
|
||||
pair called by both declared-tap entry points, `run_signal_r`
|
||||
(`aura-runner::member`) and `run_measurement` (`aura-runner::measure`); both
|
||||
CLI verbs pass a record-all plan."
|
||||
|
||||
**Current-state "single CLI verb" clause (superseded by the #319 sugar
|
||||
retirement, 2026-07-25):** "…both arms of the single CLI verb `aura run`,
|
||||
whose repeatable `--tap TAP=FOLD` selector (#310) makes the `Named`
|
||||
selection data-reachable…"
|
||||
|
||||
@@ -68,8 +68,8 @@ and the roster-enumerating refusal — plus a scalar-typed param schema; all cor
|
||||
entries are param-less today, the seam ships in every entry's build signature),
|
||||
and the shared `bind_tap_plan`/`BoundTaps` pair called by both declared-tap entry
|
||||
points, `run_signal_r` (`aura-runner::member`) and `run_measurement`
|
||||
(`aura-runner::measure`) — both arms of the single CLI verb `aura run`, whose
|
||||
repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
|
||||
(`aura-runner::measure`) — both arms of the single CLI verb `aura exec` (#319),
|
||||
whose repeatable `--tap TAP=FOLD` selector (#310) makes the `Named` selection
|
||||
data-reachable: no flag keeps the record-all default, any flag replaces the
|
||||
plan entirely (unlisted taps stay unbound/inert). The boundary is thereby
|
||||
fixed in place: *selecting* a subscription is run-mode authority, exercised
|
||||
|
||||
+11
-11
@@ -33,7 +33,7 @@ A strategy's primary, backtestable DAG output: one signed, bounded `f64 ∈ [-1,
|
||||
|
||||
### blueprint
|
||||
**Avoid:** —
|
||||
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed. Registered and inspected headless (`aura graph register`, `aura graph introspect --params` — the raw `param_space` namespace campaign axes bind against; cycle 0107/#196); a *bound* param is an overridable **default** — a sweep axis naming it re-opens it per family (#246), while `run` uses it as-is.
|
||||
The param-generic, input-role-generic graph-as-data produced by running a Rust builder; it carries free numeric params and free input roles before bootstrap. Bootstrapped into a frozen instance by binding params + data + seed. Registered and inspected headless (`aura graph register`, `aura graph introspect --params` — the raw `param_space` namespace campaign axes bind against; cycle 0107/#196); a *bound* param is an overridable **default** — a sweep axis naming it re-opens it per family (#246), while a plain `exec` uses it as-is (#319).
|
||||
|
||||
### blueprint label
|
||||
**Avoid:** —
|
||||
@@ -41,7 +41,7 @@ A registry-level name pointing at a registered blueprint's content id (`graph re
|
||||
|
||||
### bootstrap
|
||||
**Avoid:** —
|
||||
The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense. Disambiguation: the *statistical* moving-block bootstrap of a trade-R series (`r_bootstrap`, `RBootstrap`) is a different thing that keeps its statistics name — it appears as the deflation null, the `aura mc` R path, and since 0108 the `std::monte_carlo` stage's `stage bootstrap` annotation; context (construction vs annotation) disambiguates.
|
||||
The distinct, recursive construction phase that binds `(blueprint + param-set + data bindings + seed)` into a frozen instance — buffers sized, topology fixed. The explicit name for the "wiring / graph build" that C7/C12 reference — the construction/compilation sense. Disambiguation: the *statistical* moving-block bootstrap of a trade-R series (`r_bootstrap`, `RBootstrap`) is a different thing that keeps its statistics name — it appears as the deflation null and, since 0108, the `std::monte_carlo` stage's `stage bootstrap` annotation (the campaign executor's one R-bootstrap path, #319); context (construction vs annotation) disambiguates.
|
||||
|
||||
### bot
|
||||
**Avoid:** —
|
||||
@@ -53,11 +53,11 @@ A downstream consumer node, never part of the strategy: the signal-quality side
|
||||
|
||||
### campaign document
|
||||
**Avoid:** experiment doc, campaign file
|
||||
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored, validated, and executed headless (`aura campaign validate|introspect|register|show|run|runs` — `show` prints a registered document's canonical bytes back, #300), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura campaign run` (v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. `std::sweep`'s own selection group (`metric`+`select`) is optional, all-or-nothing, and permitted only as the pipeline's terminal stage when omitted (a selection-free sweep, #210).
|
||||
The role-6b research artifact (#188/#189): persisted experiment intent as closed-vocabulary canonical JSON — instruments × windows × strategy refs (by `content id` or `identity id`) × per-strategy param axes (each axis declares its `ScalarKind` once over bare values) × a process reference (content-id-only) × data-level presentation (taps to persist — the closed `tap` vocabulary — and tables to emit). Authored headless (`aura campaign validate|introspect|register|runs|show` — `show` prints a registered document's canonical bytes back, #300), content-addressed beside blueprints in the registry store; carries P1 control constructs (bounded axes, gates, ladders) as intent, executed by `aura exec` (#319; v2 pipeline shape `std::sweep [std::gate]* [std::walk_forward]? [std::monte_carlo]? [std::generalize]?` — the two annotators terminal, cycles 0107/0108, #198/#200) into a `campaign run` realization. `std::sweep`'s own selection group (`metric`+`select`) is optional, all-or-nothing, and permitted only as the pipeline's terminal stage when omitted (a selection-free sweep, #210).
|
||||
|
||||
### campaign run
|
||||
**Avoid:** campaign execution record, realized campaign
|
||||
One execution of a `campaign document` (`aura campaign run <file|content id>`; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. When the document requests `persist_taps`, the record's sparse `trace_name` (`"{campaign8}-{run}"`) points at the TraceStore family holding each nominee cell's persisted taps (0109/#201). NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record.
|
||||
One execution of a `campaign document` (`aura exec <file|content id>`, #319; a file is register-then-run sugar — the content id is the canonical address). Realized once per (strategy, instrument, window) cell in doc order and recorded as a thin `CampaignRunRecord` line in the registry's `campaign_runs.jsonl` — linking the per-stage family ids, gate survivor ordinals, `stage bootstrap` annotations, and (campaign-scope, per strategy × window) `generalizations` entries (`generalization` with `worst_case`/`sign_agreement`/`per_instrument`, plus `winners` params and `missing` instruments on shortfall) over untouched family records, run-counted per campaign id. A cell whose gate leaves no survivors records its realized prefix and the run exits 0: a null result is a valid research result. Deterministic from doc + stores + data (C1): deflation and bootstrap nulls seed from the doc's `seed`. When the document requests `persist_taps`, the record's sparse `trace_name` (`"{campaign8}-{run}"`) points at the TraceStore family holding each nominee cell's persisted taps (0109/#201). NB the stdout emit wraps each record as `{"campaign_run": …}`; the stored `campaign_runs.jsonl` line is the bare record.
|
||||
|
||||
### cdylib
|
||||
**Avoid:** —
|
||||
@@ -89,7 +89,7 @@ A composable downstream **C9 graph of cost nodes**, in **R**, that **approximate
|
||||
|
||||
### cross-instrument generalization
|
||||
**Avoid:** cross-symbol pooling, pooled generalization
|
||||
The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by `aura generalize` and, since 0108 (#200), by the `std::generalize` process stage at campaign scope (per (strategy, window) over the cells' nominees across instruments, recorded in the `campaign run`'s `generalizations`); an aggregator (a recomputable family score), never a selector that picks a winner.
|
||||
The validation read that grades how consistently one *brought* candidate holds across a set of instruments, scored on its weakest one — the across-instrument axis of the anti-false-discovery discipline. Realised by the `std::generalize` process stage at campaign scope (since 0108, #200; per (strategy, window) over the cells' nominees across instruments, recorded in `CampaignRunRecord.generalizations`, executed via `aura exec`, #319) — the standalone `aura generalize` verb that used to persist its own per-instrument family is retired (#319: `FamilyKind::CrossInstrument` is now dead); an aggregator (a recomputable family score), never a selector that picks a winner.
|
||||
|
||||
### cycle
|
||||
**Avoid:** —
|
||||
@@ -203,7 +203,7 @@ The recorded chance that a deflated sweep winner is noise rather than edge — a
|
||||
|
||||
### plateau selection
|
||||
**Avoid:** plateau-over-peak (as a noun)
|
||||
A selection objective that argmaxes the neighbourhood-smoothed metric surface (mean or worst-case) rather than the bare in-sample peak, preferring a robust parameter plateau to a lucky spike. Opt-in via `--select plateau:mean|plateau:worst`; the default selection stays a bare argmax.
|
||||
A selection objective that argmaxes the neighbourhood-smoothed metric surface (mean or worst-case) rather than the bare in-sample peak, preferring a robust parameter plateau to a lucky spike. Opt-in via a process document's `std::sweep`/`std::walk_forward` stage `"select": "plateau:mean"|"plateau:worst"` field; the default selection stays a bare argmax.
|
||||
|
||||
### playground
|
||||
**Avoid:** —
|
||||
@@ -331,15 +331,15 @@ The harness's structural parameterization — which strategy, instrument(s), bro
|
||||
|
||||
### sweep
|
||||
**Avoid:** param-sweep, parameter sweep
|
||||
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`--list-axes`) is **required** — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (`--axis name=<one-value>`), there is no default. One raw namespace, `<node>.<param>` (a splice path keeps its interior path), is the only axis name (#328): `graph introspect --params` and `aura sweep --list-axes` are line-identical (open params bare, bound params with `default=`), and `--axis` accepts exactly that raw form on both sweep routes — the older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`) is retired from the surface; naming it on `--axis` refuses (`axis names are raw node.param paths — use "fast.length" …`), and quoting it in a campaign document gets a did-you-mean toward the raw candidate. The `aura sweep` CLI verb is now thin sugar over the `campaign document` path — its blueprint form (`<bp.json> --real`) translates to a generated, content-addressed campaign run through the one executor (#210); the built-in `--strategy` sweep surface was retired by #159 (its hard-wired harnesses removed) — sweep now runs from a blueprint + `--axis`, and a retired `--strategy` token falls to the generic usage error. A ganged pair contributes ONE axis.
|
||||
An orchestration axis varying tuning params (grid or random) within a fixed structure. The inner, param-tuning loop, distinct from the structural experiment matrix. On a loaded blueprint every open knob (`graph introspect --params`) is **required** on a campaign document's `strategies[].axes` — a subset is refused with the missing knob named; pin an unwanted knob with a single-value axis (one value in its `values` list), there is no default. One raw namespace, `<node>.<param>` (a splice path keeps its interior path), is the only axis name (#328): `graph introspect --params` and a campaign axis key are line-identical (open params bare, bound params with `default=`) — the older `<blueprint>.<node>.<param>` wrapped form (e.g. `graph.fast.length`) is retired from the surface; naming it in a campaign document gets a did-you-mean toward the raw candidate, the one remaining intake seam (#319 retired the standalone `--axis`/`--list-axes` CLI flags along with the blueprint-form `aura sweep` verb, whose own `--strategy` built-in surface #159 had already retired). Realised as a campaign document's axes (#210), executed through the one `aura exec` executor (#319). A ganged pair contributes ONE axis.
|
||||
|
||||
### tap
|
||||
**Avoid:** probe, monitor, scope (for the observation slot — "probe" is taken by the sweep-terminal `blueprint_axis_probe` sense; the #77 `Recorder`→`Probe` rename was retired, 2026-07-21)
|
||||
A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. Persisted taps are charted by the printed handle: a campaign run's via the record's `trace_name`, a `sweep`/`walkforward --trace` family's via the family handle the run prints (`aura chart <handle>`; its members are keyed `<cell>/<member>` in the chart) — or, equivalently, by the `--trace <NAME>` the user chose, when `NAME` uniquely resolves against the recorded campaign documents (#238; a name reused across runs refuses rather than guessing).
|
||||
A named recorded stream produced by a recording `sink` — the addressable label (e.g. `equity`, `net_r_equity`) under which one sink's per-cycle output is persisted as a columnar (SoA) `ColumnarTrace` and selected for charting via `--tap`. Distinct from the `sink` node that emits it (a tap is the stream, the sink is the role) and from a whole recorded run (a bundle of taps); taps fire at their own cadences and are fused only by joining on the recorded timestamp, never by positional index. In a `campaign document`, `persist_taps` names taps from the CLOSED vocabulary `equity | exposure | r_equity | net_r_equity` (`tap_vocabulary`, 0109/#201) — a new observable is a new vocabulary entry or an authored blueprint sink, never an open node-path namespace. Persisted taps are charted by the printed handle: a campaign run's deterministic `trace_name` (`"{campaign8}-{run}"`, never a user-chosen name since #319) charts its whole family (`aura chart <handle>`; members keyed `<cell>/<member>` in the chart) — or, equivalently, a bare campaign id/name that uniquely resolves against the recorded campaign documents (#238; a name reused across runs refuses rather than guessing).
|
||||
|
||||
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura run` constructs a recorder at each and persists the series through the trace store; a sweep leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
|
||||
Since C27 (#282) the word also names a second, author-facing sense: a **declared tap** — a `Composite.taps` entry `Tap { name, from: {node, field} }` a hand-authored blueprint declares on an interior output wire, the output-side twin of an `input_role`. It is a pure declaration (no channel endpoint); the harness binds it run-mode-aware (a single `aura exec` constructs a recorder at each and persists the series through the trace store; a campaign member run leaves it unbound and inert). This is an OPEN, per-blueprint author-declared name — distinct from the CLOSED campaign `persist_taps` vocabulary above, which selects among fixed observables of the standard R-harness. Both senses land as `ColumnarTrace`s in the same trace store; the closed vocabulary is what a `campaign document` selects, the declared tap is what a blueprint author names.
|
||||
|
||||
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura run` (`aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura run` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert. A fold's one summary row is emitted at finalize and stamped with the instant of the last contributing (warm) value — `first` alone pins the first contributing instant, and `min`/`max` deliberately do not carry the extremum's instant (ratified #335).
|
||||
Since #283 what CONSUMES a declared tap is itself declared per run by a **tap plan**: a subscription is either `Named { label, params }` — resolved against a layered **fold registry** whose core vocabulary is `record | count | sum | mean | min | max | first | last`, each entry carrying a doc line (the help surface and the roster-enumerating refusal) and a scalar-typed param schema (all core entries param-less; growth is a new Rust entry per C25, and higher layers register entries without touching the core) — or `Live(closure)`, the single non-data variant (an in-process consumer with consumer-owned loss policy). `record` streams the full series to the trace store at constant memory (no buffer-then-drain); folds keep an O(1) accumulator and land one summary row at finalize. Both declared-tap entry points are arms of the single verb `aura exec` (#319; `aura measure` is the post-hoc IC analysis over already-persisted traces and constructs no tap plan); `aura exec` subscribes every declared tap to `record` by default, and its repeatable `--tap TAP=FOLD` selector (#310) replaces that default with an explicit plan — only listed taps are bound, unlisted taps stay unbound/inert. A fold's one summary row is emitted at finalize and stamped with the instant of the last contributing (warm) value — `first` alone pins the first contributing instant, and `min`/`max` deliberately do not carry the extremum's instant (ratified #335).
|
||||
|
||||
### topology hash
|
||||
**Avoid:** —
|
||||
@@ -355,7 +355,7 @@ The **optional** documented pre-trade-gate seam in the execution chain (`stop-ru
|
||||
|
||||
### walk-forward
|
||||
**Avoid:** —
|
||||
An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob named by `--list-axes` is **required** on a loaded blueprint — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. The `aura walkforward <blueprint.json> --real <sym> --axis <name>=<csv> …` CLI verb is thin sugar over the `campaign document` path — translated to a generated campaign (`std::sweep → std::walk_forward`) run through the one executor (#210; blueprint-generic over arbitrary blueprints and axes since #220).
|
||||
An orchestration axis: rolling in-sample optimize + out-of-sample test across moving windows, stitched into one out-of-sample verdict plus parameter stability. As with `sweep`, every open knob a campaign document's axes name is **required** — a subset is refused with the missing knob named; pin one with a single-value axis, there is no default. Realised as a campaign document's `std::sweep → std::walk_forward` process pipeline (#210; blueprint-generic over arbitrary blueprints and axes since #220), executed through the one `aura exec` executor (#319) — the standalone `aura walkforward` CLI verb is retired.
|
||||
|
||||
### World
|
||||
**Avoid:** —
|
||||
|
||||
@@ -144,9 +144,10 @@ command sequences.
|
||||
`ThirdCandleLong` node's `schema` + `eval` in it, against `aura-core`.
|
||||
3. **Backtest:** an op-script wiring `ger40_lab_nodes::ThirdCandleLong` (§1,
|
||||
`docs/authoring-guide.md`) is built into `blueprints/third-candle-long.json`
|
||||
(`aura graph build`); `aura run blueprints/third-candle-long.json --real
|
||||
GER40 --from 1704067200000 --to 1735689600000` (the window bounds are Unix
|
||||
milliseconds — here 2024) → the strategy produces a broker-independent, unsized
|
||||
(`aura graph build`); `aura exec blueprints/third-candle-long.json` (#319)
|
||||
smoke-runs it over the synthetic stream — a real-data window over `GER40`
|
||||
between two Unix-millisecond bounds is a one-cell campaign document (step
|
||||
4) → the strategy produces a broker-independent, unsized
|
||||
**bias stream** (one signed, bounded `f64 ∈ [-1,+1]` per cycle — sign = direction,
|
||||
magnitude = optional conviction). A downstream **risk-based executor** (stop-rule →
|
||||
position-management, in **R**, the protective stop defining 1R) turns the bias into
|
||||
@@ -165,7 +166,7 @@ command sequences.
|
||||
Monte-Carlo over seeds, or a structural matrix like "these 10 strategies ×
|
||||
these 3 instruments × {fixed-stop, vol-stop} risk-executors" — instruments
|
||||
× windows × strategy × param axes × process, headless-authorable and
|
||||
registered under `blueprints/`. `aura campaign run <content-id>`
|
||||
registered under `blueprints/`. `aura exec <content-id>` (#319)
|
||||
bootstraps the matrix, fans the disjoint sims over all cores (C1), and
|
||||
writes the comparable runs to `runs/`.
|
||||
5. **Compose:** "combine it with `momentum-filter` as a weighted sum" → Claude
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
# Fieldtest transcript — cycle 319 (sugar retirement, one `exec` verb)
|
||||
|
||||
Binary under test: `target/release/aura`, built from this worktree
|
||||
(`cargo build --release --workspace`, HEAD = `1476990`, tree dirty only by
|
||||
this corpus). Every command below was run from inside the scaffolded project
|
||||
`c319lab/` unless the prompt says otherwise. Public-interface sources used:
|
||||
`README.md`, `docs/authoring-guide.md`, `docs/project-layout.md`,
|
||||
`docs/design/INDEX.md` + `contracts/c14-headless-two-faces.md`, `aura --help`,
|
||||
the scaffolded `c319lab/CLAUDE.md`. No implementation source was read.
|
||||
|
||||
## Persona
|
||||
|
||||
A downstream consumer who last used aura when the research verbs were
|
||||
`run / sweep / walkforward / mc / generalize`, returning to a fresh project.
|
||||
|
||||
## Files
|
||||
|
||||
| file | what it is |
|
||||
|---|---|
|
||||
| `c319lab/` | project scaffolded with `aura new c319lab` (its `runs/` store is git-ignored by the scaffold's own `.gitignore`) |
|
||||
| `c319_1_spread_tap.ops.json` | op-script: SMA crossover with a declared `spread` tap (canonical authoring form) |
|
||||
| `c319_1b_open.ops.json` | same, both SMA lengths left open (free knobs) |
|
||||
| `c319_2b_zero_bind.ops.json` | same, `fast.length` bound to `0` — the out-of-domain value authored, not overridden |
|
||||
| `c319_2_targets_*.json` | malformed / wrong-class `exec` targets |
|
||||
| `c319lab/blueprints/c319_3_process_sweep.json` | process document: `std::sweep` argmax on `sqn` |
|
||||
| `c319lab/blueprints/c319_3_campaign_sweep.json` | campaign document: GER40, 1 month, 3 axes / 4 points |
|
||||
| `c319lab/blueprints/c319_4_campaign_override.json` | same with only 2 axes, so `bias.scale` stays a bound default (override target) |
|
||||
| `c319lab/blueprints/c319_4b_campaign_zerolen.json` | `fast.length` axis = `[0]` (out-of-domain via the campaign leg) |
|
||||
| `c319lab/blueprints/c319_5_process_wf.json` | process: `std::sweep` → `std::walk_forward` |
|
||||
| `c319lab/blueprints/c319_5_campaign_wf_flat.json` | campaign with `fast.length == slow.length` → flat spread → zero trades |
|
||||
| `c319_*.txt` / `c319_*.err` | verbatim captured runs |
|
||||
|
||||
## 1 — Coming back to a retired verb (axis 3)
|
||||
|
||||
```
|
||||
$ aura new c319lab
|
||||
created project "c319lab" (data-only; attach native nodes later with `aura nodes new`)
|
||||
|
||||
$ aura run blueprints/signal.json
|
||||
error: unrecognized subcommand 'run'
|
||||
|
||||
tip: a similar subcommand exists: 'runs'
|
||||
EXIT=2
|
||||
```
|
||||
|
||||
(full set in `c319_3_retired_verbs.err`: `sweep`, `walkforward`, `mc`,
|
||||
`generalize` all give the bare clap "unrecognized subcommand", no tip; `run`
|
||||
gets the `runs` tip.)
|
||||
|
||||
Recovery took one `aura --help`: its about-text names `exec` as the one
|
||||
executor over both document classes and points at `graph introspect --params`
|
||||
for axes. The scaffolded `c319lab/CLAUDE.md` teaches the same three lines.
|
||||
Neither the retired-verb refusal nor anything else names `exec` at the point
|
||||
of failure.
|
||||
|
||||
## 2 — Blueprint leg: smoke run, taps, overrides (axes 1, 2, 4)
|
||||
|
||||
`c319_1_tap_loop.txt` — build from an op-script, list the fold roster, run
|
||||
with `--tap spread=record`, run again under a different `--override`.
|
||||
`c319_2_override_family.txt` — the whole override refusal family on both legs.
|
||||
|
||||
Highlights (verbatim):
|
||||
|
||||
```
|
||||
$ aura exec blueprints/signal.json --override fast.length=8
|
||||
→ manifest.params=[['fast.length', {'I64': 8}]], defaults keep the rest EXIT=0
|
||||
$ aura exec blueprints/signal.json --override fast.length=8 --override fast.length=9
|
||||
aura: exec: --override names path "fast.length" twice EXIT=2
|
||||
$ aura exec blueprints/signal.json --override nosuch.param=3
|
||||
aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>` EXIT=1
|
||||
$ aura exec blueprints/signal.json --override fast.length
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length` EXIT=2
|
||||
$ aura exec blueprints/signal.json --override fast.length=abc
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length=abc` EXIT=2
|
||||
$ aura exec blueprints/signal.json --override fast.length=1.5
|
||||
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: I64, got: F64 } EXIT=1
|
||||
$ aura exec blueprints/signal.json --override fast.length=0
|
||||
thread 'main' panicked at crates/aura-std/src/sma.rs:46:9: SMA length must be >= 1 EXIT=101
|
||||
$ aura exec blueprints/signal.json --override fast.length=-3
|
||||
thread 'main' panicked at .../alloc/src/raw_vec/mod.rs:28:5: capacity overflow EXIT=101
|
||||
$ aura exec blueprints/signal.json --override c319lab_signal.fast.length=1
|
||||
thread 'main' panicked at crates/aura-cli/src/main.rs:1160:38: override set derived from these exact overrides EXIT=101
|
||||
```
|
||||
|
||||
The same out-of-domain value authored into the blueprint (`c319_2b_zero_bind.ops.json`,
|
||||
`bind {"length":{"I64":0}}`, no `--override` at all) panics identically — so
|
||||
the containment gap is the single-run leg's, not `--override`'s parser.
|
||||
|
||||
Open blueprint, and whether `--override` can close one:
|
||||
|
||||
```
|
||||
$ aura exec blueprints/c319_1b_open.bp.json
|
||||
aura: run requires a closed blueprint (no free parameters); 2 free knob(s) — bind them in the
|
||||
blueprint or vary them as campaign axes; a single bound knob can be overridden with --override
|
||||
EXIT=2
|
||||
$ aura exec blueprints/c319_1b_open.bp.json --override fast.length=3 --override slow.length=8
|
||||
(same message) EXIT=2
|
||||
```
|
||||
|
||||
Tap loop: `--tap spread=record` persisted `runs/traces/c319_spread/spread.json`
|
||||
(11 warm rows) + `index.json` carrying the run manifest; `aura chart c319_spread`
|
||||
emitted HTML. `--tap spread=median` → `aura: unknown fold 'median' — available:
|
||||
count, first, last, max, mean, min, record, sum` (exit 1); `--tap spread` →
|
||||
`aura: --tap expects TAP=FOLD, got "spread"` (exit 2); a tap name the blueprint
|
||||
does not declare → `aura: the tap plan names 'spread', but the blueprint declares
|
||||
no such tap — declared taps: ` (exit 1, trailing empty list).
|
||||
|
||||
A second `exec` of the same blueprint under a different `--override` wrote into
|
||||
the same `runs/traces/c319_spread/` and replaced the first run's series; no
|
||||
note, no second directory.
|
||||
|
||||
## 3 — Campaign leg: axis discovery → running sweep family (axes 2, 3)
|
||||
|
||||
`c319_3_sweep_family.txt`. Register blueprint + process, author the campaign
|
||||
document from `docs/authoring-guide.md` §3, validate (three tiers all green),
|
||||
`aura exec`: 4 members over GER40 2024-09-01…2024-10-01 (1661–2581 trades each)
|
||||
plus a `selection_report`, `aura: campaign run N recorded: 1 cells`, exit 0.
|
||||
|
||||
Campaign-leg overrides (`c319_2_override_family.txt`, second half): a bound
|
||||
param not on an axis is accepted (exit 0); a declared axis collides
|
||||
(`aura: exec: --override \`fast.length\` collides with a declared axis of the
|
||||
campaign; an override overrides a bound value, never an axis`, exit 2); an
|
||||
unknown path and a kind mismatch speak the campaign's referential prose (exit 1);
|
||||
a duplicate path and a non-scalar value speak the shared lexer (exit 2). The
|
||||
retired wrapped form is refused here with the documented translation pointer:
|
||||
|
||||
```
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override c319lab_signal.bias.scale=0.25
|
||||
aura: campaign references do not resolve:
|
||||
strategy 7987bec1…: axis "c319lab_signal.bias.scale" is not in the param space;
|
||||
axis names are raw node.param paths — did you mean "bias.scale"? EXIT=1
|
||||
```
|
||||
|
||||
— the same token that panics on the blueprint leg (see §2).
|
||||
|
||||
## 4 — Target classification (axis 1)
|
||||
|
||||
`c319_2_target_classes.err`, exit codes verified individually:
|
||||
|
||||
| target | stderr | exit |
|
||||
|---|---|---|
|
||||
| file, not JSON | `aura: exec: <path>: target file is not valid JSON: key must be a string at line 1 column 3` | 2 |
|
||||
| op-script array (valid JSON) | `aura: <path>: blueprint document is not valid JSON: invalid type: map, expected u32 at line 2 column 2` | 2 |
|
||||
| `{}` | `aura: <path>: blueprint document is not valid JSON: missing field \`format_version\`` | 2 |
|
||||
| `"kind":"campain"` typo | `aura: campaign document: the "kind" key must be "campaign"` | 1 |
|
||||
| a **process** document | `aura: campaign document: the "kind" key must be "campaign"` | 1 |
|
||||
| missing file / directory / bare name | `aura: '<t>' is neither a readable .json file nor a 64-hex content id` | 1 |
|
||||
| id prefix `3ca5ca90` | same as above | 1 |
|
||||
| campaign *name* | same as above | 1 |
|
||||
| unregistered 64-hex | `aura: no campaign <id> in the project store` | 1 |
|
||||
| a registered **blueprint's** id | `aura: no campaign <id> in the project store` | 1 |
|
||||
| registered campaign id | runs | 0 |
|
||||
|
||||
## 5 — Walk-forward zero-trade note on the campaign path (axis 4)
|
||||
|
||||
`c319_4_walkforward_zero_trade.err`. `fast.length == slow.length == 4` makes the
|
||||
spread identically zero, so the bias never leaves 0:
|
||||
|
||||
```
|
||||
$ aura exec blueprints/c319_5_campaign_wf_flat.json
|
||||
aura: note: all 2 walk-forward windows recorded zero trades
|
||||
aura: campaign run N recorded: 1 cells
|
||||
EXIT=0
|
||||
```
|
||||
|
||||
Class marker, exit code and "a null result is a valid research result" all as
|
||||
C14 describes.
|
||||
|
||||
## 6 — Per-cell fault containment, for contrast
|
||||
|
||||
`c319_4b_campaign_zerolen.json` puts the same out-of-domain `0` on a campaign
|
||||
axis:
|
||||
|
||||
```
|
||||
aura: warning: cell (7987bec1…, GER40, [1725148800000, 1727740800000]) failed at stage 0:
|
||||
a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
aura: campaign run N recorded: 1 cells, 1 failed (GER40: panic)
|
||||
EXIT=3
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"op": "name", "name": "c319_spread"},
|
||||
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 3}}},
|
||||
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 8}}},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
$ aura graph build < ../c319_1_spread_tap.ops.json > blueprints/c319_1_spread_tap.bp.json
|
||||
EXIT=0
|
||||
|
||||
$ aura graph introspect --folds
|
||||
count — number of warm rows (any kind; i64 row); one row at the last warm ts
|
||||
first — first warm value, at its own timestamp (any kind; kind-preserving row)
|
||||
last — last warm value, at its own timestamp (any kind; kind-preserving row)
|
||||
max — maximum of the series (f64 taps; f64 row); one row at the last warm ts
|
||||
mean — arithmetic mean of the series (f64 taps; f64 row); one row at the last warm ts
|
||||
min — minimum of the series (f64 taps; f64 row); one row at the last warm ts
|
||||
record — persist the full series, lossless, at constant memory (any kind)
|
||||
sum — sum of the series (f64 taps; f64 row); one row at the last warm ts
|
||||
|
||||
$ aura exec blueprints/c319_1_spread_tap.bp.json --override fast.length=2 --tap spread=record
|
||||
EXIT=0
|
||||
runs/traces/c319_spread/spread.json:
|
||||
{"tap":"spread","kinds":["F64"],"ts":[8,9,10,11,12,13,14,15,16,17,18],"columns":[[0.002650000000000041,-0.0002249999999999197,-0.0027250000000000885,-0.004237499999999894,-0.004124999999999934,-0.0023
|
||||
|
||||
$ aura exec blueprints/c319_1_spread_tap.bp.json --override fast.length=6 --tap spread=record # second override, same blueprint
|
||||
EXIT=0
|
||||
runs/traces/ now holds:
|
||||
c319_spread
|
||||
runs/traces/c319_spread/spread.json (run A's series is gone):
|
||||
{"tap":"spread","kinds":["F64"],"ts":[8,9,10,11,12,13,14,15,16,17,18],"columns":[[0.001383333333333292,0.0011750000000001481,0.0006749999999999812,-0.00010416666666679397,-0.001091666666666491,-0.0015
|
||||
|
||||
$ aura exec blueprints/c319_1_spread_tap.bp.json --tap spread=median
|
||||
EXIT=1
|
||||
aura: unknown fold 'median' — available: count, first, last, max, mean, min, record, sum
|
||||
|
||||
$ aura exec blueprints/c319_1_spread_tap.bp.json --tap spread
|
||||
EXIT=2
|
||||
aura: --tap expects TAP=FOLD, got "spread"
|
||||
|
||||
$ aura exec blueprints/signal.json --tap spread=record # blueprint declares no taps
|
||||
EXIT=1
|
||||
aura: the tap plan names 'spread', but the blueprint declares no such tap — declared taps:
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"op": "name", "name": "c319_open"},
|
||||
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast"},
|
||||
{"op": "add", "type": "SMA", "name": "slow"},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
== blueprint leg (blueprints/signal.json; fast.length:I64=2 slow.length:I64=4 bias.scale:F64=0.5) ==
|
||||
$ aura exec blueprints/signal.json --override fast.length=8
|
||||
(stdout run record; manifest.params=[['fast.length', {'I64': 8}]])
|
||||
EXIT=0
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length=8 --override bias.scale=0.25
|
||||
(stdout run record; manifest.params=[['fast.length', {'I64': 8}], ['bias.scale', {'F64': 0.25}]])
|
||||
EXIT=0
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length=8 --override fast.length=9
|
||||
aura: exec: --override names path "fast.length" twice
|
||||
EXIT=2
|
||||
|
||||
$ aura exec blueprints/signal.json --override nosuch.param=3
|
||||
aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/signal.json --override bias.scal=0.25
|
||||
aura: axis bias.scal: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length`
|
||||
EXIT=2
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length=1.5
|
||||
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: I64, got: F64 }
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/signal.json --override bias.scale=1
|
||||
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: F64, got: I64 }
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length=abc
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length=abc`
|
||||
EXIT=2
|
||||
|
||||
== campaign leg (blueprints/c319_4_campaign_override.json; declared axes fast.length, slow.length) ==
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=0.25
|
||||
aura: campaign run 1 recorded: 1 cells
|
||||
EXIT=0
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override fast.length=3
|
||||
aura: exec: --override `fast.length` collides with a declared axis of the campaign; an override overrides a bound value, never an axis
|
||||
EXIT=2
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override nosuch.param=3
|
||||
aura: campaign references do not resolve:
|
||||
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "nosuch.param" is not in the param space
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scal=0.25
|
||||
aura: campaign references do not resolve:
|
||||
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "bias.scal" is not in the param space
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=0.25 --override bias.scale=0.75
|
||||
aura: exec: --override names path "bias.scale" twice
|
||||
EXIT=2
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=1
|
||||
aura: campaign references do not resolve:
|
||||
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "bias.scale" declares a kind that is not the param's kind
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override bias.scale=abc
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `bias.scale=abc`
|
||||
EXIT=2
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
$ aura exec blueprints/signal.json --override fast.length=0
|
||||
|
||||
thread 'main' (2726290) panicked at crates/aura-std/src/sma.rs:46:9:
|
||||
SMA length must be >= 1
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
EXIT=101
|
||||
|
||||
$ aura exec blueprints/signal.json --override fast.length=-3
|
||||
|
||||
thread 'main' (2726295) panicked at /rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/alloc/src/raw_vec/mod.rs:28:5:
|
||||
capacity overflow
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
EXIT=101
|
||||
|
||||
$ aura exec blueprints/c319_4b_campaign_zerolen.json # same value via a campaign axis
|
||||
aura: warning: cell (7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911, GER40, [1725148800000, 1727740800000]) failed at stage 0: a member panicked: SMA length must be >= 1 — recorded, campaign continues
|
||||
aura: campaign run 1 recorded: 1 cells, 1 failed (GER40: panic)
|
||||
EXIT=3
|
||||
@@ -0,0 +1,44 @@
|
||||
$ aura exec ../c319_2_targets_notjson.json
|
||||
aura: exec: ../c319_2_targets_notjson.json: target file is not valid JSON: key must be a string at line 1 column 3
|
||||
EXIT=2
|
||||
|
||||
$ aura exec ../c319_2_targets_opscript.json
|
||||
aura: ../c319_2_targets_opscript.json: blueprint document is not valid JSON: invalid type: map, expected u32 at line 2 column 2
|
||||
EXIT=2
|
||||
|
||||
$ aura exec ../c319_2_targets_emptyobj.json
|
||||
aura: ../c319_2_targets_emptyobj.json: blueprint document is not valid JSON: missing field `format_version` at line 1 column 2
|
||||
EXIT=2
|
||||
|
||||
$ aura exec ../c319_2_targets_kindtypo.json
|
||||
aura: campaign document: the "kind" key must be "campaign"
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/c319_5_process_wf.json
|
||||
aura: campaign document: the "kind" key must be "campaign"
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/nosuch.json
|
||||
aura: 'blueprints/nosuch.json' is neither a readable .json file nor a 64-hex content id
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints
|
||||
aura: 'blueprints' is neither a readable .json file nor a 64-hex content id
|
||||
EXIT=1
|
||||
|
||||
$ aura exec 3ca5ca90
|
||||
aura: '3ca5ca90' is neither a readable .json file nor a 64-hex content id
|
||||
EXIT=1
|
||||
|
||||
$ aura exec c319-ger40-smacross-sweep
|
||||
aura: 'c319-ger40-smacross-sweep' is neither a readable .json file nor a 64-hex content id
|
||||
EXIT=1
|
||||
|
||||
$ aura exec 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911
|
||||
aura: no campaign 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911 in the project store
|
||||
EXIT=1
|
||||
|
||||
$ aura exec blueprints/c319_1b_open.bp.json
|
||||
aura: run requires a closed blueprint (no free parameters); 2 free knob(s) — bind them in the blueprint or vary them as campaign axes; a single bound knob can be overridden with --override
|
||||
EXIT=2
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version": 1, "kind": "campain", "name": "typo-kind"}
|
||||
@@ -0,0 +1 @@
|
||||
{ this is not json at all,
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{"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": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
$ aura exec blueprints/signal.json --override c319lab_signal.bias.scale=0.25
|
||||
|
||||
thread 'main' (2726331) panicked at crates/aura-cli/src/main.rs:1160:38:
|
||||
override set derived from these exact overrides
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
EXIT=101
|
||||
|
||||
$ aura exec blueprints/c319_4_campaign_override.json --override c319lab_signal.bias.scale=0.25 # same token, campaign leg
|
||||
aura: campaign references do not resolve:
|
||||
strategy 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911: axis "c319lab_signal.bias.scale" is not in the param space; axis names are raw node.param paths — did you mean "bias.scale"?
|
||||
EXIT=1
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"op": "name", "name": "c319_zero"},
|
||||
{"op": "doc", "text": "fast/slow SMA spread, tapped before the bias clamp"},
|
||||
{"op": "source", "role": "price", "kind": "F64"},
|
||||
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 0}}},
|
||||
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 8}}},
|
||||
{"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]},
|
||||
{"op": "add", "type": "Sub", "name": "sub"},
|
||||
{"op": "connect", "from": "fast.value", "to": "sub.lhs"},
|
||||
{"op": "connect", "from": "slow.value", "to": "sub.rhs"},
|
||||
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}},
|
||||
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
|
||||
{"op": "tap", "from": "sub.value", "as": "spread"},
|
||||
{"op": "expose", "from": "bias.bias", "as": "bias"}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
$ aura run blueprints/signal.json
|
||||
error: unrecognized subcommand 'run'
|
||||
|
||||
tip: a similar subcommand exists: 'runs'
|
||||
|
||||
EXIT=2
|
||||
|
||||
$ aura sweep blueprints/signal.json
|
||||
error: unrecognized subcommand 'sweep'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
EXIT=2
|
||||
|
||||
$ aura walkforward blueprints/signal.json
|
||||
error: unrecognized subcommand 'walkforward'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
EXIT=2
|
||||
|
||||
$ aura mc blueprints/signal.json
|
||||
error: unrecognized subcommand 'mc'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
EXIT=2
|
||||
|
||||
$ aura generalize blueprints/signal.json
|
||||
error: unrecognized subcommand 'generalize'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
EXIT=2
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
$ aura graph introspect --params blueprints/signal.json
|
||||
fast.length:I64 default=2
|
||||
slow.length:I64 default=4
|
||||
bias.scale:F64 default=0.5
|
||||
|
||||
$ aura graph register blueprints/signal.json
|
||||
registered blueprint 7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911 (/home/brummel/dev/aura/.claude/worktrees/issue-319-sugar-retirement/fieldtests/cycle-319-sugar-retirement/c319lab/runs/blueprints/7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911.json)
|
||||
|
||||
$ aura process register blueprints/c319_3_process_sweep.json
|
||||
registered process 954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b (/home/brummel/dev/aura/.claude/worktrees/issue-319-sugar-retirement/fieldtests/cycle-319-sugar-retirement/c319lab/runs/processes/954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b.json)
|
||||
|
||||
$ aura campaign validate blueprints/c319_3_campaign_sweep.json
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
|
||||
campaign document valid (referential): all references resolve, axes are in the param space
|
||||
campaign document valid (executable): pipeline shape and static guards pass
|
||||
|
||||
$ aura exec blueprints/c319_3_campaign_sweep.json # stderr + member summary
|
||||
aura: campaign run 2 recorded: 1 cells
|
||||
member {'fast.length': 2, 'slow.length': 16} n_trades=2581 exp_r=0.0019 sqn=0.0515
|
||||
member {'fast.length': 2, 'slow.length': 32} n_trades=1869 exp_r=-0.0433 sqn=-0.8164
|
||||
member {'fast.length': 4, 'slow.length': 16} n_trades=2388 exp_r=-0.0219 sqn=-0.5449
|
||||
member {'fast.length': 4, 'slow.length': 32} n_trades=1661 exp_r=-0.0132 sqn=-0.2080
|
||||
selection: std::sweep winner_ordinal=0 [['bias.scale', {'F64': 0.5}], ['fast.length', {'I64': 2}], ['slow.length', {'I64': 16}]]
|
||||
EXIT=0
|
||||
@@ -0,0 +1,4 @@
|
||||
$ aura exec blueprints/c319_5_campaign_wf_flat.json
|
||||
aura: note: all 2 walk-forward windows recorded zero trades
|
||||
aura: campaign run 1 recorded: 1 cells
|
||||
EXIT=0
|
||||
@@ -0,0 +1 @@
|
||||
/runs
|
||||
@@ -0,0 +1,4 @@
|
||||
# Static project context only (C17); paths only.
|
||||
[paths]
|
||||
runs = "runs"
|
||||
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
|
||||
@@ -0,0 +1,24 @@
|
||||
# c319lab — an aura research project (data-only)
|
||||
|
||||
This directory is an aura project: blueprints + research documents over the
|
||||
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
|
||||
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
|
||||
— the starter is closed; all params bound; bound values are defaults —
|
||||
`--override NODE.PARAM=VALUE` may override one for this run (#246))
|
||||
- Campaign: `aura exec <campaign.json>` executes a registered campaign
|
||||
document (file or content id) — the multi-cell/axis surface
|
||||
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
|
||||
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
||||
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
||||
continuously-tracked target position; a protective stop defines the risk
|
||||
unit R, and quality metrics are R-based. Entry signals become held state
|
||||
via the signal-side latch/edge-pulse idiom (see
|
||||
`aura graph introspect --vocabulary`).
|
||||
- Data plane: process/campaign documents are authored directly with
|
||||
`aura process` / `aura campaign`, growing one from a bare `{}` via
|
||||
`aura campaign introspect --unwired`.
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"c319_spread","doc":"fast/slow SMA spread, tapped before the bias clamp","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":3}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"c319_open","doc":"fast/slow SMA spread, tapped before the bias clamp","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"c319_zero","doc":"fast/slow SMA spread, tapped before the bias clamp","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":0}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":8}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "c319-ger40-smacross-sweep",
|
||||
"seed": 42,
|
||||
"data": {
|
||||
"instruments": ["GER40"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
|
||||
"axes": {
|
||||
"fast.length": { "kind": "I64", "values": [2, 4] },
|
||||
"slow.length": { "kind": "I64", "values": [16, 32] },
|
||||
"bias.scale": { "kind": "F64", "values": [0.5] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "c319-sweep-argmax",
|
||||
"description": "Plain axis sweep, best member by SQN.",
|
||||
"pipeline": [
|
||||
{ "block": "std::sweep", "metric": "sqn", "select": "argmax" }
|
||||
]
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "c319-ger40-smacross-sweep-2axes",
|
||||
"seed": 42,
|
||||
"data": {
|
||||
"instruments": ["GER40"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
|
||||
"axes": {
|
||||
"fast.length": { "kind": "I64", "values": [2, 4] },
|
||||
"slow.length": { "kind": "I64", "values": [16, 32] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "c319-zero-length-axis",
|
||||
"seed": 42,
|
||||
"data": {
|
||||
"instruments": ["GER40"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
|
||||
"axes": {
|
||||
"fast.length": { "kind": "I64", "values": [0] },
|
||||
"slow.length": { "kind": "I64", "values": [16, 32] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "954e735fc824ac74cceaaa6cbef8f8706a2fdaef3979ffb53cab854f0e3ca43b" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "c319-ger40-flat-walkforward",
|
||||
"seed": 42,
|
||||
"data": {
|
||||
"instruments": ["GER40"],
|
||||
"windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": { "content_id": "7987bec19abfaccf8b98075511a05edcca5edce86755b781d3328cd75a7b4911" },
|
||||
"axes": {
|
||||
"fast.length": { "kind": "I64", "values": [4] },
|
||||
"slow.length": { "kind": "I64", "values": [4] }
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": { "ref": { "content_id": "a7220282128b56f53b75cf54c586a2ade11fd6a81d1638aeb3fb844fd0f51e3f" } },
|
||||
"presentation": { "persist_taps": [], "emit": ["family_table", "selection_report"] }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "c319-sweep-then-walkforward",
|
||||
"description": "Sweep for a nominee, then walk it forward out of sample.",
|
||||
"pipeline": [
|
||||
{ "block": "std::sweep", "metric": "sqn", "select": "argmax" },
|
||||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, "step_ms": 604800000, "mode": "rolling", "metric": "sqn", "select": "argmax" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"c319lab_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,280 @@
|
||||
# Fieldtest transcript — milestone 42 (sugar retirement, document-first authoring)
|
||||
|
||||
Binary: `target/release/aura` built from the working tree at HEAD `2f1bace`
|
||||
(`cargo build --release -p aura-cli`, exit 0). Every command below was issued
|
||||
through that binary; no implementation source was read.
|
||||
|
||||
## Persona
|
||||
|
||||
A downstream consumer with no prior aura knowledge, holding only the CLI's own
|
||||
help, the scaffolded project's `CLAUDE.md`, and (from scenario 2 on) the public
|
||||
README / authoring guide. Scenario 2 additionally carries sugar-era muscle
|
||||
memory: `run` / `sweep` / `walkforward` / `mc` / `generalize`.
|
||||
|
||||
## Files
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `coldstart-lab/` | scenario 1 — scaffolded by `aura new`, then grown to a 4-cell selecting sweep |
|
||||
| `coldstart-lab/documents/cold-process.json` | the process document authored from the `--unwired` ladder |
|
||||
| `coldstart-lab/documents/cold-campaign.json` | the campaign document authored from the `--unwired` ladder |
|
||||
| `spine-lab/` | scenario 3 — scaffolded, plus an op-script-built tapped blueprint |
|
||||
| `spine-lab/blueprints/m42_trend.ops.json` | the op-script (canonical authoring form) |
|
||||
| `spine-lab/blueprints/m42_trend.json` | `aura graph build` output, registered as label `m42_trend` |
|
||||
| `spine-lab/documents/spine-process.json` | grid → walk_forward → monte_carlo → generalize |
|
||||
| `spine-lab/documents/spine-campaign.json` | GER40 + USDJPY, 2 years, 6 axis points |
|
||||
| `logs/m42_1_unwired_ladder.sh` + `.txt` | replayable repro of the `--unwired` blind spot |
|
||||
| `logs/m42_2_retired_verbs.txt` | the five retired verbs + two retired flags, verbatim |
|
||||
| `logs/m42_3_spine.txt` | the spine run, reproduce, chart, tap refusals |
|
||||
| `logs/m42_3b_override_family.txt` | the whole `--override` refusal family, both legs |
|
||||
|
||||
`runs/` under each lab is gitignored by the scaffolder.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 1 — COLD START (help + scaffolded docs only)
|
||||
|
||||
### To a first successful single run — 4 commands, 0 dead ends
|
||||
|
||||
```
|
||||
1 $ aura --help
|
||||
→ about-text names the two layers, `exec` as the one executor, the
|
||||
document data plane (`aura process` / `aura campaign`), the
|
||||
`introspect --unwired` growth idiom, and `graph introspect --params`
|
||||
2 $ aura new --help
|
||||
3 $ aura new coldstart-lab
|
||||
created project "coldstart-lab" (data-only; attach native nodes later …)
|
||||
4 $ cat coldstart-lab/CLAUDE.md (scaffolded; 5 bullets, all correct)
|
||||
5 $ aura exec blueprints/signal.json
|
||||
{"manifest":{…,"topology_hash":"0d3c6940…"},"metrics":{…,"expectancy_r":1.271…}}
|
||||
EXIT=0
|
||||
```
|
||||
|
||||
Clean. The scaffolded `CLAUDE.md`'s first bullet is literally the command that
|
||||
works.
|
||||
|
||||
### To a first multi-cell family with a selection result — 24 commands, 5 dead ends
|
||||
|
||||
Discovery (7 commands, all productive):
|
||||
|
||||
```
|
||||
6 $ aura campaign --help
|
||||
7 $ echo '{}' > bare.json; aura campaign introspect --unwired bare.json
|
||||
8 $ aura process introspect --unwired bare.json
|
||||
9 $ aura process introspect --help
|
||||
10 $ aura process introspect --vocabulary → the 6 stage blocks + meanings
|
||||
11 $ aura process introspect --block std::sweep → metric | select | deflate
|
||||
12 $ aura graph introspect --params blueprints/signal.json
|
||||
fast.length:I64 default=2 / slow.length:I64 default=4 / bias.scale:F64 default=0.5
|
||||
13 $ aura data list → 30+ symbols incl. GER40
|
||||
14 $ aura data info GER40
|
||||
{"symbol":"GER40","digits":1,"pipSize":1.0,…} ← geometry only, no time coverage
|
||||
15 $ aura graph register blueprints/signal.json → 0d3c6940…
|
||||
16 $ aura process introspect --metrics → rankable | gate | generalize tags
|
||||
```
|
||||
|
||||
Growing the documents, with the refusals verbatim:
|
||||
|
||||
```
|
||||
17 $ aura process validate documents/cold-process.json # pipeline: [{"std::sweep":{…}}]
|
||||
aura: process document: a pipeline stage must carry a string "block" id EXIT=1
|
||||
← DEAD END 1. Neither `--unwired` ("pipeline (required, list of stage
|
||||
blocks)") nor `--block std::sweep` shows a stage's literal JSON shape.
|
||||
The refusal itself is precise and fixed it in one edit.
|
||||
|
||||
18 $ aura process validate (pipeline: [{"block":"std::sweep",…}]) EXIT=0
|
||||
19 $ aura process register documents/cold-process.json → 26079ba0…
|
||||
|
||||
20 $ aura campaign validate documents/cold-campaign.json # presentation: {}
|
||||
aura: campaign document: missing field `emit` EXIT=1
|
||||
← DEAD END 2. `campaign introspect --unwired` on this same file had
|
||||
reported no open slot for presentation at all (see logs/m42_1_*).
|
||||
|
||||
21 $ … "emit": "summary"
|
||||
aura: campaign document: invalid type: string "summary", expected a sequence EXIT=1
|
||||
← DEAD END 3 (the refusal names no legal value)
|
||||
|
||||
22 $ … "emit": []
|
||||
aura: campaign document: unknown variant `list`, expected one of `I64`, `F64`, `Bool`, `Timestamp`
|
||||
← DEAD END 4 — the axes `kind` slot; `--unwired` says only
|
||||
"axes (required, map: param name -> { kind, values })". EXIT=1
|
||||
|
||||
23 $ … axes kind → "I64", emit → ["nonsense"]
|
||||
aura: campaign document invalid:
|
||||
presentation.emit: unknown kind "nonsense" EXIT=1
|
||||
← DEAD END 5 (still no enumeration of the legal kinds)
|
||||
|
||||
24 $ aura campaign introspect --vocabulary → std::presentation exists
|
||||
25 $ aura campaign introspect --block std::presentation
|
||||
persist_taps required, list of: equity | exposure | r_equity | net_r_equity
|
||||
emit required, list of: family_table | selection_report ← the answer
|
||||
26 $ aura campaign validate documents/cold-campaign.json
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 2 axes (4 points), … 1 cell(s)
|
||||
campaign document valid (referential): all references resolve, axes are in the param space
|
||||
campaign document valid (executable): pipeline shape and static guards pass EXIT=0
|
||||
27 $ aura campaign register documents/cold-campaign.json → dbf47283…
|
||||
28 $ aura exec documents/cold-campaign.json
|
||||
4 × {"family_id":"dbf47283-0-GER40-w0-r0-s0-0","report":{…}}
|
||||
{"selection_report":{…,"winner_ordinal":2,"params":[["fast.length",{"I64":3}],
|
||||
["slow.length",{"I64":8}]],"selection":{"selection_metric":"expectancy_r",
|
||||
"n_trials":4,"raw_winner_metric":0.008618…,"mode":"Argmax"}}}
|
||||
{"campaign_run":{…,"coverage":{"effective_from_ms":1704154499999,
|
||||
"effective_to_ms":1711659420000}}}
|
||||
aura: campaign run 0 recorded: 1 cells EXIT=0
|
||||
```
|
||||
|
||||
The guessed `from_ms`/`to_ms` (2024-01-01 … 2024-04-01) clipped silently and
|
||||
honestly to the archive's real coverage, which the `campaign_run` record then
|
||||
reports back as `coverage.effective_*`. There is no *pre*-run way to learn a
|
||||
symbol's time coverage: `aura data info` returns geometry only.
|
||||
|
||||
**Cold start total: 28 commands, 5 dead ends, all resolved from the surface.**
|
||||
|
||||
---
|
||||
|
||||
## Scenario 2 — RETURNING SUGAR USER
|
||||
|
||||
Full log: `logs/m42_2_retired_verbs.txt`. Condensed:
|
||||
|
||||
```
|
||||
$ aura run blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'run'
|
||||
tip: a similar subcommand exists: 'runs' EXIT=2
|
||||
|
||||
$ aura runs blueprints/m42_trend.json # following the only tip offered
|
||||
error: unrecognized subcommand 'blueprints/m42_trend.json' EXIT=2
|
||||
|
||||
$ aura sweep / walkforward / mc / generalize <bp>
|
||||
error: unrecognized subcommand '<verb>' # no tip, no retirement notice EXIT=2
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --sweep fast.length=2,3
|
||||
error: unexpected argument '--sweep' found
|
||||
tip: to pass '--sweep' as a value, use '-- --sweep' EXIT=2
|
||||
|
||||
$ aura graph introspect --list-axes blueprints/m42_trend.json
|
||||
error: unexpected argument '--list-axes' found EXIT=2
|
||||
```
|
||||
|
||||
Not one of the seven refusals names `exec`, a campaign document, or the fact
|
||||
that the verb was retired. Two of them actively point the wrong way (`runs`;
|
||||
`-- --sweep`). Re-routing costs exactly one extra command — `aura --help`,
|
||||
whose about-text is genuinely excellent — so the honest cost is **2 commands
|
||||
to re-orient, 0 commands of it attributable to the refusal**.
|
||||
|
||||
---
|
||||
|
||||
## Scenario 3 — VALIDATION SPINE (real data, document-first)
|
||||
|
||||
Full log: `logs/m42_3_spine.txt`; override family: `logs/m42_3b_override_family.txt`.
|
||||
|
||||
Authoring a tapped blueprint in the canonical op-script form worked first try:
|
||||
|
||||
```
|
||||
$ aura graph build --help # embeds the full op-list reference, incl. {"op":"tap",…}
|
||||
$ aura graph introspect --vocabulary ; --node SMA|Sub|Sign|Bias
|
||||
$ aura graph build < blueprints/m42_trend.ops.json > blueprints/m42_trend.json EXIT=0
|
||||
$ aura graph register blueprints/m42_trend.json --name m42_trend
|
||||
registered blueprint b657c714… ; label "m42_trend" -> b657c714…
|
||||
$ aura graph introspect --registered
|
||||
m42_trend b657c714… fast/slow SMA crossover direction, clamped into the bias contract
|
||||
```
|
||||
|
||||
`aura graph build` is reachable only from `aura graph --help` / the README —
|
||||
the scaffolded project `CLAUDE.md` never mentions how to author a *new*
|
||||
blueprint.
|
||||
|
||||
The methodology, as a process document (one dead end):
|
||||
|
||||
```
|
||||
$ aura campaign validate documents/spine-campaign.json # pipeline had a std::gate after walk_forward
|
||||
aura: campaign is not executable:
|
||||
process pipeline is not executable: stage 2: std::gate cannot follow std::walk_forward EXIT=1
|
||||
```
|
||||
|
||||
Precise (names the stage index and the illegal adjacency); it does not say what
|
||||
*may* follow. Dropping the gate:
|
||||
|
||||
```
|
||||
$ aura campaign validate documents/spine-campaign.json # 3 tiers green EXIT=0
|
||||
$ aura campaign register documents/spine-campaign.json → 0959eda8…
|
||||
$ aura exec documents/spine-campaign.json EXIT=0
|
||||
(42 member reports on stdout; 2.4 s wall for 2 instruments × 2 years × 21 WF windows)
|
||||
aura: campaign run 0 recorded: 2 cells
|
||||
aura: note: tap "net_r_equity" needs a cost model; add a cost block to the campaign document; skipped
|
||||
aura: traces persisted: 0959eda8-0 (1 tap(s) x 2 cell(s))
|
||||
```
|
||||
|
||||
The `campaign_run` record carries the whole retired research scope:
|
||||
|
||||
```
|
||||
cell GER40 stages [std::grid, std::walk_forward, std::monte_carlo]
|
||||
pooled_oos mean=-0.004729 p5=-0.0469 p95=0.0381 prob_le_zero=0.59 n=21910
|
||||
cell USDJPY stages [std::grid, std::walk_forward, std::monte_carlo]
|
||||
pooled_oos mean=-0.033809 p5=-0.0766 p95=0.0177 prob_le_zero=0.885 n=25258
|
||||
generalize worst_case=-0.251491 sign_agreement=0
|
||||
per_instrument=[GER40 -0.2515, USDJPY -0.0935] + per-instrument winners
|
||||
```
|
||||
|
||||
`aura runs family <id> rank expectancy_r` shows every walk-forward window with
|
||||
`deflated_score` / `overfit_probability` alongside; `aura reproduce
|
||||
0959eda8-0-GER40-w0-r0-s1-0` reports all 21 members **bit-identical**.
|
||||
|
||||
`presentation.emit` asked for `family_table` **and** `selection_report`; the run
|
||||
emitted 42 family-table lines and **zero** selection-report lines, with no note
|
||||
— contrast the `net_r_equity` tap, which announced its own skip.
|
||||
|
||||
### The `--override` what-if
|
||||
|
||||
```
|
||||
$ aura exec documents/spine-campaign.json --override bias.scale=0.25 EXIT=0
|
||||
manifest.params gains ["bias.scale",{"F64":0.25}]; manifest.defaults empties;
|
||||
topology_hash stays b657c714… (the registered blueprint's id)
|
||||
every R metric bit-identical to the baseline — correct: research runs flat-1R,
|
||||
only the bias *sign* gates firing (invariant 7).
|
||||
```
|
||||
|
||||
Same operation on the blueprint leg does **not** keep the id:
|
||||
|
||||
```
|
||||
$ aura exec blueprints/m42_trend.json → b657c714…
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=10 → f952a1c8…
|
||||
^ a NO-OP override: 10 is the bound default
|
||||
$ aura reproduce f952a1c8…
|
||||
aura: no such family 'f952a1c8519a2dbd…'
|
||||
```
|
||||
|
||||
### The `--override` refusal family (blueprint leg)
|
||||
|
||||
```
|
||||
--override fast.length EXIT=2 aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length`
|
||||
--override fast.length=notanumber EXIT=2 (same, good)
|
||||
--override nosuch.param=1 EXIT=1 aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
|
||||
--override fast.length=0 EXIT=1 aura: SMA length must be >= 1 ← the #319 fix, working
|
||||
--override fast.length=-1 EXIT=1 aura: capacity overflow ← the same fix, not working
|
||||
--override fast.length=-5 EXIT=1 aura: capacity overflow
|
||||
--override bias.scale=2 EXIT=1 aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: F64, got: I64 }
|
||||
--override bias.scale=2.0 EXIT=0
|
||||
--override bias.scale=-5.0 EXIT=1 aura: Bias scale must be > 0
|
||||
```
|
||||
|
||||
and on the campaign leg for the same inputs:
|
||||
|
||||
```
|
||||
--override fast.length=7 EXIT=2 aura: exec: --override `fast.length` collides with a declared axis of the campaign; an override overrides a bound value, never an axis
|
||||
--override stop_k=1.0 EXIT=1 aura: campaign references do not resolve:
|
||||
strategy b657c714…: axis "stop_k" is not in the param space
|
||||
--override bias.scale=-5 EXIT=1 aura: campaign references do not resolve:
|
||||
strategy b657c714…: axis "bias.scale" declares a kind that is not the param's kind
|
||||
```
|
||||
|
||||
### Traces
|
||||
|
||||
```
|
||||
$ aura chart 0959eda8-0 EXIT=1 aura: no family member has a tap named 'equity'
|
||||
$ aura chart 0959eda8-0 --tap r_equity EXIT=0 (298 KB self-contained HTML on stdout)
|
||||
$ aura chart b657c714-GER40-w0 EXIT=1 aura: no recorded run or family 'b657c714-GER40-w0'
|
||||
under runs/traces (check the handle a sweep/walk-forward/campaign run printed for a
|
||||
typo — re-running with this handle as `--trace` will not create it)
|
||||
$ aura exec blueprints/signal.json --tap equity=record
|
||||
EXIT=1 aura: the tap plan names 'equity', but the blueprint declares no such tap — declared taps:
|
||||
```
|
||||
@@ -0,0 +1 @@
|
||||
/runs
|
||||
@@ -0,0 +1,4 @@
|
||||
# Static project context only (C17); paths only.
|
||||
[paths]
|
||||
runs = "runs"
|
||||
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
|
||||
@@ -0,0 +1,24 @@
|
||||
# coldstart-lab — an aura research project (data-only)
|
||||
|
||||
This directory is an aura project: blueprints + research documents over the
|
||||
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
|
||||
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
|
||||
— the starter is closed; all params bound; bound values are defaults —
|
||||
`--override NODE.PARAM=VALUE` may override one for this run (#246))
|
||||
- Campaign: `aura exec <campaign.json>` executes a registered campaign
|
||||
document (file or content id) — the multi-cell/axis surface
|
||||
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
|
||||
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
||||
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
||||
continuously-tracked target position; a protective stop defines the risk
|
||||
unit R, and quality metrics are R-based. Entry signals become held state
|
||||
via the signal-side latch/edge-pulse idiom (see
|
||||
`aura graph introspect --vocabulary`).
|
||||
- Data plane: process/campaign documents are authored directly with
|
||||
`aura process` / `aura campaign`, growing one from a bare `{}` via
|
||||
`aura campaign introspect --unwired`.
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"coldstart_lab_signal","doc":"fast/slow SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "campaign",
|
||||
"name": "cold-start-sweep",
|
||||
"description": "Cold-start 4-cell sweep over the starter blueprint's two lengths on GER40.",
|
||||
"data": {
|
||||
"instruments": [
|
||||
"GER40"
|
||||
],
|
||||
"windows": [
|
||||
{
|
||||
"from_ms": 1704067200000,
|
||||
"to_ms": 1711929600000
|
||||
}
|
||||
]
|
||||
},
|
||||
"strategies": [
|
||||
{
|
||||
"ref": {
|
||||
"content_id": "0d3c69401c8e6c9e56661fce348a4c288325a39b7167d7794b30b5141930ebf1"
|
||||
},
|
||||
"axes": {
|
||||
"fast.length": {
|
||||
"kind": "I64",
|
||||
"values": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"slow.length": {
|
||||
"kind": "I64",
|
||||
"values": [
|
||||
8,
|
||||
16
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"process": {
|
||||
"ref": {
|
||||
"content_id": "26079ba0317aba8f07740f0fb0616ffed683467618925a3b483ba01d06f74a1f"
|
||||
}
|
||||
},
|
||||
"seed": 0,
|
||||
"presentation": {
|
||||
"persist_taps": [],
|
||||
"emit": [
|
||||
"family_table",
|
||||
"selection_report"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"kind": "process",
|
||||
"name": "cold-start-select",
|
||||
"description": "Sweep the axes space and pick the argmax expectancy_r member.",
|
||||
"pipeline": [
|
||||
{ "block": "std::sweep", "metric": "expectancy_r", "select": "argmax" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/sh
|
||||
# m42_1 — the --unwired ladder's presentation blind spot, minimal replay.
|
||||
# Run from inside any scaffolded project. AURA=<path to the release binary>.
|
||||
set -u
|
||||
: "${AURA:=aura}"
|
||||
t=$(mktemp -d)
|
||||
|
||||
echo '{}' > "$t/a.json"
|
||||
echo '$ aura campaign introspect --unwired a.json # a.json = {}'
|
||||
"$AURA" campaign introspect --unwired "$t/a.json" | grep -E "^open slot: (data|presentation)"
|
||||
|
||||
cat > "$t/b.json" <<'J'
|
||||
{"format_version":1,"kind":"campaign","name":"x","seed":0,
|
||||
"data":{"instruments":["GER40"],"windows":[{"from_ms":1,"to_ms":2}]},
|
||||
"strategies":[{"ref":{"content_id":"0"},"axes":{}}],
|
||||
"process":{"ref":{"content_id":"0"}},
|
||||
"presentation":{}}
|
||||
J
|
||||
echo
|
||||
echo '$ aura campaign introspect --unwired b.json # data filled, presentation left {}'
|
||||
"$AURA" campaign introspect --unwired "$t/b.json" | grep -E "^open slot:"
|
||||
echo ' ^ only OPTIONAL slots are listed. The two REQUIRED sub-slots of an empty
|
||||
presentation section (persist_taps, emit) are never named — contrast an empty
|
||||
data section, which drills down to instruments/windows (see above).'
|
||||
|
||||
echo
|
||||
echo '$ aura campaign validate b.json # the slot the ladder never named'
|
||||
"$AURA" campaign validate "$t/b.json" 2>&1 | head -2
|
||||
|
||||
echo
|
||||
echo '$ aura campaign introspect --block std::presentation # where the answer actually lives'
|
||||
"$AURA" campaign introspect --block std::presentation
|
||||
rm -rf "$t"
|
||||
@@ -0,0 +1,23 @@
|
||||
$ aura campaign introspect --unwired a.json # a.json = {}
|
||||
open slot: data (required section: instruments + windows)
|
||||
open slot: presentation (required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit)
|
||||
|
||||
$ aura campaign introspect --unwired b.json # data filled, presentation left {}
|
||||
open slot: description (optional, string — a one-line meaning (C29-gated when present))
|
||||
open slot: risk (optional, list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime)
|
||||
open slot: cost (optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross))
|
||||
^ only OPTIONAL slots are listed. The two REQUIRED sub-slots of an empty
|
||||
presentation section (persist_taps, emit) are never named — contrast an empty
|
||||
data section, which drills down to instruments/windows (see above).
|
||||
|
||||
$ aura campaign validate b.json # the slot the ladder never named
|
||||
aura: campaign document: missing field `persist_taps`
|
||||
|
||||
$ aura campaign introspect --block std::presentation # where the answer actually lives
|
||||
std::presentation — campaign section: taps to persist + tables to emit (data-level only)
|
||||
persist_taps required, list of: equity | exposure | r_equity | net_r_equity
|
||||
equity cumulative pip equity per cycle
|
||||
exposure signed position exposure per cycle
|
||||
r_equity cumulative gross R per cycle
|
||||
net_r_equity cumulative net R per cycle, after the cost model
|
||||
emit required, list of: family_table | selection_report
|
||||
@@ -0,0 +1,69 @@
|
||||
# m42_2 — returning sugar user: the five retired verbs, verbatim (release binary @ 2f1bace)
|
||||
|
||||
$ aura run blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'run'
|
||||
|
||||
tip: a similar subcommand exists: 'runs'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura sweep blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'sweep'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura walkforward blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'walkforward'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura mc blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'mc'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura generalize blueprints/m42_trend.json
|
||||
error: unrecognized subcommand 'generalize'
|
||||
|
||||
Usage: aura [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura runs blueprints/m42_trend.json # following the only tip offered
|
||||
error: unrecognized subcommand 'blueprints/m42_trend.json'
|
||||
|
||||
Usage: aura runs [OPTIONS] <COMMAND>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --sweep fast.length=2,3 # sugar-era flag muscle memory
|
||||
error: unexpected argument '--sweep' found
|
||||
|
||||
tip: to pass '--sweep' as a value, use '-- --sweep'
|
||||
|
||||
Usage: aura exec <TARGET>
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
|
||||
$ aura graph introspect --list-axes blueprints/m42_trend.json # retired discovery flag
|
||||
error: unexpected argument '--list-axes' found
|
||||
|
||||
Usage: aura graph introspect [OPTIONS]
|
||||
|
||||
For more information, try '--help'.
|
||||
EXIT=2
|
||||
@@ -0,0 +1,48 @@
|
||||
# m42_3 — the validation spine document-first, GER40 + USDJPY, 2023-01-01..2024-12-31
|
||||
# process pipeline: std::grid -> std::walk_forward(90/30/30 rolling, argmax expectancy_r) -> std::monte_carlo(200x5) -> std::generalize
|
||||
|
||||
$ aura campaign validate documents/spine-campaign.json
|
||||
campaign document valid (intrinsic): 1 strategy(ies), 2 axes (6 points), 2 instrument(s), 1 window(s), 1 regime(s) (default) — 2 cell(s)
|
||||
campaign document valid (referential): all references resolve, axes are in the param space
|
||||
campaign document valid (executable): pipeline shape and static guards pass
|
||||
EXIT=0
|
||||
|
||||
$ aura exec documents/spine-campaign.json # stdout: 42 member reports + campaign_run; stderr below
|
||||
EXIT=0
|
||||
aura: campaign run 2 recorded: 2 cells
|
||||
aura: note: tap "net_r_equity" needs a cost model; add a cost block to the campaign document; skipped
|
||||
aura: traces persisted: 0959eda8-2 (1 tap(s) x 2 cell(s))
|
||||
stdout lines: 43
|
||||
|
||||
# campaign_run record, trimmed to the validation payload:
|
||||
cell GER40 stages ['std::grid', 'std::walk_forward', 'std::monte_carlo']
|
||||
monte_carlo pooled_oos: mean=-0.004729 p5=-0.0469 p95=0.0381 prob_le_zero=0.59 n_trades=21910
|
||||
coverage {'effective_from_ms': 1672618499999, 'effective_to_ms': 1735592220000}
|
||||
cell USDJPY stages ['std::grid', 'std::walk_forward', 'std::monte_carlo']
|
||||
monte_carlo pooled_oos: mean=-0.033809 p5=-0.0766 p95=0.0177 prob_le_zero=0.885 n_trades=25258
|
||||
coverage {'effective_from_ms': 1672617660000, 'effective_to_ms': 1735603200000}
|
||||
generalize: metric=expectancy_r n_instruments=2 worst_case=-0.251491 sign_agreement=0 per_instrument=[['GER40', -0.2514905108214054], ['USDJPY', -0.09353171411968528]]
|
||||
NOTE: presentation.emit asked for family_table AND selection_report;
|
||||
stdout carried 42 family_table lines and ZERO selection_report lines, silently.
|
||||
|
||||
$ aura runs families
|
||||
{"family_id":"0959eda8-0-GER40-w0-r0-s1-0","kind":"WalkForward","members":21}
|
||||
{"family_id":"0959eda8-0-USDJPY-w0-r0-s1-0","kind":"WalkForward","members":21}
|
||||
|
||||
$ aura reproduce 0959eda8-0-GER40-w0-r0-s1-0 (first 2 of 21 members)
|
||||
0959eda8-0-GER40-w0-r0-s1-0 member fast.length=10, slow.length=100, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
0959eda8-0-GER40-w0-r0-s1-0 member fast.length=10, slow.length=50, stop_length=3, stop_k=2 reproduced: bit-identical
|
||||
|
||||
$ aura chart 0959eda8-0 # default tap is equity; the campaign persisted r_equity
|
||||
EXIT=1
|
||||
aura: no family member has a tap named 'equity'
|
||||
|
||||
$ aura chart b657c714-GER40-w0 # the literal directory name under runs/traces/0959eda8-0/
|
||||
EXIT=1
|
||||
aura: no recorded run or family 'b657c714-GER40-w0' under runs/traces (check the handle a sweep/walk-forward/campaign run printed for a typo — re-running with this handle as `--trace` will not create it)
|
||||
^ names a `--trace` flag that exists on no verb after the retirement.
|
||||
|
||||
$ aura exec blueprints/signal.json --tap equity=record # the scaffolded starter blueprint
|
||||
EXIT=1
|
||||
aura: the tap plan names 'equity', but the blueprint declares no such tap — declared taps:
|
||||
^ the scaffolded starter declares no taps at all; the list after the em dash is empty.
|
||||
@@ -0,0 +1,61 @@
|
||||
# m42_3b — the --override refusal family on both legs (release binary @ 2f1bace)
|
||||
# blueprint leg
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length
|
||||
EXIT=2
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length`
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=notanumber
|
||||
EXIT=2
|
||||
aura: exec: --override expects NODE.PARAM=VALUE, got `fast.length=notanumber`
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override nosuch.param=1
|
||||
EXIT=1
|
||||
aura: axis nosuch.param: names no param of this blueprint (open or bound) — see `aura graph introspect --params <bp>`
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=0
|
||||
EXIT=1
|
||||
aura: SMA length must be >= 1
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=-1
|
||||
EXIT=1
|
||||
aura: capacity overflow
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=-5
|
||||
EXIT=1
|
||||
aura: capacity overflow
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override bias.scale=2
|
||||
EXIT=1
|
||||
aura: this blueprint does not compile to a runnable harness: ParamKindMismatch { slot: 0, expected: F64, got: I64 }
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override bias.scale=-5.0
|
||||
EXIT=1
|
||||
aura: Bias scale must be > 0
|
||||
|
||||
$ aura exec blueprints/m42_trend.json --override bias.scale=0.5
|
||||
EXIT=0
|
||||
|
||||
# campaign leg
|
||||
|
||||
$ aura exec documents/spine-campaign.json --override fast.length=7
|
||||
EXIT=2
|
||||
aura: exec: --override `fast.length` collides with a declared axis of the campaign; an override overrides a bound value, never an axis
|
||||
|
||||
$ aura exec documents/spine-campaign.json --override stop_k=1.0
|
||||
EXIT=1
|
||||
aura: campaign references do not resolve:
|
||||
strategy b657c7148655fe3774910614031a8dcae6be317f0bbe7de6cbabfeb5ccb1d7cf: axis "stop_k" is not in the param space
|
||||
|
||||
$ aura exec documents/spine-campaign.json --override bias.scale=-5
|
||||
EXIT=1
|
||||
aura: campaign references do not resolve:
|
||||
strategy b657c7148655fe3774910614031a8dcae6be317f0bbe7de6cbabfeb5ccb1d7cf: axis "bias.scale" declares a kind that is not the param's kind
|
||||
|
||||
# topology_hash under --override — blueprint leg vs campaign leg
|
||||
$ aura exec blueprints/m42_trend.json -> topology_hash:
|
||||
b657c7148655fe3774910614031a8dcae6be317f0bbe7de6cbabfeb5ccb1d7cf
|
||||
$ aura exec blueprints/m42_trend.json --override fast.length=10 (NO-OP: 10 is the bound default) -> topology_hash:
|
||||
f952a1c8519a2dbd30fac8facb6149a8774bf4094d552a11e50afecbad9f6ccc
|
||||
$ aura reproduce <that hash>
|
||||
aura: no such family 'f952a1c8519a2dbd30fac8facb6149a8774bf4094d552a11e50afecbad9f6ccc'
|
||||
@@ -0,0 +1 @@
|
||||
/runs
|
||||
@@ -0,0 +1,4 @@
|
||||
# Static project context only (C17); paths only.
|
||||
[paths]
|
||||
runs = "runs"
|
||||
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
|
||||
@@ -0,0 +1,24 @@
|
||||
# spine-lab — an aura research project (data-only)
|
||||
|
||||
This directory is an aura project: blueprints + research documents over the
|
||||
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
|
||||
- Run: `aura exec blueprints/signal.json` (single smoke run, synthetic stream
|
||||
— the starter is closed; all params bound; bound values are defaults —
|
||||
`--override NODE.PARAM=VALUE` may override one for this run (#246))
|
||||
- Campaign: `aura exec <campaign.json>` executes a registered campaign
|
||||
document (file or content id) — the multi-cell/axis surface
|
||||
- Axes: `aura graph introspect --params blueprints/signal.json` lists the
|
||||
open + bound-overridable axes, RAW `<node>.<param>` names (#328)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
||||
- Execution model: a strategy emits a bias in [-1,+1] per cycle, held as the
|
||||
continuously-tracked target position; a protective stop defines the risk
|
||||
unit R, and quality metrics are R-based. Entry signals become held state
|
||||
via the signal-side latch/edge-pulse idiom (see
|
||||
`aura graph introspect --vocabulary`).
|
||||
- Data plane: process/campaign documents are authored directly with
|
||||
`aura process` / `aura campaign`, growing one from a bare `{}` via
|
||||
`aura campaign introspect --unwired`.
|
||||
@@ -0,0 +1 @@
|
||||
{"format_version":1,"blueprint":{"name":"m42_trend","doc":"fast/slow SMA crossover direction, clamped into the bias contract","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":10}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":50}}]}},{"primitive":{"type":"Sub","name":"spread"}},{"primitive":{"type":"Sign","name":"dir"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":4,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}}]}}
|
||||
@@ -0,0 +1,17 @@
|
||||
[
|
||||
{"op":"name","name":"m42_trend"},
|
||||
{"op":"doc","text":"fast/slow SMA crossover direction, clamped into the bias contract"},
|
||||
{"op":"source","role":"price","kind":"F64"},
|
||||
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":10}}},
|
||||
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":50}}},
|
||||
{"op":"add","type":"Sub","name":"spread"},
|
||||
{"op":"add","type":"Sign","name":"dir"},
|
||||
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":1.0}}},
|
||||
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
||||
{"op":"connect","from":"fast.value","to":"spread.lhs"},
|
||||
{"op":"connect","from":"slow.value","to":"spread.rhs"},
|
||||
{"op":"connect","from":"spread.value","to":"dir.value"},
|
||||
{"op":"connect","from":"dir.value","to":"bias.signal"},
|
||||
{"op":"expose","from":"bias.bias","as":"bias"},
|
||||
{"op":"tap","from":"spread.value","as":"spread"}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user