Compare commits

...

7 Commits

Author SHA1 Message Date
claude bbac29db2d docs(guide): campaign-validate transcript shows the real content-id shape
Ratifies the fieldtest spec-gap: the transcript abbreviated the
strategy reference to a 4-hex placeholder while the binary prints the
full 64-hex content id — the quoted line now carries the real id from
the committed cycle-328 fixture, keeping the transcript byte-honest.

refs #328
2026-07-25 02:10:16 +02:00
claude 5e32f3ccdf fieldtest: axis-namespace — 4 examples, 0 bugs / 0 friction / 1 spec-gap / 4 working
Source-blind consumer fieldtest against the release binary at 4474814:
discovery-surface identity (--params vs --list-axes byte-identical,
raw + default= on bound), the document-first loop with two
bound-override axes (validate + run e2e, manifest raw), the translation
seams (all three verbs refuse wrapped identically exit 2; campaign
validate did-you-mean exit 1), and manifests raw in params AND defaults
on all routes with reproduce 4/4 bit-identical. One spec-gap: the guide
transcript abbreviated the strategy content-id (ratified in the
follow-up doc commit). Not exercisable source-blind: replay of a
pre-#328 wrapped-name on-disk family (noted in TRANSCRIPT).

refs #328
2026-07-25 02:10:16 +02:00
claude 4474814fa7 audit: #328 cycle close — manifests record raw on every route, walkforward intake refuses wrapped
Architect drift review (cycle a3785a6..b3b7115) found the cycle's raw
switch incomplete on two high items; both fixed forward here rather
than minuted as residue, since #319 builds on this namespace:

- The real/campaign executor still minted WRAPPED manifest.params and
  manifest.defaults (AC2 overclaim in the feat commit body). The mint
  seams now reshape to raw on every route (member.rs raw_bound_defaults,
  runner.rs manifest_space mirror of the family.rs pattern); the fix
  surfaced a latent regression in persist_campaign_traces, which keyed
  point_from_params by exact wrapped name and is now translation-aware
  (same recipe as reproduce_family_in). Old on-disk families stay
  replayable (C29; reproduce reads both shapes).
- The walkforward --axis intake silently accepted wrapped names — the
  silent alias fork 2 rejected. It now runs the same
  refuse_wrapped_synthetic_axes preflight as the plain synthetic sweep
  (new e2e: aura_walkforward_synthetic_blueprint_refuses_a_wrapped_form
  _axis_name); family builders translate raw names onto wrapped
  SweepBinder slots on all three entry points. C24's "retired from the
  surface" claim is thereby made true instead of softened.
- Low items: stale graph_construct docstring updated; new
  graph_params_and_sweep_list_axes_are_line_identical pins the two
  discovery surfaces against format drift.
- Bench infra: aura-bench's campaign_sweep surface seeded wrapped axis
  literals and was correctly refused by the new intake — seeds converted
  to raw; full bench rerun: all 5 surfaces fingerprint OK, deltas within
  load noise (report-only, no baseline update needed).

Library-unit tests calling family builders directly keep wrapped input
(valid: translation is idempotent on exact wrapped names; intake-level
refusal is the user contract). Suite + clippy verified green post-fix.

refs #328
2026-07-25 01:58:50 +02:00
claude b3b7115825 docs(ledger, glossary, guide): the one-namespace axis prose
The C24 axis-discovery paragraph loses its obsolete rationale (names
prefixed by the wrapping, discovery pinned to the sweep verb) in favour
of the one-raw-namespace statement; C18 records the ratified #246
bound-override coincidence contract (param_space OR bound_param_space,
no schema flag) as the document-side absorption #328 demanded; the
glossary sweep and use entries, the authoring-guide worked block, and
the README axis examples move to raw names. Refusal transcripts quote
the byte-actual binary output (review caught an invented aura: error:
prefix; the campaign-validate transcript shows the real fault_block
shape). Frozen corpora (fieldtests/, *.history.md) keep their
historical wrapped prose.

refs #328
2026-07-25 01:33:18 +02:00
claude 3e1e7e21da feat(aura-runner, aura-cli, aura-registry): one raw axis namespace end to end
Reconciles the sweep-axis namespace (closes #328): the raw form
<node>.<param> (splice paths keep their interior path) is now the only
user-facing axis name — printed by --list-axes, accepted by --axis on
BOTH sweep routes, recorded in the synthetic run manifest, discovered
by graph introspect --params (bound params included, default= lexicon),
and validated by campaign documents. The wrapped <blueprint>.<...> form
is retired with translation refusals on both seams.

Mechanics:
- classify_axis_intake (aura-runner axes.rs): explicit raw-first
  acceptance predicate shared by both --axis intake routes. Deliberately
  not built on raw_matches_wrapped, whose equality branch would have
  accepted the retired form silently (skeptic finding, minuted on #328).
  Wrapped hit -> translation refusal naming the raw candidate (shared
  prose builder wrapped_axis_refusal, one literal for both routes).
- The synthetic route resolution moves to the raw frame the real route
  (bind_axes) already uses: override_paths/wrapped_bound_overrides_of
  match via raw_matches_wrapped (also removing a latent slicing panic),
  blueprint_sweep_family translates raw names onto wrapped SweepBinder
  slots, manifest.params records raw keys (name-only reshaping, zip is
  positional). The two sweep routes now share one convention.
- reproduce translates recorded manifest names raw-or-wrapped onto the
  wrapped space: old registered artifacts stay replayable (C29), new
  raw manifests round-trip.
- introspect --params gains the bound default= lines via the same
  render_value the --list-axes bound pass uses - the two discovery
  surfaces are byte-identical by construction.
- AxisNotInParamSpace carries raw_candidate; ref_fault_prose renders
  the did-you-mean iff present. Sweep-terminal bind errors
  (MissingKnob/KindMismatch) render the raw knob name (render seam
  only; acceptance criterion: no user-facing surface prints a wrapped
  axis name).
- Downstream consequence fixes: scaffold quickstart example spoke
  wrapped; ~90 wrapped test literals converted to raw across six test
  files (deliberately-bogus refusal names and the untouched internal
  walkforward wrapped route kept, with comments).

Fork decisions and the skeptic correction are minuted on #328
(comments 2026-07-24/25). Grounding-check PASS first attempt (12
assumptions ratified). Review (opus): translation ambiguity cleared
(uniform single prefix per blueprint), reproduce both-shapes traced;
repair pass added the synthetic-route refusal e2e + the shared prose
builder. Verified: cargo test --workspace green (99 targets, 0
failures), clippy -D warnings clean.

refs #246, refs #319, refs #331
2026-07-25 01:33:18 +02:00
claude a3785a6ec6 docs(guide): Session trigger cadence sets emission density, not the index
Ratifies the m37 milestone-fieldtest concern: the guide recommended a
once-per-bar trigger without saying whether bars_since_open depends on
it. It does not — the index is clock-derived (ctx.now()), so a denser
trigger stream (raw m1 price, as the shipped corpus wires it) emits
the same per-bar index more often. The once-per-bar wiring is a
convention for one-emission-per-bar, not a correctness requirement.
2026-07-24 23:43:53 +02:00
claude 697d81dd22 fieldtest: m37 data-authorability-boundary — 3 scenarios, 0 bugs / 2 friction / 1 spec-gap / 4 working
Milestone-close gate for milestone 37 (data-authorability boundary,
#310/#317/#271): source-blind consumer fieldtest against the release
binary at eb2b0a1, scenarios derived top-down from the milestone
promise rather than per-cycle axes.

- m37_1: NY Session authored via args + fold-selected taps, run e2e
  over US500 (default and --tap plans; summaries correct, unlisted
  tap inert).
- m37_2: London anchor registered by name, spliced via use into a
  consumer with open SMA lengths, swept over GER40, reproduced 2/2
  bit-identical (v2 propagates through the splice).
- m37_3a-e: five-way boundary-refusal battery; all exit 1 with
  actionable prose, escalation path named for namespaced types.

Gate verdict: green — all three boundary mechanisms deliver the
promise; genuinely-new logic is turned away toward Rust. Findings
routed: fold-summary read-back absorbed into #309, cross-run tap-file
lifecycle evidence onto #311, bare-refusal escalation-pointer parity
onto #341 (refs #309, refs #311, refs #341).
2026-07-24 23:43:53 +02:00
71 changed files with 1543 additions and 399 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ Invoke it as `aura <command> …` (examples below use the plain name).
- **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.
- **Axis** — one named, sweepable knob of a blueprint (e.g. `graph.fast.length`),
- **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).
+2 -2
View File
@@ -106,9 +106,9 @@ fn seed_blueprint(bin: &Path, dir: &Path, file: &str, name: &str) -> Result<Stri
"sweep",
file,
"--axis",
"sma_signal.fast.length=2,4",
"fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"slow.length=8,16",
"--name",
name,
],
+15 -5
View File
@@ -12,6 +12,7 @@ use aura_engine::{
BindOpError, BlueprintDoc, CompileError, Composite, GraphSession, LoadError, Op, OpError,
Scalar, ScalarKind,
};
use aura_runner::runner::render_value;
use serde::Deserialize;
use crate::research_docs::resolve_id_prefix;
@@ -614,10 +615,12 @@ 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 — exactly the
// namespace campaign axes are validated against (`validate_campaign_refs`
// checks the raw space; the wrapped `--list-axes` namespace on `aura sweep`
// is the sweep-verb view, not the campaign view).
// --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).
match params_lines(target, env) {
Ok(s) => print!("{s}"),
Err(m) => {
@@ -864,7 +867,11 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu
/// line per open param of the RAW composite (no harness wrap) — the
/// campaign-axis namespace `validate_campaign_refs` checks axes against. The
/// kind renders via `ScalarKind`'s `Debug` (`I64`/`F64`/`Bool`/`Timestamp`),
/// the same form `introspect --node` already uses for param kinds.
/// 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.
fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String, String> {
use std::fmt::Write as _;
let text = resolve_blueprint_text(target, env)?;
@@ -875,6 +882,9 @@ fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String,
for p in composite.param_space() {
let _ = writeln!(out, "{}:{:?}", p.name, p.kind);
}
for b in composite.bound_param_space() {
let _ = writeln!(out, "{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
}
Ok(out)
}
+120 -54
View File
@@ -45,7 +45,7 @@ use aura_measurement::information_coefficient;
// `blueprint_axis_probe_reopened` is production code only from `verb_sugar.rs`
// (a sibling module reaching it via `crate::blueprint_axis_probe_reopened`,
// the crate-root re-export this `use` gives it), not from this module itself.
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, wrapped_bound_names, RunData};
use aura_runner::member::{blueprint_axis_probe, blueprint_axis_probe_reopened, run_signal_r, RunData};
use aura_runner::{TapPlan, TapSubscription};
#[cfg(test)]
use aura_runner::member::{run_blueprint_member, wrap_r, SYNTHETIC_PIP_SIZE};
@@ -813,17 +813,67 @@ impl IcReport {
/// fully closed) — not only the fully-closed case: `--axis`/`override_paths`
/// accepts a bound name regardless of how many knobs happen to be open
/// alongside it, so the discovery surface must list it regardless too. Names
/// are exactly what `--axis` binds.
/// are RAW `<node>.<param>` paths (#328: the wrapped `<blueprint>.<node>.<param>`
/// form is retired from the surface) — exactly what `--axis` binds.
fn list_blueprint_axes(doc: &str, env: &aura_runner::project::Env) {
let space = blueprint_axis_probe(doc, env).param_space();
for p in &space {
println!("{}:{:?}", p.name, p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
// Strip the wrapper's one leading node segment (#328): open names print
// RAW, matching `--axis`'s own accepted namespace.
println!("{}:{:?}", aura_runner::axes::wrapped_to_raw_axis(&p.name), p.kind); // ScalarKind Debug -> I64/F64/Bool/Timestamp
}
let signal = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
let bp = signal.name().to_string();
for b in signal.bound_param_space() {
println!("{bp}.{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
// `bound_param_space()`'s `.name` is already RAW (#203) — no blueprint-name
// concatenation (#328: the blueprint name stays out of axis paths, C23).
println!("{}:{:?} default={}", b.name, b.kind, render_value(&b.value));
}
}
/// The one-sentence prose for a refused WRAPPED `--axis` name (#328), shared
/// by both intake routes so the wording carried by `refuse_wrapped_synthetic_axes`
/// (the synthetic-sweep preflight) and `validate_and_register_axes` (the
/// real-route/campaign preflight) cannot drift into two copies. Returns the
/// message WITHOUT the leading `aura: ` — each caller prepends that itself
/// (the synthetic route via its own `eprintln!`, the real route via
/// `exit_axis_register_error`'s shared `eprintln!("aura: {m}")`), so the
/// rendered bytes on both routes stay byte-identical.
fn wrapped_axis_refusal(n: &str, raw: &str) -> String {
format!(
"axis \"{n}\": axis names are raw node.param paths — \
use \"{raw}\" (the wrapped --list-axes form was retired, #328)"
)
}
/// `--axis` intake preflight for the SYNTHETIC (no `--real`) blueprint-sweep
/// route (#328): this route bypasses `validate_and_register_axes` entirely (no
/// project/registry touched), so it needs its own copy of the same WRAPPED-name
/// refusal — the shared [`aura_runner::axes::classify_axis_intake`] predicate,
/// intercepting only a `WrappedRetired` hit (echoing the translation pointer,
/// exit 2, before the sweep ever runs). A name matching neither namespace is
/// deliberately left unrefused HERE: `blueprint_sweep_family`'s own
/// `override_paths` rejects it downstream with today's unchanged prose, so this
/// preflight does not duplicate that message under a second wording.
fn refuse_wrapped_synthetic_axes(
doc: &str,
env: &aura_runner::project::Env,
axes: &[(String, Vec<Scalar>)],
) {
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
let raw_bound: HashSet<String> = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary")
.bound_param_space()
.into_iter()
.map(|b| b.name)
.collect();
for (n, _) in axes {
if let aura_runner::axes::AxisIntake::WrappedRetired(raw) =
aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound)
{
eprintln!("aura: {}", wrapped_axis_refusal(n, &raw));
std::process::exit(2);
}
}
}
@@ -1661,14 +1711,17 @@ enum AxisRegisterError {
NoProject(String),
}
/// Validate every `--axis` name against the blueprint's WRAPPED probe namespace
/// (`blueprint_axis_probe`/`--list-axes`) — an axis naming a BOUND param (#246:
/// re-openable, same as every already-open knob) passes exactly like an open
/// one; only a name matching NEITHER space is refused — then canonicalize +
/// register the blueprint by topology hash and strip every axis name to the RAW
/// campaign namespace (the sweep sequence, #210 c0110). A raw-form or
/// fat-fingered axis name is refused here, echoing exactly what the user typed,
/// before the archive is touched or the blueprint is registered — shared by
/// Validate every `--axis` name against the RAW campaign-axis namespace only
/// (#328: `--list-axes`'s own namespace, open-or-bound) — a WRAPPED name (the
/// retired `<blueprint>.<node>.<param>` form) is refused with a translation
/// pointer to its raw candidate; a name in neither namespace gets today's
/// unmatched-axis prose, unchanged. [`aura_runner::axes::classify_axis_intake`]
/// is the shared predicate (deliberately not `raw_matches_wrapped`, whose own
/// equality branch would silently accept a wrapped name — see its doc
/// comment). Then canonicalize + register the blueprint by topology hash — no
/// further strip: the axes are already raw by the time this returns them (the
/// old wrapped->raw strip is gone, since a splice path's own dots, e.g.
/// `anchor.sess.period_minutes`, must not be mangled a second time). Shared by
/// every campaign-path dispatcher (sweep/generalize/walkforward/mc all landed
/// the identical block; #220 slice-1 deferred this dedup to "once wf/mc land
/// the same block", rule-of-three now exceeded 4x). The override set itself is
@@ -1682,17 +1735,24 @@ fn validate_and_register_axes(
axes: &[(String, Vec<Scalar>)],
env: &aura_runner::project::Env,
) -> Result<(String, Vec<(String, Vec<Scalar>)>), AxisRegisterError> {
let space = blueprint_axis_probe(doc, env).param_space();
let wrapped_open = blueprint_axis_probe(doc, env).param_space();
let blueprint = blueprint_from_json(doc, &|t| env.resolve(t))
.expect("doc parse-validated at the dispatch boundary");
let bound = wrapped_bound_names(&blueprint);
let raw_bound: HashSet<String> =
blueprint.bound_param_space().into_iter().map(|b| b.name).collect();
for (n, _) in axes {
if !space.iter().any(|p| &p.name == n) && !bound.contains(n) {
return Err(AxisRegisterError::UnknownAxis(format!(
"axis \"{n}\" is not one of this blueprint's \
sweepable axes — run 'aura sweep <bp> --list-axes' \
to see them"
)));
match aura_runner::axes::classify_axis_intake(n, &wrapped_open, &raw_bound) {
aura_runner::axes::AxisIntake::Raw => {}
aura_runner::axes::AxisIntake::WrappedRetired(raw) => {
return Err(AxisRegisterError::UnknownAxis(wrapped_axis_refusal(n, &raw)));
}
aura_runner::axes::AxisIntake::Unknown => {
return Err(AxisRegisterError::UnknownAxis(format!(
"axis \"{n}\" is not one of this blueprint's \
sweepable axes — run 'aura sweep <bp> --list-axes' \
to see them"
)));
}
}
}
if env.provenance().is_none() {
@@ -1709,11 +1769,7 @@ fn validate_and_register_axes(
let topo = topology_hash(&blueprint);
reg.put_blueprint(&topo, &canonical)
.map_err(|e| AxisRegisterError::Registry(e.to_string()))?;
let raw_axes: Vec<(String, Vec<Scalar>)> = axes
.iter()
.map(|(n, v)| (aura_runner::axes::wrapped_to_raw_axis(n).to_string(), v.clone()))
.collect();
Ok((canonical, raw_axes))
Ok((canonical, axes.to_vec()))
}
/// Exit-map for a `validate_and_register_axes` failure — the stderr line and
@@ -2326,8 +2382,8 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
// strategy ref resolves against this one write; a second
// put under `content_id_of(&canonical)` would target the
// identical key and is not needed.
// Axis validation + canonicalize/register + the wrapped->raw
// strip are single-sourced in `validate_and_register_axes`
// Axis validation (RAW-only, #328) + canonicalize/register are
// single-sourced in `validate_and_register_axes`
// (data-free, so this fires before the archive is touched).
let (canonical, raw_axes) = validate_and_register_axes("sweep", &doc, &axes, env)
.unwrap_or_else(|e| exit_axis_register_error(e));
@@ -2361,6 +2417,10 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
);
std::process::exit(2);
}
// #328: this route bypasses `validate_and_register_axes` (no
// project/registry involved), so it needs its own WRAPPED-name
// refusal ahead of the sweep.
refuse_wrapped_synthetic_axes(&doc, env, &axes);
run_blueprint_sweep(
&doc, &axes, &name, persist,
DataSource::from_choice(data, env), env,
@@ -2418,8 +2478,8 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
eprintln!("aura: {m}");
std::process::exit(2);
});
// Axis validation + canonicalize/register + the wrapped->raw
// strip are single-sourced in `validate_and_register_axes`
// Axis validation (RAW-only, #328) + canonicalize/register are
// single-sourced in `validate_and_register_axes`
// (data-free, so this fires before the archive is touched).
let (canonical, raw_axes) = validate_and_register_axes("walkforward", &doc, &axes, env)
.unwrap_or_else(|e| exit_axis_register_error(e));
@@ -2463,10 +2523,11 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
exit_on_campaign_result(result);
return;
}
// Synthetic in-process family path — unchanged (#220 non-goal); it
// persists no per-window taps, so `--trace` stays refused here (#224
// delivered only the `--real` campaign path above), mirroring
// `dispatch_sweep`'s synthetic-arm refusal with its own named pointer.
// Synthetic in-process family path (#220 non-goal, unchanged beyond
// the #328 tidy below); it persists no per-window taps, so
// `--trace` stays refused here (#224 delivered only the `--real`
// campaign path above), mirroring `dispatch_sweep`'s synthetic-arm
// refusal with its own named pointer.
if a.trace.is_some() {
eprintln!(
"aura: --trace is not yet available on a synthetic walkforward (no --real); see #224"
@@ -2485,6 +2546,10 @@ fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
None => Selection::Argmax,
};
let name = a.name.clone().unwrap_or_else(|| "walkforward".to_string());
// #328: this route bypasses `validate_and_register_axes` (no
// project/registry involved, same gap the plain synthetic sweep
// arm had) — the same WRAPPED-name preflight closes it here too.
refuse_wrapped_synthetic_axes(&doc, env, &axes);
run_blueprint_walkforward(&doc, &axes, &name, DataSource::Synthetic, select, env);
}
None => {
@@ -2532,8 +2597,8 @@ fn dispatch_mc(a: McCmd, env: &aura_runner::project::Env) {
eprintln!("aura: {}", usage());
std::process::exit(2);
}
// Axis validation + canonicalize/register + the wrapped->raw
// strip are single-sourced in `validate_and_register_axes`
// Axis validation (RAW-only, #328) + canonicalize/register are
// single-sourced in `validate_and_register_axes`
// (data-free, so this fires before the archive is touched).
let (canonical, raw_axes) = validate_and_register_axes("mc", &doc, &axes, env)
.unwrap_or_else(|e| exit_axis_register_error(e));
@@ -3532,11 +3597,11 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// #246: a fully-bound (closed) blueprint IS sweepable — an axis naming a
/// bound param re-opens it (the bound value is the default), so the retired
/// "fully bound; nothing to sweep" refusal must not resurface. Inverse
/// guard: an axis naming NEITHER an open nor a bound param gets the one
/// clear boundary message (not a terse `UnknownKnob` debug leak).
/// #246/#328: a fully-bound (closed) blueprint IS sweepable — a RAW axis
/// naming a bound param re-opens it (the bound value is the default), so
/// the retired "fully bound; nothing to sweep" refusal must not resurface.
/// Inverse guard: an axis naming NEITHER an open nor a bound param gets
/// the one clear boundary message (not a terse `UnknownKnob` debug leak).
#[test]
fn blueprint_sweep_family_overrides_a_bound_param_and_names_unknown_axes() {
let env = aura_runner::project::Env::std();
@@ -3544,9 +3609,9 @@ mod tests {
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
// (a) override axis: two members, no refusal
// (a) override axis (RAW name, #328): two members, no refusal
let axes = vec![(
"sma_signal.fast.length".to_string(),
"fast.length".to_string(),
vec![Scalar::i64(2), Scalar::i64(4)],
)];
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
@@ -3554,7 +3619,7 @@ mod tests {
assert_eq!(fam.points.len(), 2);
// (b) unknown axis: the boundary message, no UnknownKnob leak
let bad = vec![("sma_signal.nope".to_string(), vec![Scalar::i64(1)])];
let bad = vec![("nope".to_string(), vec![Scalar::i64(1)])];
let err = blueprint_sweep_family(&doc, &bad, &DataSource::Synthetic, &env)
.expect_err("an axis matching neither space is refused");
assert!(err.contains("names no param"), "boundary message, got: {err}");
@@ -3562,35 +3627,36 @@ mod tests {
assert!(!err.contains("UnknownKnob"), "must not leak the debug render: {err}");
}
/// #249: an axis-reopened bound param flows through `params` ("what
/// #249/#328: an axis-reopened bound param flows through `params` ("what
/// varied") and must NOT also appear in `defaults` ("what was held") — the
/// two are disjoint by construction. Sweeping `fast.length` on the fully
/// bound r_sma example leaves `slow.length`/`bias.scale` untouched (they
/// stay in `defaults`), while `fast.length` moves to `params` and drops
/// out of `defaults` entirely.
/// two are disjoint by construction. Sweeping `fast.length` (RAW, #328) on
/// the fully bound r_sma example leaves `slow.length`/`bias.scale`
/// untouched (they stay in `defaults`), while `fast.length` moves to
/// `params` and drops out of `defaults` entirely. Both `manifest.params`
/// and `manifest.defaults` keys are RAW (#328, batch 2 flips defaults too).
#[test]
fn sweep_override_excludes_the_reopened_default_from_the_manifest() {
let env = aura_runner::project::Env::std();
let closed = load_closed_r_sma();
let doc = blueprint_to_json(&closed).expect("serializes");
let axes = vec![("sma_signal.fast.length".to_string(), vec![Scalar::i64(2)])];
let axes = vec![("fast.length".to_string(), vec![Scalar::i64(2)])];
let fam = blueprint_sweep_family(&doc, &axes, &DataSource::Synthetic, &env)
.expect("a bound param is a default — the axis overrides it");
assert_eq!(fam.points.len(), 1);
let manifest = &fam.points[0].report.manifest;
let param_names: Vec<&str> = manifest.params.iter().map(|(n, _)| n.as_str()).collect();
assert!(param_names.contains(&"sma_signal.fast.length"), "swept axis is a param: {param_names:?}");
assert!(param_names.contains(&"fast.length"), "swept axis is a raw param: {param_names:?}");
let default_names: Vec<&str> = manifest.defaults.iter().map(|(n, _)| n.as_str()).collect();
assert!(
!default_names.contains(&"sma_signal.fast.length"),
!default_names.contains(&"fast.length"),
"the reopened default must not also appear in defaults: {default_names:?}"
);
assert_eq!(
default_names,
["sma_signal.slow.length", "sma_signal.bias.scale"],
"the untouched bound params stay in defaults: {default_names:?}"
["slow.length", "bias.scale"],
"the untouched bound params stay in defaults, RAW: {default_names:?}"
);
}
+42 -2
View File
@@ -462,8 +462,14 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
RefFault::StrategyUnloadable { id, error } => {
format!("strategy {id} cannot be loaded: {error}")
}
RefFault::AxisNotInParamSpace { strategy, axis } => {
format!("strategy {strategy}: axis \"{axis}\" is not in the param space")
RefFault::AxisNotInParamSpace { strategy, axis, raw_candidate } => {
let mut msg = format!("strategy {strategy}: axis \"{axis}\" is not in the param space");
if let Some(candidate) = raw_candidate {
msg.push_str(&format!(
"; axis names are raw node.param paths — did you mean \"{candidate}\"?"
));
}
msg
}
RefFault::AxisKindMismatch { strategy, axis } => {
format!("strategy {strategy}: axis \"{axis}\" declares a kind that is not the param's kind")
@@ -761,6 +767,7 @@ mod tests {
ref_fault_prose(&RefFault::AxisNotInParamSpace {
strategy: "9f3a".into(),
axis: "nope".into(),
raw_candidate: None,
}),
ref_fault_prose(&RefFault::AxisKindMismatch {
strategy: "9f3a".into(),
@@ -781,6 +788,39 @@ mod tests {
}
}
/// #328: `ref_fault_prose` appends the did-you-mean clause, contiguous
/// and verbatim, exactly when `raw_candidate` is present — the document-
/// side refusal seam's mirror of the sweep `--axis` intake translation
/// (`aura-runner`'s `axes.rs` classify predicate), sharing the same
/// `axis names are raw node.param paths` clause pinned there.
#[test]
fn ref_fault_prose_renders_the_did_you_mean_when_a_raw_candidate_is_present() {
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
strategy: "9f3a".into(),
axis: "graph.fast.length".into(),
raw_candidate: Some("fast.length".into()),
});
assert_eq!(
msg,
"strategy 9f3a: axis \"graph.fast.length\" is not in the param space; \
axis names are raw node.param paths — did you mean \"fast.length\"?"
);
}
/// #328 (negative): a rejected axis with no `raw_candidate` (stripping one
/// leading segment yields no param-space hit, or the axis has no leading
/// segment to strip at all) renders today's prose unchanged — no
/// speculative suggestion.
#[test]
fn ref_fault_prose_omits_the_did_you_mean_when_no_raw_candidate() {
let msg = ref_fault_prose(&RefFault::AxisNotInParamSpace {
strategy: "9f3a".into(),
axis: "nope".into(),
raw_candidate: None,
});
assert_eq!(msg, "strategy 9f3a: axis \"nope\" is not in the param space");
}
#[test]
/// #194 (the prefix trap): a doc ref that carries the display `content:`
/// prefix (a copy-paste from register/introspect output) is bare-only in
+7 -5
View File
@@ -234,8 +234,9 @@ 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 __NAME_SNAKE___signal.fast.length=2,4,8`
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes)
- 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)
- 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`).
@@ -459,10 +460,11 @@ 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 rendered blueprint-name axis prefix (#246: a bound param
// is a default an axis overrides).
// 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).
assert!(claude.contains("blueprints/signal.json"));
assert!(claude.contains("demo_lab_signal.fast.length"));
assert!(claude.contains("--axis fast.length"));
}
/// #315: the scaffolded project CLAUDE.md teaches the execution semantics
File diff suppressed because it is too large Load Diff
+66 -19
View File
@@ -660,16 +660,44 @@ fn register_refuses_undescribed_composites_end_to_end() {
/// Property (#196, the campaign-axis namespace): `--params <FILE>` prints the
/// RAW composite param space — one `{name}:{kind:?}` line per open param, in
/// lowering order, WITHOUT the harness-wrap prefix `aura sweep --list-axes`
/// shows. The open fixture leaves exactly the two SMA lengths unbound
/// (`bias.scale` is bound in the document).
/// lowering order (#328: the one raw axis namespace — `aura sweep
/// --list-axes` prints the identical lines for the same blueprint, see
/// `graph_params_and_sweep_list_axes_are_line_identical` below). The open
/// fixture leaves exactly the two SMA lengths unbound (`bias.scale` is bound
/// in the document).
#[test]
fn graph_params_lists_raw_axis_namespace() {
let dir = temp_cwd("params");
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_sma_open.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(stdout, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
assert_eq!(
stdout, "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"
);
}
/// 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.
#[test]
fn graph_params_and_sweep_list_axes_are_line_identical() {
let dir = temp_cwd("params-vs-list-axes");
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"
);
}
/// Property (#196, file-mode --content-id): a blueprint FILE's printed content
@@ -719,7 +747,10 @@ fn graph_params_by_content_id_matches_params_by_file() {
run_in(&dir, &["graph", "introspect", "--params", &bp]);
assert_eq!(by_file_code, Some(0), "stdout: {by_file_out} stderr: {by_file_err}");
assert_eq!(by_id_out, by_file_out, "by-id and by-file agree on the raw axis namespace");
assert_eq!(by_id_out, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
assert_eq!(
by_id_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"
);
}
/// #194 (the prefix trap): a `--params <ID>` target may carry the display
@@ -736,7 +767,10 @@ fn graph_params_tolerates_content_prefix_on_target() {
let (pfx_out, pfx_err, pfx_code) =
run_in(&dir, &["graph", "introspect", "--params", &format!("content:{id}")]);
assert_eq!(pfx_code, Some(0), "content:-prefixed --params must resolve: stdout {pfx_out} stderr {pfx_err}");
assert_eq!(pfx_out, "fast.length:I64\nslow.length:I64\n", "same raw axis namespace as the bare id");
assert_eq!(
pfx_out, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
"same raw axis namespace as the bare id"
);
}
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
@@ -965,7 +999,10 @@ fn shipped_r_sma_example_is_genuinely_closed() {
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--params", &example("r_sma.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
assert_eq!(
stdout, "fast.length:I64 default=2\nslow.length:I64 default=4\nbias.scale:F64 default=0.5\n",
"zero OPEN params — every knob is now printed as a raw bound default instead"
);
}
/// Property (#159): the open fixture (`tests/fixtures/r_sma_open.json`) is a
@@ -983,8 +1020,8 @@ fn open_r_sma_fixture_lists_its_axis_namespace() {
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_sma_open.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(
stdout, "fast.length:I64\nslow.length:I64\n",
"the raw open params, in order, from the public gallery copy"
stdout, "fast.length:I64\nslow.length:I64\nbias.scale:F64 default=0.5\n",
"the raw open params, then the raw bound param, from the public gallery copy"
);
}
@@ -1021,12 +1058,17 @@ fn shipped_r_breakout_example_is_genuinely_closed() {
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
assert_eq!(
stdout,
"delay.lag:I64 default=1\nchannel_hi.length:I64 default=3\nchannel_lo.length:I64 default=3\n",
"zero OPEN params — every knob is now printed as a raw bound default instead"
);
}
/// Property (#159 cut 2): the open fixture (`tests/fixtures/r_breakout_open.json`)
/// lists its ONE ganged channel axis (#61: the two rolling windows are structurally
/// one knob).
/// one knob) followed by the still-bound `delay.lag` default (#328: bound params
/// are listed too, line-identical to `--list-axes`).
#[test]
fn open_r_breakout_fixture_lists_its_axis_namespace() {
let dir = temp_cwd("r-breakout-example-open-params");
@@ -1034,8 +1076,9 @@ fn open_r_breakout_fixture_lists_its_axis_namespace() {
run_in(&dir, &["graph", "introspect", "--params", &fixture("r_breakout_open.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(
stdout, "channel_length:I64\n",
"the ganged channel knob — one public axis for the two rolling windows (#61)"
stdout, "channel_length:I64\ndelay.lag:I64 default=1\n",
"the ganged channel knob — one public axis for the two rolling windows (#61)\
then the still-bound delay.lag default"
);
}
@@ -1064,7 +1107,7 @@ fn open_r_breakout_fixture_gang_axis_matches_the_closed_example() {
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", "r_breakout_signal.channel_length=3"],
&["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();
@@ -1086,7 +1129,11 @@ fn shipped_r_meanrev_example_is_genuinely_closed() {
let (stdout, stderr, code) =
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
assert_eq!(
stdout,
"mean_window.length:I64 default=3\nvar_window.length:I64 default=3\nband.factor:F64 default=2\n",
"zero OPEN params — every knob is now printed as a raw bound default instead"
);
}
/// Property (#159 cut 3): the open fixture (`tests/fixtures/r_meanrev_open.json`)
@@ -1124,8 +1171,8 @@ fn open_r_meanrev_fixture_gang_plus_plain_axis_matches_the_closed_example() {
&sweep_dir,
&[
"sweep", &fixture("r_meanrev_open.json"),
"--axis", "r_meanrev_signal.window=3",
"--axis", "r_meanrev_signal.band.factor=2.0",
"--axis", "window=3",
"--axis", "band.factor=2.0",
],
);
assert_eq!(sweep_code, Some(0), "sweep stderr: {sweep_stderr}");
@@ -1890,8 +1937,8 @@ fn graph_build_use_end_to_end_axes() {
run_in(&dir, &["sweep", consumer_bp.to_str().unwrap(), "--list-axes"]);
assert_eq!(axes_code, Some(0), "list-axes: {axes_out} {axes_err}");
assert_eq!(
axes_out, "graph.trend.sma.length:I64\n",
"the spliced instance's open param surfaces path-qualified, bare (unbound) form"
axes_out, "trend.sma.length:I64\n",
"the spliced instance's open param surfaces path-qualified, bare (unbound) RAW form (#328)"
);
}
+2 -2
View File
@@ -117,9 +117,9 @@ fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
"sweep",
&closed_bp,
"--axis",
"sma_signal.fast.length=2,4",
"fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"slow.length=8,16",
"--name",
"proj-anchor",
])
+2 -2
View File
@@ -135,7 +135,7 @@ fn data_only_project_sweeps_without_any_build() {
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
let proj = base.join("scratch");
let out = aura(
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4"],
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4"],
&proj,
);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -157,7 +157,7 @@ fn data_only_project_sweep_over_the_starter_opens_one_member_per_axis_value() {
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
let proj = base.join("scratch");
let out = aura(
&["sweep", "blueprints/signal.json", "--axis", "scratch_signal.fast.length=2,4,8"],
&["sweep", "blueprints/signal.json", "--axis", "fast.length=2,4,8"],
&proj,
);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
+13 -11
View File
@@ -160,8 +160,8 @@ fn project_node_open_param_sweeps_and_reproduces() {
// 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 wrapped
// name is `<blueprint>.<node>.<param>`.
// 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"]);
assert!(
axes.status.success(),
@@ -170,10 +170,10 @@ fn project_node_open_param_sweeps_and_reproduces() {
);
assert_eq!(
String::from_utf8_lossy(&axes.stdout),
"scaled_signal.gain.factor:F64\n\
scaled_signal.fast.length:I64 default=2\n\
scaled_signal.slow.length:I64 default=4\n\
scaled_signal.bias.scale:F64 default=0.5\n",
"gain.factor:F64\n\
fast.length:I64 default=2\n\
slow.length:I64 default=4\n\
bias.scale:F64 default=0.5\n",
"the project node's own OPEN param is the one open axis, alongside the \
fixture's bound defaults; stderr: {}",
String::from_utf8_lossy(&axes.stderr)
@@ -198,7 +198,7 @@ fn project_node_open_param_sweeps_and_reproduces() {
"--to",
GER40_TO_MS,
"--axis",
"scaled_signal.gain.factor=0.5,1.0",
"gain.factor=0.5,1.0",
"--name",
"knob",
],
@@ -212,9 +212,11 @@ fn project_node_open_param_sweeps_and_reproduces() {
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "one member line per factor point: {stdout}");
// Each member's manifest carries the swept PROJECT-node param binding under
// its wrapped name — the load-bearing proof that it is MY node's knob that
// varied across the family, not some incidental std axis.
// Each member's manifest carries the swept PROJECT-node param binding
// under its RAW name (this cycle's tidy fix, #328 batch 2: the
// real/campaign route's own `manifest.params` mints raw too now) — the
// load-bearing proof that it is MY node's knob that varied across the
// family, not some incidental std axis.
for (line, factor) in lines.iter().zip([0.5_f64, 1.0]) {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
assert_eq!(
@@ -227,7 +229,7 @@ fn project_node_open_param_sweeps_and_reproduces() {
.expect("manifest.params is an array");
let bound = params
.iter()
.find(|p| p[0].as_str() == Some("scaled_signal.gain.factor"))
.find(|p| p[0].as_str() == Some("gain.factor"))
.and_then(|p| p[1]["F64"].as_f64());
assert_eq!(
bound,
+10 -9
View File
@@ -329,9 +329,9 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
"sweep",
&closed_bp,
"--axis",
"sma_signal.fast.length=2,4",
"fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"slow.length=8,16",
"--name",
"campaign-ref-seed",
],
@@ -377,10 +377,11 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
.trim_start_matches("content:")
.to_string();
// "fast.length": the axis name is the RAW composite's `param_space` name.
// "fast.length": the axis name is the RAW composite's `param_space` name
// the SAME namespace `aura sweep --axis` itself binds against now (#328).
// `validate_campaign_refs` loads the stored blueprint bare, unlike the
// sweep's `wrap_r`-wrapped axis probe, so it does NOT carry the
// "sma_signal." prefix `aura sweep --axis` binds against.
// sweep's `wrap_r`-wrapped axis probe, so it never carries a wrap prefix
// to begin with.
let campaign = format!(
r#"{{
"format_version": 1,
@@ -448,9 +449,9 @@ fn campaign_validate_resolves_identity_ref_via_index_first_lookup_then_index_hit
"sweep",
&closed_bp,
"--axis",
"sma_signal.fast.length=2,4",
"fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"slow.length=8,16",
"--name",
"campaign-identity-seed",
],
@@ -1288,9 +1289,9 @@ fn seed_blueprint(dir: &Path, name: &str) -> String {
"sweep",
&closed_bp,
"--axis",
"sma_signal.fast.length=2,4",
"fast.length=2,4",
"--axis",
"sma_signal.slow.length=8,16",
"slow.length=8,16",
"--name",
name,
],
+120 -5
View File
@@ -421,7 +421,18 @@ pub enum RefFault {
StrategyNotFound(String),
IdentityUnmatched(String),
StrategyUnloadable { id: String, error: String },
AxisNotInParamSpace { strategy: String, axis: String },
AxisNotInParamSpace {
strategy: String,
axis: String,
/// #328 translation aid: stripping ONE leading segment off `axis`
/// (the wrapped `<blueprint>.<node>.<param>` shape a stale transcript
/// might quote) and finding that remainder in the strategy's
/// `param_space()` or `bound_param_space()` — `Some` iff the strip-
/// then-hit lands, computed at fault-construction time so the prose
/// layer never re-derives it. `None` when no leading segment strips
/// to a hit (today's prose stays unchanged, no speculation).
raw_candidate: Option<String>,
},
AxisKindMismatch { strategy: String, axis: String },
/// An open param of the resolved strategy is bound by no campaign axis —
/// the executor's every-open-knob-required rule, mirrored at validate
@@ -523,10 +534,22 @@ impl Registry {
});
}
}
None => faults.push(RefFault::AxisNotInParamSpace {
strategy: label.clone(),
axis: axis.clone(),
}),
None => {
// #328 did-you-mean: strip one leading segment off the
// rejected axis (the wrapped-name shape a stale
// transcript might quote) and check whether the
// remainder is a legal raw axis here — never
// speculating when it isn't.
let raw_candidate = axis.split_once('.').map(|(_, rest)| rest).filter(|stripped| {
space.iter().any(|p| p.name == *stripped)
|| bound.iter().any(|b| b.name == *stripped)
}).map(str::to_string);
faults.push(RefFault::AxisNotInParamSpace {
strategy: label.clone(),
axis: axis.clone(),
raw_candidate,
})
}
}
}
// The reverse direction: every open param must be bound by some
@@ -2251,6 +2274,98 @@ mod tests {
assert_eq!(reg.validate_campaign_refs(&by_identity, &resolve).expect("io ok"), Vec::new());
}
/// #328: a rejected axis shaped like a stale wrapped transcript (one
/// bogus leading segment glued in front of a real raw param name) carries
/// that real name as `AxisNotInParamSpace::raw_candidate` — computed at
/// fault-construction time in `validate_campaign_refs`, never re-derived
/// by the prose layer.
#[test]
fn axis_not_in_param_space_carries_a_raw_candidate_when_strippable() {
use aura_engine::blueprint_to_json;
let reg = Registry::open(temp_family_dir("axis_raw_candidate_hit"));
let resolve = |t: &str| aura_vocabulary::std_vocabulary(t);
let composite = bias_fixture();
let real = composite.param_space().first().expect("fixture has an open param").clone();
let real_name = real.name.clone();
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
let bp_id = aura_research::content_id_of(&blueprint_json);
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
let proc_id = reg.put_process(process).expect("seed process");
let wrapped_axis = format!("graph.{real_name}");
let campaign_text = format!(
concat!(
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
r#""axes":{{"{axis}":{{"kind":"F64","values":[0.5]}}}}}}],"#,
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
),
bp = bp_id,
axis = wrapped_axis,
proc = proc_id,
);
let doc = aura_research::parse_campaign(&campaign_text).expect("campaign parses");
let faults = reg.validate_campaign_refs(&doc, &resolve).expect("io ok");
let hit = faults
.iter()
.find(|f| matches!(f, RefFault::AxisNotInParamSpace { axis, .. } if axis == &wrapped_axis));
match hit {
Some(RefFault::AxisNotInParamSpace { raw_candidate, .. }) => {
assert_eq!(raw_candidate.as_deref(), Some(real_name.as_str()));
}
other => panic!("expected AxisNotInParamSpace for {wrapped_axis:?}, got {other:?}"),
}
}
/// #328 (negative): a rejected axis whose stripped remainder ALSO misses
/// the param space carries no `raw_candidate` — the fault never
/// speculates when stripping one leading segment lands nowhere.
#[test]
fn axis_not_in_param_space_carries_no_candidate_when_stripped_remainder_also_misses() {
use aura_engine::blueprint_to_json;
let reg = Registry::open(temp_family_dir("axis_raw_candidate_miss"));
let resolve = |t: &str| aura_vocabulary::std_vocabulary(t);
let composite = bias_fixture();
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
let bp_id = aura_research::content_id_of(&blueprint_json);
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
let proc_id = reg.put_process(process).expect("seed process");
let bogus_axis = "bogus.alsobogus";
let campaign_text = format!(
concat!(
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
r#""axes":{{"{axis}":{{"kind":"F64","values":[0.5]}}}}}}],"#,
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
),
bp = bp_id,
axis = bogus_axis,
proc = proc_id,
);
let doc = aura_research::parse_campaign(&campaign_text).expect("campaign parses");
let faults = reg.validate_campaign_refs(&doc, &resolve).expect("io ok");
let hit = faults
.iter()
.find(|f| matches!(f, RefFault::AxisNotInParamSpace { axis, .. } if axis == bogus_axis));
match hit {
Some(RefFault::AxisNotInParamSpace { raw_candidate, .. }) => {
assert_eq!(raw_candidate, &None);
}
other => panic!("expected AxisNotInParamSpace for {bogus_axis:?}, got {other:?}"),
}
}
/// Property (#191): the identity-index sidecar is a last-line-wins cache
/// over identity id → content id — a missing file reads as empty (no
/// error, since the scan is the truth path), a later append for the same
+116
View File
@@ -45,6 +45,75 @@ pub fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool {
wrapped == raw || wrapped_to_raw_axis(wrapped) == raw
}
/// (a) #328: the explicit `--axis` acceptance predicate, shared by BOTH
/// intake routes (`validate_and_register_axes`'s real route and the synthetic
/// sweep-verb intake ahead of `run_blueprint_sweep`). RAW is checked FIRST and
/// independently — a legal RAW name (an open param's [`wrapped_to_raw_axis`]
/// suffix, or a bound param's own already-raw name, #203) is accepted
/// outright, so a pathological name that is both raw-legal and wrapped-exact
/// (e.g. an unwrapped param) resolves as raw, never as a translation refusal.
/// Only once that fails is a WRAPPED hit considered (an exact `param_space()`
/// name, or one leading segment stripped off `name` landing on a bound
/// param): that is a translation refusal naming the raw candidate, never a
/// silent alias. Anything matching neither namespace is [`AxisIntake::Unknown`]
/// — the caller's own unmatched-axis prose applies, unchanged.
///
/// Deliberately NOT built on [`raw_matches_wrapped`]: that predicate's own
/// equality branch also matches an EXACT wrapped name (see its doc comment),
/// so using it as the acceptance gate would silently accept the retired form
/// instead of refusing it.
#[derive(Debug, PartialEq, Eq)]
pub enum AxisIntake {
/// A legal RAW name (`--list-axes`'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.
WrappedRetired(String),
/// Neither namespace: the caller's own unknown-axis prose applies.
Unknown,
}
/// Classify one `--axis` name against `wrapped_open` (the WRAPPED open-param
/// probe, `blueprint_axis_probe(..).param_space()`) and `raw_bound` (the
/// strategy's own `bound_param_space()` names — already RAW, #203). See
/// [`AxisIntake`] for the precedence.
pub fn classify_axis_intake(
name: &str,
wrapped_open: &[ParamSpec],
raw_bound: &HashSet<String>,
) -> AxisIntake {
let is_raw = wrapped_open.iter().any(|p| wrapped_to_raw_axis(&p.name) == name)
|| raw_bound.contains(name);
if is_raw {
return AxisIntake::Raw;
}
let is_wrapped = wrapped_open.iter().any(|p| p.name == name)
|| raw_bound.iter().any(|b| name.split_once('.').map(|(_, rest)| rest) == Some(b.as_str()));
if is_wrapped {
AxisIntake::WrappedRetired(wrapped_to_raw_axis(name).to_string())
} else {
AxisIntake::Unknown
}
}
/// Translate one RAW axis name onto the ONE wrapped `space` slot it
/// suffix-matches ([`raw_matches_wrapped`]), falling back to `raw` unchanged
/// when no wrapped slot matches — the caller's own downstream name
/// resolution (e.g. the sweep terminal's `UnknownKnob`) surfaces that case,
/// never panicked here. #328: the shared translation `blueprint_sweep_family`
/// established for its `SweepBinder::axis` call, extracted so
/// `blueprint_sweep_over`/`validate_axis_grid` (the walkforward synthetic
/// route's own binder calls, which key by the same exact wrapped
/// `param_space()` name) can translate a RAW `--axis` name too instead of
/// requiring the retired wrapped form.
pub fn wrapped_name_of(raw: &str, space: &[ParamSpec]) -> String {
space
.iter()
.find(|p| raw_matches_wrapped(raw, &p.name))
.map(|p| p.name.clone())
.unwrap_or_else(|| raw.to_string())
}
/// Suffix-join each raw campaign axis onto exactly one wrapped param
/// ([`raw_matches_wrapped`] — wrapped == raw, or stripping the wrapper's one
/// node segment yields raw), then require every wrapped slot to be covered.
@@ -139,6 +208,53 @@ mod tests {
ParamSpec { name: name.to_string(), kind: ScalarKind::I64 }
}
#[test]
/// #328: a legal RAW name — either an open param's raw suffix or a bound
/// param's own (already-raw) name — classifies `Raw`, accepted as-is.
fn classify_axis_intake_accepts_a_raw_open_or_bound_name() {
let wrapped_open = vec![spec("sma_signal.fast.length")];
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
assert_eq!(classify_axis_intake("fast.length", &wrapped_open, &raw_bound), AxisIntake::Raw);
assert_eq!(classify_axis_intake("bias.scale", &wrapped_open, &raw_bound), AxisIntake::Raw);
}
#[test]
/// #328: a WRAPPED name — the exact `param_space()` string, or a bound
/// param prefixed by one wrap segment — classifies `WrappedRetired`,
/// carrying its raw candidate, never accepted silently.
fn classify_axis_intake_flags_a_wrapped_name_with_its_raw_candidate() {
let wrapped_open = vec![spec("sma_signal.fast.length")];
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
assert_eq!(
classify_axis_intake("sma_signal.fast.length", &wrapped_open, &raw_bound),
AxisIntake::WrappedRetired("fast.length".to_string())
);
assert_eq!(
classify_axis_intake("sma_signal.bias.scale", &wrapped_open, &raw_bound),
AxisIntake::WrappedRetired("bias.scale".to_string())
);
}
#[test]
/// #328: a name matching neither namespace is `Unknown` — the caller's own
/// unmatched-axis prose applies, unchanged by this predicate.
fn classify_axis_intake_reports_unknown_for_neither_space() {
let wrapped_open = vec![spec("sma_signal.fast.length")];
let raw_bound: HashSet<String> = ["bias.scale".to_string()].into_iter().collect();
assert_eq!(classify_axis_intake("nope", &wrapped_open, &raw_bound), AxisIntake::Unknown);
}
#[test]
/// #328: raw is checked FIRST — an unwrapped (no-dot) param whose raw name
/// happens to equal its own wrapped `param_space()` string (a root-level
/// knob with no node-path prefix) resolves as `Raw`, never misclassified
/// as a wrapped-exact hit needing translation.
fn classify_axis_intake_prefers_raw_when_a_name_is_both() {
let wrapped_open = vec![spec("length")]; // no dot: raw == wrapped
let raw_bound: HashSet<String> = HashSet::new();
assert_eq!(classify_axis_intake("length", &wrapped_open, &raw_bound), AxisIntake::Raw);
}
#[test]
/// The only shape treated as a direct store address is a bare 64-char
/// lowercase-hex token; anything else (wrong length, uppercase, non-hex)
+66 -18
View File
@@ -254,16 +254,30 @@ const DEFLATION_SEED: u64 = 0xDEF1_A7ED;
/// 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) => 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 } => format!(
"axis {knob}: expected {expected:?}, supplied {got:?} — \
see `aura sweep <bp> --list-axes`"
),
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 — \
@@ -394,11 +408,17 @@ fn validate_axis_grid(
) -> 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(first_name, first_vals.clone());
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(n, vals.clone());
binder = binder.axis(&crate::axes::wrapped_name_of(n, &space), vals.clone());
}
binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ())
}
@@ -463,15 +483,38 @@ pub fn blueprint_sweep_family(
&overrides,
)
};
// seed the named axes verbatim: the first via Composite::axis (consumes the probe),
// the rest via SweepBinder::axis. resolve_axes name- and kind-checks them at the
// sweep terminal, so an UnknownKnob / KindMismatch is returned, not panicked.
// 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(first_name, first_vals.clone());
let mut binder = probe.axis(&wrapped_name_of(first_name), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(n, vals.clone());
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 —
@@ -485,7 +528,7 @@ pub fn blueprint_sweep_family(
// 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, &space, data.run_sources(env, &binding.columns()), window, 0, pip, &topo, env, DEFAULT_STOP, &binding, &[], NO_INSTRUMENT_CONTEXT)
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) => {
@@ -545,11 +588,16 @@ pub fn blueprint_sweep_over(
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(first_name, first_vals.clone());
let mut binder = probe.axis(&crate::axes::wrapped_name_of(first_name, &space), first_vals.clone());
for (n, vals) in iter {
binder = binder.axis(n, vals.clone());
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| {
+2 -2
View File
@@ -12,7 +12,7 @@ use std::collections::BTreeMap;
use aura_core::{Scalar, Timestamp};
use aura_engine::{Composite, Harness, MeasurementReport, RunManifest};
use crate::member::{key_supply, resolve_run_data, wrapped_bound_defaults, RunData};
use crate::member::{key_supply, raw_bound_defaults, resolve_run_data, RunData};
use crate::project::Env;
use crate::tap_plan::{bind_tap_plan, TapPlan};
@@ -79,7 +79,7 @@ pub fn run_measurement(
std::process::exit(1);
}
let names: Vec<String> = signal.param_space().iter().map(|p| p.name.clone()).collect();
let defaults = wrapped_bound_defaults(&signal);
let defaults = raw_bound_defaults(&signal);
let (sources, window, _pip_size) = resolve_run_data(&data, env, &binding);
// Compile the signal DIRECTLY — no wrap_r, no broker/executor/eq-ex-r sinks.
+58 -43
View File
@@ -82,22 +82,20 @@ pub fn sim_optimal_manifest(
}
}
/// The wrap-prefixed BOUND-param defaults of `signal` (#249): every
/// `bound_param_space()` entry as a `(<signal.name()>.<param>, value)` pair, in
/// `bound_param_space()` order — the `RunManifest.defaults` field. Must be read
/// off `signal` BEFORE it is consumed by `wrap_r`/reopening: an axis-reopened
/// bound param has already left `bound_param_space()` by construction
/// (`Composite::reopen` forgets the bound value), so a caller that reopens
/// overrides before calling this naturally excludes them — no separate filter
/// needed. Same prefixing rule as `wrapped_bound_names`, kept separate because
/// that helper discards the value this one needs.
pub fn wrapped_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
let prefix = format!("{}.", signal.name());
signal
.bound_param_space()
.into_iter()
.map(|b| (format!("{prefix}{}", b.name), b.value))
.collect()
/// The RAW-namespace BOUND-param defaults of `signal` (#249/#328): every
/// `bound_param_space()` entry as a `(<param>, value)` pair, already RAW
/// (`bound_param_space()`'s own path-qualified name, #203 — no wrap-prefix
/// concatenation), in `bound_param_space()` order — the `RunManifest.defaults`
/// field. Must be read off `signal` BEFORE it is consumed by
/// `wrap_r`/reopening: an axis-reopened bound param has already left
/// `bound_param_space()` by construction (`Composite::reopen` forgets the
/// bound value), so a caller that reopens overrides before calling this
/// naturally excludes them — no separate filter needed. Contrast
/// `wrapped_bound_names`, which DOES wrap-prefix (it matches against the
/// WRAPPED open `param_space()`, a different coordinate system than this
/// manifest-recording helper needs).
pub fn raw_bound_defaults(signal: &Composite) -> Vec<(String, Scalar)> {
signal.bound_param_space().into_iter().map(|b| (b.name, b.value)).collect()
}
/// The honest broker label for the dual-tap r-sma harness: it runs a RiskExecutor
@@ -506,7 +504,7 @@ pub fn run_signal_r(
.iter()
.map(|p| p.name.clone())
.collect();
let defaults = wrapped_bound_defaults(&signal); // read before `signal` is consumed below
let defaults = raw_bound_defaults(&signal); // read before `signal` is consumed below
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
@@ -580,7 +578,7 @@ pub fn run_blueprint_member(
cost: &[aura_research::CostSpec],
instrument: &str,
) -> RunReport {
let defaults = wrapped_bound_defaults(&signal); // read before `signal` is consumed below
let defaults = raw_bound_defaults(&signal); // read before `signal` is consumed below
let (tx_eq, rx_eq) = mpsc::channel();
let (tx_ex, rx_ex) = mpsc::channel();
let (tx_r, rx_r) = mpsc::channel();
@@ -706,55 +704,72 @@ pub fn wrapped_bound_names(signal: &Composite) -> HashSet<String> {
.collect()
}
/// The override subset of `names` (#246): every WRAPPED-coordinate name
/// missing the un-reopened wrapped OPEN space but naming a BOUND param of the
/// strategy — returned in STRATEGY coordinates (the wrap prefix
/// `<signal.name()>.` stripped) for `Composite::reopen`. Names matching
/// neither space are skipped here; the caller decides whether that is an
/// error (sweep boundary) or falls through to its existing resolution
/// errors. The silent variant `reproduce_family_in` uses over the RECORDED
/// manifest param names (the sweep boundary uses the stricter
/// `override_paths`, which errors on an unmatched name instead). Contrast
/// `axes::raw_bound_overrides_of`, whose `names` are already RAW (a campaign
/// document's own namespace, #203) and needs no such stripping.
/// The override subset of `names` (#246): every name missing the un-reopened
/// wrapped OPEN space but naming a BOUND param of the strategy — returned in
/// STRATEGY coordinates (the wrap segment stripped) for `Composite::reopen`.
/// Names matching neither space are skipped here; the caller decides whether
/// that is an error (sweep boundary) or falls through to its existing
/// resolution errors. The silent variant `reproduce_family_in`/`runner.rs`
/// use over the RECORDED manifest param names (the sweep boundary uses the
/// stricter `override_paths`, which errors on an unmatched name instead).
///
/// #328: matches via [`crate::axes::raw_matches_wrapped`], so `names` may be
/// WRAPPED (every route this cycle leaves untouched) OR RAW (a `Sweep` family
/// minted by `blueprint_sweep_family`, whose manifest now records the raw
/// campaign namespace, #328) — `reproduce_family_in` reproduces families from
/// either route generically, so this helper must tolerate both shapes rather
/// than assume one. Contrast `axes::raw_bound_overrides_of`, whose `names` are
/// ALWAYS RAW (a campaign document's own namespace, #203) by contract.
pub fn wrapped_bound_overrides_of(
names: &[String],
open_space: &[ParamSpec],
signal: &Composite,
) -> Vec<String> {
let open: HashSet<&str> = open_space.iter().map(|p| p.name.as_str()).collect();
let prefix = format!("{}.", signal.name());
let bound = wrapped_bound_names(signal);
names
.iter()
.filter(|n| !open.contains(n.as_str()) && bound.contains(*n))
.map(|n| n[prefix.len()..].to_string())
.filter(|n| !open_space.iter().any(|p| crate::axes::raw_matches_wrapped(n, &p.name)))
.filter_map(|n| {
bound
.iter()
.find(|b| crate::axes::raw_matches_wrapped(n, b))
.map(|b| crate::axes::wrapped_to_raw_axis(b).to_string())
})
.collect()
}
/// The sweep-boundary variant (#246): like `wrapped_bound_overrides_of`, but
/// an axis matching NEITHER the open nor the bound space is the error — the
/// honest replacement of the retired "fully bound; nothing to sweep" refusal.
///
/// #328: matches via [`crate::axes::raw_matches_wrapped`] rather than exact
/// wrapped-name equality, so a RAW incoming name (`fast.length`) resolves
/// against the WRAPPED open/bound space exactly like an exact wrapped hit
/// (`sma_signal.fast.length`) would — the one shared suffix convention
/// `axes.rs` defines, reused rather than re-derived here. This is the
/// resolution mechanism, not an acceptance gate (contrast the CLI's own
/// `--axis` intake, which refuses a wrapped name before this ever runs on the
/// sweep-verb route); callers whose axes are not intake-filtered this cycle
/// (the walk-forward routes) keep accepting either form unchanged.
pub fn override_paths(
axes: &[(String, Vec<Scalar>)],
open_space: &[ParamSpec],
signal: &Composite,
) -> Result<Vec<String>, String> {
let open: HashSet<&str> = open_space.iter().map(|p| p.name.as_str()).collect();
let prefix = format!("{}.", signal.name());
let bound = wrapped_bound_names(signal);
let mut overrides = Vec::new();
for (name, _) in axes {
if open.contains(name.as_str()) {
if open_space.iter().any(|p| crate::axes::raw_matches_wrapped(name, &p.name)) {
continue;
}
if bound.contains(name) {
overrides.push(name[prefix.len()..].to_string());
} else {
return Err(format!(
"axis {name}: names no param of this blueprint (open or bound) — \
see `aura sweep <bp> --list-axes`"
));
match bound.iter().find(|b| crate::axes::raw_matches_wrapped(name, b)) {
Some(b) => overrides.push(crate::axes::wrapped_to_raw_axis(b).to_string()),
None => {
return Err(format!(
"axis {name}: names no param of this blueprint (open or bound) — \
see `aura sweep <bp> --list-axes`"
));
}
}
}
Ok(overrides)
+26 -1
View File
@@ -9,6 +9,7 @@
use std::collections::BTreeMap;
use std::sync::mpsc;
use aura_core::Scalar;
use aura_engine::{blueprint_from_json, window_of};
use aura_registry::{group_families, Family, FamilyKind, Registry};
use aura_backtest::point_from_params;
@@ -142,7 +143,31 @@ pub fn reproduce_family_in(
});
}
let space = crate::member::wrap_r(reload()?, tx_eq, tx_ex, tx_r, tx_req, stop, true, SYNTHETIC_PIP_SIZE, &binding, None).param_space();
let point = point_from_params(&space, &stored.manifest.params)
// #328: `stored.manifest.params` is RAW on every mint route now (the
// synthetic sweep family and, since this cycle's tidy fix, the
// real/campaign route too) — but a family minted before this fix may
// still carry WRAPPED names on disk (C29: no retroactive rewrite of a
// registered artifact), so `reproduce_family_in` reproduces either
// shape generically: each recorded name is translated onto its
// matching WRAPPED `space` slot (`raw_matches_wrapped`, tolerant of
// both shapes) before `point_from_params`, which still keys by the
// exact wrapped name. Non-axis stamps (`stop_length`, `cost[k].<knob>`)
// carry no wrap segment either way and translate to themselves (no
// `space` hit).
let wrapped_params: Vec<(String, Scalar)> = stored
.manifest
.params
.iter()
.map(|(n, v)| {
let wrapped = space
.iter()
.find(|p| crate::axes::raw_matches_wrapped(n, &p.name))
.map(|p| p.name.clone())
.unwrap_or_else(|| n.clone());
(wrapped, *v)
})
.collect();
let point = point_from_params(&space, &wrapped_params)
.map_err(|m| RunnerError { exit_code: 1, message: m })?;
// A MonteCarlo member carries no tuning params (the params-join is empty), so its
// reproduce line would print a BLANK member label; the seed IS its realization
+38 -2
View File
@@ -116,6 +116,21 @@ impl MemberRunner for DefaultMemberRunner<'_> {
.param_space();
let point = bind_axes(&space, &cell.strategy_id, params)?;
let signal = reopen_all(signal, &overrides);
// manifest.params records RAW names (#328: the real/campaign route was
// the one mint left wrapped after the synthetic route's own switch) —
// 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 (`space` itself stays wrapped for `bind_axes`
// above, which still keys by the exact wrapped `param_space()` name).
let manifest_space: Vec<aura_core::ParamSpec> = space
.iter()
.map(|p| aura_core::ParamSpec {
name: crate::axes::wrapped_to_raw_axis(&p.name).to_string(),
kind: p.kind,
})
.collect();
// The member's resolved input binding (campaign data.bindings
// overrides win over name defaults). A refusal is a member fault,
@@ -173,7 +188,7 @@ impl MemberRunner for DefaultMemberRunner<'_> {
let mut report = run_blueprint_member(
signal,
&point,
&space,
&manifest_space,
sources,
(from, to),
0,
@@ -490,7 +505,28 @@ pub fn persist_campaign_traces(
// is the member report's own `manifest.window` — already
// epoch-ns (`run_blueprint_member` stamped the post-seam
// bounds), so no second ms->ns crossing here.
let point = point_from_params(&space, &member_report.manifest.params)?;
//
// #328: `member_report.manifest.params` is RAW (this cycle's tidy
// fix put the real/campaign mint on the raw frame too), but
// `point_from_params` still keys by the exact WRAPPED `space`
// name — translate each recorded name onto its matching wrapped
// slot first (`raw_matches_wrapped`, tolerant of either shape, the
// same `reproduce_family_in` recipe), never re-resolving by a
// renamed identity.
let wrapped_params: Vec<(String, Scalar)> = member_report
.manifest
.params
.iter()
.map(|(n, v)| {
let wrapped = space
.iter()
.find(|p| crate::axes::raw_matches_wrapped(n, &p.name))
.map(|p| p.name.clone())
.unwrap_or_else(|| n.clone());
(wrapped, *v)
})
.collect();
let point = point_from_params(&space, &wrapped_params)?;
let (from, to) = member_report.manifest.window;
let no_data = || {
format!(
+37 -13
View File
@@ -40,7 +40,7 @@ the same shape every node in `aura-std` already follows.
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 <bp>.fast.length=2,4,8` overrides them
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
bound defaults alike, so every override target is discoverable.
- **A project-specific *native* node type** — the moment §0's three-part
@@ -284,7 +284,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 —
wrapped like any knob, e.g. `graph.channel_length` — appears in `--list-axes`).
e.g. `channel_length` — appears in `--list-axes`).
### Worked example: declaring a measurement tap
@@ -344,6 +344,14 @@ separate in-session bool — `bars_since_open` alone is the contract. DST is
handled by `chrono-tz`, so the same local minute reads the same index in
summer (CEST) and winter (CET).
The index is derived from the clock (`ctx.now()`), never counted from trigger
firings — so the trigger's cadence only sets how *often* the node emits, not
what it emits. Feeding a denser stream (e.g. a raw m1 `price`) is equally
valid: the node fires once per input tick and reports the same
`period_minutes`-bar index throughout that bar. The once-per-bar wiring above
is the convention when you want exactly one emission per completed bar; it is
not a correctness requirement.
Wire it like any node —
`{"op":"add","type":"SessionFrankfurt","bind":{"period_minutes":{"I64":15}}}`,
feed its `trigger` from a once-per-bar stream, and read `bars_since_open`.
@@ -405,23 +413,39 @@ slow.length:I64
bias.scale:F64
```
These printed names are the **raw param-space namespace**: op-script params
and a campaign document's `strategies[].axes` keys (§3) share this one raw
form. There is a third, *wrapped* surface: the dissolved `aura sweep
<blueprint> --axis` CLI (glossary `sweep`) accepts only the names `aura
sweep <blueprint> --list-axes` prints — the raw name prefixed with the
root-composite instance name, `graph.<param>` — never the raw form; the
campaign document the sweep sugar generates still stores the raw form
(#210):
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):
```
raw (--params, campaign axes): fast.length
wrapped (--list-axes, --axis): graph.fast.length
$ 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
```
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:
```
$ 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:
axis "graph.fast.length" is not in the param space; axis names are raw
node.param paths — did you mean "fast.length"?
```
A ganged knob's raw address has one path segment less than a member address
would — it sits at the composite's own level, like a role name (e.g.
`channel_length`, not `channel_hi.length`) — and wraps identically (`graph.channel_length`).
`channel_length`, not `channel_hi.length`).
`--content-id`, `--identity-id`, `--params`, and `graph register` all accept
**either** shape: the raw op-script array or an already-built `#155`
+12
View File
@@ -133,6 +133,18 @@ show <content-id>` prints a registered document's canonical bytes (#300), so the
generate → retrieve → hand-extend → re-register loop needs no direct store
filesystem access.
**The bound-override coincidence (#246, ratified #328).** A campaign axis naming a
**bound** param of the referenced blueprint is deliberately accepted alongside one
naming an open param: `validate_campaign_refs` checks each axis's name against
`param_space()` **OR** `bound_param_space()` — the bound value stands as the
axis's default, and the axis's own values re-open it for that campaign. There is
no schema flag distinguishing the two cases (no `open`/`bound` marker on the axis
entry); name coincidence against one of the two namespaces IS the mechanism, and
the referenced blueprint is the single source of truth for which namespace a name
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
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
+8 -3
View File
@@ -128,9 +128,14 @@ there is no default; pin a knob you do not want to vary with a single-value axis
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). The names are prefixed by the current wrapping
(`sma_signal.fast.length`, the nested-composite prefix), which is why discovery lives
on the sweep verb (it owns the wrapping), not `graph introspect`. `--trace` on
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
+2 -2
View File
@@ -331,7 +331,7 @@ 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. `aura sweep --axis` takes the `--list-axes`-printed, root-composite-wrapped name (e.g. `graph.fast.length`) — not the raw `param_space` name (`fast.length`) that `graph introspect --params` and a campaign document's axes use; the CLI strips exactly one leading wrapper segment (#210). 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 (`--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.
### 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)
@@ -347,7 +347,7 @@ The `content id` of a run's signal blueprint in its run-record role: stamped int
### use (op)
**Avoid:** —
The construction op that splices a **registered** blueprint into the building graph as a nested `composite` under a chosen instance name (`{"op":"use","ref":{"content_id"|"name":…},"name":…,"bind":{…}}`, #317): the reference — a content id, a unique content-id prefix, or a `blueprint label` — resolves CLI-side, before the engine ever sees it; the fetched composite is C29-gated, spliced, and its resolution echoed on stderr (`aura: note:`, the existing benign marker). The emitted blueprint carries the splice inline as an ordinary nested composite — no reference or registry dependency survives into the artifact. An instance's open input roles become its `<instance>.<role>` in-ports, its output boundary fields its `<instance>.<field>` out-fields — the existing nested-composite param-prefix discipline extends unchanged (`graph.<instance>.<node>.<param>` on `sweep --list-axes`). Pairs with the `input` op: a pattern meant to be `use`d declares its formal parameters as open `input` roles, left unbound at `finish()` (#317's open-pattern amendment) — only running it standalone requires them bound.
The construction op that splices a **registered** blueprint into the building graph as a nested `composite` under a chosen instance name (`{"op":"use","ref":{"content_id"|"name":…},"name":…,"bind":{…}}`, #317): the reference — a content id, a unique content-id prefix, or a `blueprint label` — resolves CLI-side, before the engine ever sees it; the fetched composite is C29-gated, spliced, and its resolution echoed on stderr (`aura: note:`, the existing benign marker). The emitted blueprint carries the splice inline as an ordinary nested composite — no reference or registry dependency survives into the artifact. An instance's open input roles become its `<instance>.<role>` in-ports, its output boundary fields its `<instance>.<field>` out-fields — the existing nested-composite param-prefix discipline extends unchanged (`<instance>.<node>.<param>`, the raw axis form since #328). Pairs with the `input` op: a pattern meant to be `use`d declares its formal parameters as open `input` roles, left unbound at `finish()` (#317's open-pattern amendment) — only running it standalone requires them bound.
### veto
**Avoid:** risk-manager
@@ -0,0 +1,109 @@
# Fieldtest transcript — cycle #328 (axis-namespace reconciliation)
Per-cycle field test of the one-raw-namespace axis surface: the raw
`<node>.<param>` form as the only user-facing sweep-axis name across every
surface (discovery, intake on all three verbs, run manifests, campaign
documents), with the wrapped `<blueprint>.<node>.<param>` form retired behind
translation refusals.
Binary: `target/release/aura`, built from HEAD `4474814` via
`cargo build --release -p aura-cli` (Finished in 5.45s — HEAD builds clean).
Scratch project `aura new ax_lab` in `/tmp/axwork/ax_lab`; real data from the
built-in archive (US500). Public interface only (README, authoring-guide,
glossary, design ledger, `aura … --help`) — no crate source read.
Base blueprint `cycle328_crossover`: an SMA-crossover → bias signal with a
deliberate mix — `fast.length` **open**, `slow.length` **bound** (default=6),
`bias.scale` **bound** (default=0.5) — so both discovery line shapes and the
bound-override contract are exercised. Content id
`f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240`.
---
## Example 1 — discovery-surface identity (`--params` vs `--list-axes`)
```
$ aura graph introspect --params cycle328_crossover.bp.json
fast.length:I64
slow.length:I64 default=6
bias.scale:F64 default=0.5
$ aura sweep cycle328_crossover.bp.json --list-axes
fast.length:I64
slow.length:I64 default=6
bias.scale:F64 default=0.5
$ diff params.txt listaxes.txt # → IDENTICAL (no diff)
```
Byte-identical, raw `<node>.<param>` form, bound params carrying `default=<v>`.
`--params` discovers bound params too. (Finding W1.)
## Example 2 — document-first loop (paste verbatim, incl. bound-override)
Axes pasted verbatim from `--params` into a hand-written campaign document
(`cycle328_2_campaign.json`), including two bound-override axes (`slow.length`,
`bias.scale` — bound defaults re-opened by name, #246 coincidence contract):
```
$ aura campaign validate cycle328_2_campaign.json # inside project, refs registered
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 campaign run cycle328_2_campaign.json # exit 0
aura: campaign run 0 recorded: 1 cells
# manifest.params (raw): fast.length, slow.length, bias.scale (+ stop knobs)
```
All three tiers pass; the bound-override axes validate; the run manifest records
raw names. (Finding W2.)
## Example 3 — translation seams (wrapped form on every intake)
```
$ aura sweep cycle328_crossover.bp.json --axis graph.fast.length=2,4 ... (exit 2)
$ aura sweep cycle328_crossover.bp.json --real US500 --axis graph.fast.length=... (exit 2)
$ aura walkforward cycle328_crossover.bp.json --real US500 --axis graph.fast.length=... (exit 2)
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 cycle328_3d_wrapped_campaign.json (exit 1)
aura: campaign references do not resolve:
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: axis "graph.fast.length" is not in the param space; axis names are raw node.param paths — did you mean "fast.length"?
```
All three verbs refuse identically, naming the raw candidate; campaign validate
appends a did-you-mean. Note the campaign-validate strategy identifier is the
FULL 64-hex content id, where the authoring guide's transcript shows a 4-hex
`strategy 9f3a:` placeholder (Finding SG1). Edge probes:
```
$ aura sweep ... --axis fastt.length=2,4 # typo
aura: axis fastt.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
$ aura sweep ... --axis graph.fast.bogus=1 # wrapped, tail not a param
aura: axis graph.fast.bogus: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
$ aura sweep ... --axis project.graph.fast.length=2 # two leading segments (> one)
aura: axis project.graph.fast.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
```
Typos and >1-segment wrappings fall to a clean generic error — consistent with
the documented "one leading segment strips to a hit" contract. (Finding W3.)
## Example 4 — manifest raw names + reproduce
```
$ aura sweep cycle328_crossover.bp.json --axis fast.length=2,4 --axis slow.length=8,16 --name cycle328-sweep-bo
# each member manifest:
# params: [["fast.length",..],["slow.length",..],["stop_length",..],["stop_k",..]] (raw)
# defaults: [["bias.scale",{"F64":0.5}]] (raw)
$ aura reproduce cycle328-sweep-bo-0 (exit 0)
cycle328-sweep-bo-0 member fast.length=2, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
... reproduced 4/4 members bit-identically
```
Positive real/walkforward routes also record raw (params AND defaults) and run
clean (exit 0): `cycle328_6a_sweep_real.*`, `cycle328_6b_walkforward.*`. No
wrapped name appears on any surface. (Finding W4.)
---
Not exercised: the "old wrapped-name manifests remain reproducible" backward-
compat claim (no pre-#328 on-disk family is reachable through the public
interface to a source-blind consumer).
@@ -0,0 +1,3 @@
fast.length:I64
slow.length:I64 default=6
bias.scale:F64 default=0.5
@@ -0,0 +1,3 @@
fast.length:I64
slow.length:I64 default=6
bias.scale:F64 default=0.5
@@ -0,0 +1,22 @@
{
"format_version": 1,
"kind": "campaign",
"name": "cycle328-doc-first-boundoverride",
"seed": 7,
"data": {
"instruments": ["US500"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1725580800000 } ]
},
"strategies": [
{
"ref": { "content_id": "f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240" },
"axes": {
"fast.length": { "kind": "I64", "values": [2, 4] },
"slow.length": { "kind": "I64", "values": [8, 16] },
"bias.scale": { "kind": "F64", "values": [0.5] }
}
}
],
"process": { "ref": { "content_id": "f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9" } },
"presentation": { "persist_taps": [], "emit": ["family_table"] }
}
@@ -0,0 +1 @@
{"format_version":1,"kind":"process","name":"ax-sweep-select","description":"Sweep the grid and select the argmax-SQN cell.","pipeline":[{"block":"std::sweep","metric":"sqn","select":"argmax"}]}
@@ -0,0 +1 @@
aura: campaign run 0 recorded: 1 cells
@@ -0,0 +1,5 @@
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":46.422499999968466,"max_drawdown":125.89999999998915,"bias_sign_flips":861,"r":{"expectancy_r":0.052960279961470944,"n_trades":846,"win_rate":0.31796690307328607,"avg_win_r":1.8016249253621428,"avg_loss_r":-0.7622750573223781,"profit_factor":1.1018669053593053,"max_r_drawdown":29.11884107504975,"n_open_at_end":1,"sqn":0.8388429079176243,"sqn_normalized":0.2884001324999155,"net_expectancy_r":0.052960279961470944,"conviction_terciles_r":[-0.05618014716431794,0.13546662237309962,0.07959436467563116]}}}}
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":23.387499999972977,"max_drawdown":113.9349999999979,"bias_sign_flips":521,"r":{"expectancy_r":0.09527242267306019,"n_trades":529,"win_rate":0.3005671077504726,"avg_win_r":2.0866957092558716,"avg_loss_r":-0.7605013680476617,"profit_factor":1.179110545808399,"max_r_drawdown":26.058017560225167,"n_open_at_end":1,"sqn":1.126033935020108,"sqn_normalized":0.4895799717478731,"net_expectancy_r":0.09527242267306019,"conviction_terciles_r":[-0.07002826150651001,0.25569891492505703,0.10011907679313316]}}}}
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":80.28249999994746,"max_drawdown":87.96499999997366,"bias_sign_flips":780,"r":{"expectancy_r":0.13213069018419635,"n_trades":821,"win_rate":0.3617539585870889,"avg_win_r":1.848016846738723,"avg_loss_r":-0.8404231046568231,"profit_factor":1.2463301607589143,"max_r_drawdown":28.220480388404972,"n_open_at_end":1,"sqn":1.8795915057542347,"sqn_normalized":0.655981958519425,"net_expectancy_r":0.13213069018419635,"conviction_terciles_r":[0.11958074358694365,0.22575289096933593,0.05101263327150186]}}}}
{"family_id":"844fe329-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["bias.scale",{"F64":0.5}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[],"window":[1725148800000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":19.558750000014825,"max_drawdown":85.75000000000286,"bias_sign_flips":399,"r":{"expectancy_r":0.11821373408670934,"n_trades":479,"win_rate":0.31315240083507306,"avg_win_r":2.3901102129922043,"avg_loss_r":-0.9176053292440638,"profit_factor":1.1875649234489034,"max_r_drawdown":28.108663585145585,"n_open_at_end":1,"sqn":1.0959164423645071,"sqn_normalized":0.5007370035157714,"net_expectancy_r":0.11821373408670934,"conviction_terciles_r":[0.2576229784937875,0.13377757213522506,-0.035888040591340105]}}}}
{"campaign_run":{"campaign":"844fe3290562346f37c756a74f3d76bab9a3c7257341ec8dafec0025ac2f68db","process":"f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9","run":0,"seed":7,"cells":[{"strategy":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","instrument":"US500","window_ms":[1725148800000,1725580800000],"stages":[{"block":"std::sweep","family_id":"844fe329-0-US500-w0-r0-s0-0","selection":{"winner_ordinal":2,"params":[["bias.scale",{"F64":0.5}],["fast.length",{"I64":4}],["slow.length",{"I64":8}]],"selection":{"selection_metric":"sqn","n_trials":4,"raw_winner_metric":1.8795915057542347,"mode":"Argmax"}}}],"coverage":{"effective_from_ms":1725235200000,"effective_to_ms":1725580800000}}]}}
@@ -0,0 +1,3 @@
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
@@ -0,0 +1 @@
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
@@ -0,0 +1 @@
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
@@ -0,0 +1 @@
aura: axis "graph.fast.length": axis names are raw node.param paths — use "fast.length" (the wrapped --list-axes form was retired, #328)
@@ -0,0 +1,4 @@
campaign document valid (intrinsic): 1 strategy(ies), 1 axes (2 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)
aura: campaign references do not resolve:
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: axis "graph.fast.length" is not in the param space; axis names are raw node.param paths — did you mean "fast.length"?
strategy f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240: open param "fast.length" is bound by no campaign axis
@@ -0,0 +1,20 @@
{
"format_version": 1,
"kind": "campaign",
"name": "cycle328-wrapped-axis-in-doc",
"seed": 7,
"data": {
"instruments": ["US500"],
"windows": [ { "from_ms": 1725148800000, "to_ms": 1725580800000 } ]
},
"strategies": [
{
"ref": { "content_id": "f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240" },
"axes": {
"graph.fast.length": { "kind": "I64", "values": [2, 4] }
}
}
],
"process": { "ref": { "content_id": "f14023a1fef5640311163690f1140532412ea5e1d1cbb93e293e63fcf0bf2bf9" } },
"presentation": { "persist_taps": [], "emit": ["family_table"] }
}
@@ -0,0 +1,2 @@
{"family_id":"844fe329-0-US500-w0-r0-s0-0","kind":"Sweep","members":4}
{"family_id":"cycle328-sweep-bo-0","kind":"Sweep","members":4}
@@ -0,0 +1,4 @@
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.23484999999997555,"max_drawdown":0.38129999999997777,"bias_sign_flips":2,"r":{"expectancy_r":0.05853072923940165,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":1.127161364109783,"avg_loss_r":-0.47578458819578906,"profit_factor":1.18452908319715,"max_r_drawdown":0.2739524763392649,"n_open_at_end":1,"sqn":0.1070277396717771,"sqn_normalized":0.1070277396717771,"net_expectancy_r":0.05853072923940165,"conviction_terciles_r":[-0.2739524763392649,1.127161364109783,-0.6776167000523132]}}}
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.037062500000010136,"max_drawdown":0.04543749999999568,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.5274999999999815,"max_drawdown":0.5609499999999865,"bias_sign_flips":2,"r":{"expectancy_r":-0.6426407768510923,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":0.5474739899790243,"avg_loss_r":-1.2376981602661505,"profit_factor":0.22116619687844463,"max_r_drawdown":1.245647494511426,"n_open_at_end":1,"sqn":-1.0799322573025962,"sqn_normalized":-1.0799322573025962,"net_expectancy_r":-0.6426407768510923,"conviction_terciles_r":[-1.2456474945114262,0.5474739899790243,-1.229748826020875]}}}
{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.0076125000000051846,"max_drawdown":0.02868749999999659,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}
@@ -0,0 +1,5 @@
cycle328-sweep-bo-0 member fast.length=2, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
cycle328-sweep-bo-0 member fast.length=2, slow.length=16, stop_length=3, stop_k=2 reproduced: bit-identical
cycle328-sweep-bo-0 member fast.length=4, slow.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
cycle328-sweep-bo-0 member fast.length=4, slow.length=16, stop_length=3, stop_k=2 reproduced: bit-identical
reproduced 4/4 members bit-identically
@@ -0,0 +1,4 @@
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.23484999999997555,"max_drawdown":0.38129999999997777,"bias_sign_flips":2,"r":{"expectancy_r":0.05853072923940165,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":1.127161364109783,"avg_loss_r":-0.47578458819578906,"profit_factor":1.18452908319715,"max_r_drawdown":0.2739524763392649,"n_open_at_end":1,"sqn":0.1070277396717771,"sqn_normalized":0.1070277396717771,"net_expectancy_r":0.05853072923940165,"conviction_terciles_r":[-0.2739524763392649,1.127161364109783,-0.6776167000523132]}}}}
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.037062500000010136,"max_drawdown":0.04543749999999568,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}}
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":-0.5274999999999815,"max_drawdown":0.5609499999999865,"bias_sign_flips":2,"r":{"expectancy_r":-0.6426407768510923,"n_trades":3,"win_rate":0.3333333333333333,"avg_win_r":0.5474739899790243,"avg_loss_r":-1.2376981602661505,"profit_factor":0.22116619687844463,"max_r_drawdown":1.245647494511426,"n_open_at_end":1,"sqn":-1.0799322573025962,"sqn_normalized":-1.0799322573025962,"net_expectancy_r":-0.6426407768510923,"conviction_terciles_r":[-1.2456474945114262,0.5474739899790243,-1.229748826020875]}}}}
{"family_id":"cycle328-sweep-bo-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":16}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1,18],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=0.0001)","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":0.0076125000000051846,"max_drawdown":0.02868749999999659,"bias_sign_flips":0,"r":{"expectancy_r":0.1570020451313814,"n_trades":1,"win_rate":1.0,"avg_win_r":0.1570020451313814,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":0.0,"sqn_normalized":0.0,"net_expectancy_r":0.1570020451313814,"conviction_terciles_r":[0.0,0.0,0.0]}}}}
@@ -0,0 +1 @@
aura: axis fastt.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
@@ -0,0 +1 @@
aura: axis graph.fast.bogus: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
@@ -0,0 +1 @@
aura: axis project.graph.fast.length: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
@@ -0,0 +1,2 @@
{"family_id":"1102542f-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":2}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":46.422499999968466,"max_drawdown":125.89999999998915,"bias_sign_flips":861,"r":{"expectancy_r":0.052960279961470944,"n_trades":846,"win_rate":0.31796690307328607,"avg_win_r":1.8016249253621428,"avg_loss_r":-0.7622750573223781,"profit_factor":1.1018669053593053,"max_r_drawdown":29.11884107504975,"n_open_at_end":1,"sqn":0.8388429079176243,"sqn_normalized":0.2884001324999155,"net_expectancy_r":0.052960279961470944,"conviction_terciles_r":[-0.05618014716431794,0.13546662237309962,0.07959436467563116]}}}}
{"family_id":"1102542f-0-US500-w0-r0-s0-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":80.28249999994746,"max_drawdown":87.96499999997366,"bias_sign_flips":780,"r":{"expectancy_r":0.13213069018419635,"n_trades":821,"win_rate":0.3617539585870889,"avg_win_r":1.848016846738723,"avg_loss_r":-0.8404231046568231,"profit_factor":1.2463301607589143,"max_r_drawdown":28.220480388404972,"n_open_at_end":1,"sqn":1.8795915057542347,"sqn_normalized":0.655981958519425,"net_expectancy_r":0.13213069018419635,"conviction_terciles_r":[0.11958074358694365,0.22575289096933593,0.05101263327150186]}}}}
@@ -0,0 +1,2 @@
{"family_id":"3fbe4493-0-US500-w0-r0-s1-0","report":{"manifest":{"commit":"4474814fa79e227cd2d93e34a6982b49f31ba846","params":[["fast.length",{"I64":4}],["slow.length",{"I64":8}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["bias.scale",{"F64":0.5}]],"window":[1730986785000000000,1732903979999000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","selection":{"selection_metric":"sqn_normalized","n_trials":2,"raw_winner_metric":-0.047813708639866226,"mode":"Argmax","deflated_score":-0.2491637240312123,"overfit_probability":0.9090909090909091,"n_resamples":1000,"block_len":5,"seed":0},"instrument":"US500","topology_hash":"f5fdf729d0333f0286c19d57143fb7933137f27a6128e72489c508096c7a2240","project":{"commit":"6bb76063046d41062b68fbde79950b9d06fbd41d"}},"metrics":{"total_pips":86.88750000012675,"max_drawdown":212.54749999991324,"bias_sign_flips":3244,"r":{"expectancy_r":0.030790024810340468,"n_trades":3446,"win_rate":0.3444573418456181,"avg_win_r":1.6171611559204189,"avg_loss_r":-0.8027746200004905,"profit_factor":1.0585080262251694,"max_r_drawdown":132.69624561269114,"n_open_at_end":1,"sqn":0.9012552216989966,"sqn_normalized":0.15352890674784425,"net_expectancy_r":0.030790024810340468,"conviction_terciles_r":[0.03406326248492066,0.038379965173779666,0.019929695543143295]}}}}
{"walkforward":{"oos_r":{"avg_loss_r":-0.8027746200004905,"avg_win_r":1.6171611559204189,"conviction_terciles_r":[0.0,0.0,0.0],"expectancy_r":0.030790024810340468,"max_r_drawdown":132.69624561269114,"n_open_at_end":0,"n_trades":3446,"net_expectancy_r":0.030790024810340468,"profit_factor":1.0585080262251694,"sqn":0.9012552216989966,"sqn_normalized":0.15352890674784425,"win_rate":0.3444573418456181},"param_stability":[{"mean":4.0,"p25":4.0,"p5":4.0,"p50":4.0,"p75":4.0,"p95":4.0},{"mean":8.0,"p25":8.0,"p5":8.0,"p50":8.0,"p75":8.0,"p95":8.0},{"mean":3.0,"p25":3.0,"p5":3.0,"p50":3.0,"p75":3.0,"p95":3.0},{"mean":2.0,"p25":2.0,"p5":2.0,"p50":2.0,"p75":2.0,"p95":2.0}],"stitched_total_pips":86.88750000012675,"windows":1}}
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","doc":"SMA crossover bias: fast minus slow moving average, scaled to a bounded directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":6}}]}},{"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"}]}}
@@ -0,0 +1,13 @@
[
{"op":"doc","text":"SMA crossover bias: fast minus slow moving average, scaled to a bounded directional bias"},
{"op":"source","role":"price","kind":"F64"},
{"op":"add","type":"SMA","name":"fast"},
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":6}}},
{"op":"add","type":"Sub","name":"sub"},
{"op":"add","type":"Bias","name":"bias","bind":{"scale":{"F64":0.5}}},
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
{"op":"connect","from":"fast.value","to":"sub.lhs"},
{"op":"connect","from":"slow.value","to":"sub.rhs"},
{"op":"connect","from":"sub.value","to":"bias.signal"},
{"op":"expose","from":"bias.bias","as":"bias"}
]
@@ -0,0 +1,134 @@
# Fieldtest transcript — milestone-37 (data-authorability boundary)
Milestone-scope field test: the three data-authorability sites as ONE
downstream story — fold selection as run-mode data (#310), a registered
composite spliced by name (#317), construction args configuring non-scalar
structure (#271) — and the boundary refusing what stays Rust-only.
Binary: `target/release/aura`, built from HEAD `eb2b0a1` via
`cargo build --workspace --release` (Finished in 5.53s, incremental — HEAD
builds clean). Scratch project `aura new m37_lab` in `/tmp/m37_lab`; real data
from the built-in Pepperstone archive. Public interface only (README,
authoring-guide, glossary, design ledger, `aura … --help`, `graph introspect`) —
no crate source read.
---
## Scenario 1 — `m37_1_ny_momentum` — Session args + fold-selected taps, e2e
A NY first-session-bar momentum strategy authored purely as data: `Session`
configured to `America/New_York` / `09:30` through the `args` channel, gating a
SMA(5)SMA(20) sign onto the first 15m session bar. Four declared taps
(`spread`, `first_bar`, `gated_signal`, `slow_ma`).
```
$ aura graph build < m37_1_ny_momentum.ops.json > m37_1_ny_momentum.bp.json
$ head -c 25 m37_1_ny_momentum.bp.json
{"format_version":2,"blue # arg-bearing Session -> v2
$ aura graph introspect --params m37_1_ny_momentum.ops.json
# (empty = closed, runnable)
$ aura graph introspect --content-id --identity-id < m37_1_ny_momentum.ops.json
c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1
31de7ebd09122b9cef690618dbbdc08257056745b948840734982cd62a611265
# default plan (record all four taps):
$ aura run m37_1_ny_momentum.bp.json --real US500 --from 1725148800000 --to 1725580800000
{"manifest":{... "topology_hash":"c8e123f0..." ...},"metrics":{"total_pips":-2.099...,
"r":{"expectancy_r":0.1566...,"n_trades":10, ...}}} (exit 0, no stderr)
# fold-selected plan: spread=mean, first_bar=count, gated_signal=last; slow_ma unlisted
$ aura run m37_1_ny_momentum.bp.json --real US500 --from 1725148800000 --to 1725580800000 \
--tap spread=mean --tap first_bar=count --tap gated_signal=last
{"manifest":{...IDENTICAL manifest+metrics as the default run...}} (exit 0)
# stderr: aura: note: declared tap "slow_ma" unbound this run
```
The fold summary rows (the reason to pick a fold over `record`) land ONLY in
the trace store, not on the run's stdout (see `m37_1_fold_summary_rows.txt`):
```
runs/traces/graph/index.json "taps":["spread","first_bar","gated_signal"] # slow_ma excluded
spread.json {"tap":"spread","kinds":["F64"],"ts":[...],"columns":[[-0.2033544...]]} # mean
first_bar.json {"tap":"first_bar","kinds":["I64"],"ts":[...],"columns":[[5239.0]]} # count = warm rows
gated_signal.json {"tap":"gated_signal","kinds":["F64"],"ts":[...],"columns":[[0.0]]} # last
```
`aura run` prints only manifest+metrics; `aura chart graph` emits HTML (exit 0),
index-gated (the stale `slow_ma.json` from the default run lingers on disk but
is NOT in the index and does NOT leak into the chart). There is no CLI verb that
prints a fold's scalar summary as text. (Findings F1, SG1.)
---
## Scenario 2 — `m37_2` — registered composite spliced by name + args, swept
A London-open session anchor (open `input` pattern, `Session``Europe/London`
/ `08:00` via `args`) registered under a label, then spliced by name into a
swept consumer.
```
$ aura graph build < m37_2_london_anchor.ops.json > m37_2_london_anchor.bp.json # v2
$ aura graph register m37_2_london_anchor.bp.json --name london_anchor
registered blueprint 2deeddd943c9...
label "london_anchor" -> 2deeddd943c9...
$ aura graph introspect --registered
london_anchor 2deeddd943c9 London cash-open session anchor: bars elapsed since the 08:00 Europe/London open
$ aura graph build < m37_2_consumer.ops.json > m37_2_consumer.bp.json
# stderr: aura: note: use "anchor": london_anchor -> 2deeddd943c9...
$ head -c 25 m37_2_consumer.bp.json
{"format_version":2,"blue # v2 propagates THROUGH the splice
$ aura graph introspect --content-id --identity-id < m37_2_consumer.ops.json
38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46
f964f3b0dadb8d04e73696bc61119222755c7feef81c55e35e5b85b91bcee7ec
$ aura sweep m37_2_consumer.bp.json --list-axes
graph.fast.length:I64
graph.slow.length:I64
graph.anchor.sess.period_minutes:I64 default=15 # spliced-instance param, path-qualified
...
$ aura sweep m37_2_consumer.bp.json --real GER40 --from 1725148800000 --to 1725580800000 \
--axis graph.fast.length=5,10 --axis graph.slow.length=20 --name m37_london
# 2 members, exit 0
$ aura reproduce d5259c94-0-GER40-w0-r0-s0-0
... member graph.fast.length=5, ... reproduced: bit-identical
... member graph.fast.length=10, ... reproduced: bit-identical
reproduced 2/2 members bit-identically
```
Everything worked first try. (Finding W3.)
---
## Scenario 3 — the boundary refuses what stays Rust-only
```
# genuinely-new logic as a namespaced type -> refused, NAMES the Rust escalation verb
$ aura graph build < m37_3a_new_logic.ops.json
aura: op 2 (add): unknown node type "my_lab::ThirdCandle" — type id looks
project-namespaced but this project binds no node crate — attach one
with `aura nodes new <name>` (exit 1)
# same, but bare (un-namespaced) type -> refused, but NO escalation pointer
$ echo '[...{"op":"add","type":"ThirdCandle",...}...]' | aura graph build
aura: op 1 (add): unknown node type "ThirdCandle" (exit 1) # F2
# arg-bearing type entered half-configured -> MissingArg
$ aura graph build < m37_3b_session_no_args.ops.json
aura: op 2 (add): node sess is missing required arg "tz" (exit 1)
# construction args on an arg-less type -> refused
$ aura graph build < m37_3c_args_on_argless.ops.json
aura: op 2 (add): node fast takes no construction args (exit 1)
# a fold that is not a registered fold -> roster-enumerating refusal
$ aura run m37_1_ny_momentum.bp.json --real US500 --from ... --to ... --tap spread=median
aura: unknown fold 'median' — available: count, first, last, max, mean, min, record, sum (exit 1)
# splice a composite that was never registered -> refused, enumerates labels
$ aura graph build < m37_3e_unknown_label.ops.json
aura: op 2 (use "anchor"): no registered blueprint labeled "tokyo_anchor"
— registered labels: london_anchor (exit 1)
```
Every boundary holds: exit 1, actionable prose. (Finding W4.)
@@ -0,0 +1,10 @@
# fold summary rows persisted by the --tap plan (spread=mean, first_bar=count, gated_signal=last)
## index.json taps:
"taps":["spread","first_bar","gated_signal"]
## spread (mean):
{"tap":"spread","kinds":["F64"],"ts":[1725580800000000000],"columns":[[-0.20335440613026667]]}
## first_bar (count):
{"tap":"first_bar","kinds":["I64"],"ts":[1725580800000000000],"columns":[[5239.0]]}
## gated_signal (last):
{"tap":"gated_signal","kinds":["F64"],"ts":[1725580800000000000],"columns":[[0.0]]}
@@ -0,0 +1 @@
{"format_version":2,"blueprint":{"name":"graph","doc":"NY first-session-bar momentum: SMA(5)-SMA(20) sign, traded only on the first 09:30 America/New_York 15m session bar","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":5}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":20}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Sign","name":"sgn"}},{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"America/New_York"},{"name":"open","value":"09:30"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}},{"primitive":{"type":"EqConst","name":"isfirst","bound":[{"pos":0,"name":"target","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Const","name":"flat","bound":[{"pos":0,"name":"value","kind":"F64","value":{"F64":0.0}}]}},{"primitive":{"type":"Select","name":"gate"}},{"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":4,"to":5,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":3,"to":7,"slot":1,"from_field":0},{"from":6,"to":7,"slot":2,"from_field":0},{"from":7,"to":8,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":4,"slot":0},{"node":6,"slot":0}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}],"taps":[{"name":"spread","from":{"node":2,"field":0}},{"name":"first_bar","from":{"node":5,"field":0}},{"name":"gated_signal","from":{"node":7,"field":0}},{"name":"slow_ma","from":{"node":1,"field":0}}]}}
@@ -0,0 +1 @@
{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[],"defaults":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["graph.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-2.0999999999994543,"max_drawdown":29.899999999999636,"bias_sign_flips":12,"r":{"expectancy_r":0.15668981035489754,"n_trades":10,"win_rate":0.3,"avg_win_r":2.6354203218756065,"avg_loss_r":-0.9056232660111208,"profit_factor":1.2471696505846321,"max_r_drawdown":5.2917804259042285,"n_open_at_end":0,"sqn":0.23053858556757656,"sqn_normalized":0.23053858556757656,"net_expectancy_r":0.15668981035489754,"conviction_terciles_r":[-0.2831414429722934,0.6217793811679744,0.13774607224048319]}}}
@@ -0,0 +1,31 @@
[
{"op": "doc", "text": "NY first-session-bar momentum: SMA(5)-SMA(20) sign, traded only on the first 09:30 America/New_York 15m session bar"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 5}}},
{"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 20}}},
{"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": "Sign", "name": "sgn"},
{"op": "connect", "from": "sub.value", "to": "sgn.value"},
{"op": "add", "type": "Session", "name": "sess",
"args": {"tz": "America/New_York", "open": "09:30"},
"bind": {"period_minutes": {"I64": 15}}},
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
{"op": "add", "type": "EqConst", "name": "isfirst", "bind": {"target": {"I64": 1}}},
{"op": "connect", "from": "sess.bars_since_open", "to": "isfirst.value"},
{"op": "add", "type": "Const", "name": "flat", "bind": {"value": {"F64": 0.0}}},
{"op": "feed", "role": "price", "into": ["flat.clock"]},
{"op": "add", "type": "Select", "name": "gate"},
{"op": "connect", "from": "isfirst.value", "to": "gate.cond"},
{"op": "connect", "from": "sgn.value", "to": "gate.then"},
{"op": "connect", "from": "flat.value", "to": "gate.otherwise"},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "gate.value", "to": "bias.signal"},
{"op": "tap", "from": "sub.value", "as": "spread"},
{"op": "tap", "from": "isfirst.value", "as": "first_bar"},
{"op": "tap", "from": "gate.value", "as": "gated_signal"},
{"op": "tap", "from": "slow.value", "as": "slow_ma"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1 @@
{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[],"defaults":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["graph.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725235200000000000,1725580800000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","topology_hash":"c8e123f0a8948034592eb5b15f32e9a3d6b0705c4c942e7563f655faa0d1e9f1","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-2.0999999999994543,"max_drawdown":29.899999999999636,"bias_sign_flips":12,"r":{"expectancy_r":0.15668981035489754,"n_trades":10,"win_rate":0.3,"avg_win_r":2.6354203218756065,"avg_loss_r":-0.9056232660111208,"profit_factor":1.2471696505846321,"max_r_drawdown":5.2917804259042285,"n_open_at_end":0,"sqn":0.23053858556757656,"sqn_normalized":0.23053858556757656,"net_expectancy_r":0.15668981035489754,"conviction_terciles_r":[-0.2831414429722934,0.6217793811679744,0.13774607224048319]}}}
@@ -0,0 +1 @@
{"format_version":2,"blueprint":{"name":"graph","doc":"London-open breakout: SMA-fast>slow sign traded only on the first London session bar, blend period and MA lengths left open to sweep","nodes":[{"composite":{"name":"anchor","doc":"London cash-open session anchor: bars elapsed since the 08:00 Europe/London open","nodes":[{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"Europe/London"},{"name":"open","value":"08:00"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"bars"}]}},{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Sign","name":"sgn"}},{"primitive":{"type":"EqConst","name":"isfirst","bound":[{"pos":0,"name":"target","kind":"I64","value":{"I64":1}}]}},{"primitive":{"type":"Const","name":"flat","bound":[{"pos":0,"name":"value","kind":"F64","value":{"F64":0.0}}]}},{"primitive":{"type":"Select","name":"gate"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":1.0}}]}}],"edges":[{"from":1,"to":3,"slot":0,"from_field":0},{"from":2,"to":3,"slot":1,"from_field":0},{"from":3,"to":4,"slot":0,"from_field":0},{"from":0,"to":5,"slot":0,"from_field":0},{"from":5,"to":7,"slot":0,"from_field":0},{"from":4,"to":7,"slot":1,"from_field":0},{"from":6,"to":7,"slot":2,"from_field":0},{"from":7,"to":8,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0},{"node":2,"slot":0},{"node":6,"slot":0}],"source":"F64"}],"output":[{"node":8,"field":0,"name":"bias"}]}}
@@ -0,0 +1 @@
aura: note: use "anchor": london_anchor -> 2deeddd943c92252a401e33563f4ba8f1b85cd8a14cdf269fa1b795e4cc88eb3
@@ -0,0 +1,25 @@
[
{"op": "doc", "text": "London-open breakout: SMA-fast>slow sign traded only on the first London session bar, blend period and MA lengths left open to sweep"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "use", "ref": {"name": "london_anchor"}, "name": "anchor"},
{"op": "feed", "role": "price", "into": ["anchor.price"]},
{"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": "Sign", "name": "sgn"},
{"op": "connect", "from": "sub.value", "to": "sgn.value"},
{"op": "add", "type": "EqConst", "name": "isfirst", "bind": {"target": {"I64": 1}}},
{"op": "connect", "from": "anchor.bars", "to": "isfirst.value"},
{"op": "add", "type": "Const", "name": "flat", "bind": {"value": {"F64": 0.0}}},
{"op": "feed", "role": "price", "into": ["flat.clock"]},
{"op": "add", "type": "Select", "name": "gate"},
{"op": "connect", "from": "isfirst.value", "to": "gate.cond"},
{"op": "connect", "from": "sgn.value", "to": "gate.then"},
{"op": "connect", "from": "flat.value", "to": "gate.otherwise"},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "gate.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1 @@
{"format_version":2,"blueprint":{"name":"graph","doc":"London cash-open session anchor: bars elapsed since the 08:00 Europe/London open","nodes":[{"primitive":{"type":"Session","name":"sess","args":[{"name":"tz","value":"Europe/London"},{"name":"open","value":"08:00"}],"bound":[{"pos":0,"name":"period_minutes","kind":"I64","value":{"I64":15}}]}}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}]}],"output":[{"node":0,"field":0,"name":"bars"}]}}
@@ -0,0 +1,9 @@
[
{"op": "doc", "text": "London cash-open session anchor: bars elapsed since the 08:00 Europe/London open"},
{"op": "input", "role": "price"},
{"op": "add", "type": "Session", "name": "sess",
"args": {"tz": "Europe/London", "open": "08:00"},
"bind": {"period_minutes": {"I64": 15}}},
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
{"op": "expose", "from": "sess.bars_since_open", "as": "bars"}
]
@@ -0,0 +1,2 @@
{"family_id":"d5259c94-0-GER40-w0-r0-s0-0","report":{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[["graph.fast.length",{"I64":5}],["graph.slow.length",{"I64":20}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["graph.anchor.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725236099999000000,1725566340000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":6.5,"max_drawdown":73.5,"bias_sign_flips":11,"r":{"expectancy_r":-0.12406127289073887,"n_trades":8,"win_rate":0.25,"avg_win_r":1.9588513586755996,"avg_loss_r":-0.8183654834128516,"profit_factor":0.7978714475699166,"max_r_drawdown":4.3468217126941955,"n_open_at_end":0,"sqn":-0.22997299620402561,"sqn_normalized":-0.22997299620402561,"net_expectancy_r":-0.12406127289073887,"conviction_terciles_r":[1.2415119190137232,-0.23860471549936846,-0.9198999582184172]}}}}
{"family_id":"d5259c94-0-GER40-w0-r0-s0-0","report":{"manifest":{"commit":"eb2b0a132c9d07b2435981e6e9494a3527515304","params":[["graph.fast.length",{"I64":10}],["graph.slow.length",{"I64":20}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],"defaults":[["graph.anchor.sess.period_minutes",{"I64":15}],["graph.isfirst.target",{"I64":1}],["graph.flat.value",{"F64":0.0}],["graph.bias.scale",{"F64":1.0}]],"window":[1725236099999000000,1725566340000000000],"seed":0,"broker":"sim-optimal+risk-executor(pip_size=1)","instrument":"GER40","topology_hash":"38369f968b9c10e6a56bca6ac1c347955492e4f58b125510cb09f4609502ad46","project":{"commit":"1a838a8eb2ff0610ba90b5ec28052eb8abf14ae2-dirty"}},"metrics":{"total_pips":-22.5,"max_drawdown":87.5,"bias_sign_flips":12,"r":{"expectancy_r":-0.31488681817637804,"n_trades":11,"win_rate":0.2727272727272727,"avg_win_r":1.3490747111619477,"avg_loss_r":-0.9388723916782501,"profit_factor":0.5388410833781364,"max_r_drawdown":6.818086529508443,"n_open_at_end":0,"sqn":-0.7651422293114423,"sqn_normalized":-0.7651422293114423,"net_expectancy_r":-0.31488681817637804,"conviction_terciles_r":[0.27036658787590495,-0.6769671918050129,-0.3917464990869556]}}}}
@@ -0,0 +1,9 @@
[
{"op": "doc", "text": "attempt to author genuinely-new logic as data: a node type that is not in the closed vocabulary"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "my_lab::ThirdCandle", "name": "tc"},
{"op": "feed", "role": "price", "into": ["tc.series"]},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "tc.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,7 @@
[
{"op": "doc", "text": "attempt to enter an arg-bearing type half-configured: Session with no construction args"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "Session", "name": "sess", "bind": {"period_minutes": {"I64": 15}}},
{"op": "feed", "role": "price", "into": ["sess.trigger"]},
{"op": "expose", "from": "sess.bars_since_open", "as": "bars"}
]
@@ -0,0 +1,9 @@
[
{"op": "doc", "text": "attempt to pass construction args to an arg-less type: SMA with an args object"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "args": {"window": "5"}, "bind": {"length": {"I64": 5}}},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "feed", "role": "price", "into": ["fast.series"]},
{"op": "connect", "from": "fast.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,9 @@
[
{"op": "doc", "text": "attempt to splice a composite that was never registered: use by an unknown label"},
{"op": "source", "role": "price", "kind": "F64"},
{"op": "use", "ref": {"name": "tokyo_anchor"}, "name": "anchor"},
{"op": "feed", "role": "price", "into": ["anchor.price"]},
{"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 1.0}}},
{"op": "connect", "from": "anchor.bars", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]