From dc5f1742f477a643e45e46ab2cd38765a8961e34 Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 25 Jul 2026 23:21:07 +0200 Subject: [PATCH] =?UTF-8?q?fix(aura-cli,=20aura-runner):=20review=20findin?= =?UTF-8?q?gs=20=E2=80=94=20duplicate=20--override=20refusal,=20live=20dis?= =?UTF-8?q?covery=20prose,=20neutral=20JSON=20refusal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In-cycle fixes for the independent review of the #319 package, RED-first for both Importants. A repeated --override path now refuses at the shared lexer (exit 2, path named) instead of panicking exit 101 through reopen_all — which also un-misattributes the campaign leg's collision prose for duplicates. override_paths' refusal points at aura graph introspect --params (the retired --list-axes reference swept from every live comment, one explicitly-historical mention remains). exec classifies file targets through a Result-bearing peek: unparseable JSON refuses neutrally before any leg is chosen. RunPresentation (single-variant) removed; run_campaign_returning's contract note names its one deliberate usage-class exit; C14's routing sentence and C20's family-builder paragraph amended to current truth (superseded text to the history sidecars); bench error strings and deletion blank-line runs tidied. refs #319 --- crates/aura-bench/src/surfaces/campaign.rs | 2 +- crates/aura-bench/src/surfaces/fixed_cost.rs | 4 +- crates/aura-cli/src/campaign_run.rs | 73 +++---- crates/aura-cli/src/graph_construct.rs | 14 +- crates/aura-cli/src/main.rs | 93 ++++++--- crates/aura-cli/tests/cli_run.rs | 196 ++++++------------ crates/aura-cli/tests/exec.rs | 97 ++++++++- crates/aura-engine/src/blueprint.rs | 6 +- crates/aura-runner/src/axes.rs | 3 +- crates/aura-runner/src/member.rs | 10 +- .../c14-headless-two-faces.history.md | 15 ++ .../contracts/c14-headless-two-faces.md | 15 +- .../contracts/c20-strategy-harness.history.md | 13 ++ docs/design/contracts/c20-strategy-harness.md | 21 +- 14 files changed, 326 insertions(+), 236 deletions(-) diff --git a/crates/aura-bench/src/surfaces/campaign.rs b/crates/aura-bench/src/surfaces/campaign.rs index 2c4da11..e6cf477 100644 --- a/crates/aura-bench/src/surfaces/campaign.rs +++ b/crates/aura-bench/src/surfaces/campaign.rs @@ -251,7 +251,7 @@ fn campaign_rep(bin: &Path, sizing: Sizing, process_doc: &str) -> Result Result { for _ in 0..RUN_SAMPLES { let timed = run_timed(bin, &["exec", "bench_sma_a.json"], &scratch.0)?; if timed.exit != Some(0) { - return Err(format!("aura run exited {:?}: {}", timed.exit, timed.stdout)); + return Err(format!("aura exec exited {:?}: {}", timed.exit, timed.stdout)); } run_walls.push(timed.wall_s * 1000.0); if first_line.is_none() { @@ -70,7 +70,7 @@ pub fn rep(bin: &Path) -> Result { .stdout .lines() .next() - .ok_or("aura run must print its JSON record line")? + .ok_or("aura exec must print its JSON record line")? .to_string(), ); } diff --git a/crates/aura-cli/src/campaign_run.rs b/crates/aura-cli/src/campaign_run.rs index 9f772b8..9c8dec5 100644 --- a/crates/aura-cli/src/campaign_run.rs +++ b/crates/aura-cli/src/campaign_run.rs @@ -108,15 +108,6 @@ struct CampaignRunLine<'a> { campaign_run: &'a CampaignRunRecord, } -/// Stdout shape of a campaign run: emit-gated member/selection lines + the -/// always-on final `campaign_run` record line. `MemberLinesOnly` (the -/// dissolved-verb sugar path's suppressed-record-line mode) retired with the -/// quintet (#319 Task 8) — `Full` is the only surviving shape. -#[derive(Clone, Copy, PartialEq)] -pub(crate) enum RunPresentation { - Full, -} - /// `aura campaign run `: resolve, gate, execute, emit. Every `Err` /// is a refusal `campaign_cmd` renders as `aura: {msg}` + exit 1; an `Ok` /// carries the failed-cell count (#272) so the caller can exit 3 on a @@ -161,7 +152,7 @@ pub(crate) fn run_campaign( )); }; - run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments, overrides) + run_campaign_by_id(&campaign_id, env, parallel_instruments, overrides) } /// An executed campaign plus the context its presenter and the dissolved-verb @@ -180,18 +171,15 @@ pub(crate) struct CampaignRun { /// The one campaign executor path from a resolved content id onward: fetch /// the stored canonical bytes by id (so file addressing and id addressing /// produce the same realization by construction) and re-run the intrinsic -/// tier on them, execute, and emit per `presentation`. Called by -/// `run_campaign` (`RunPresentation::Full`, the sole surviving shape since -/// the quintet's dissolved-verb sugar path retired, #319 Task 8). +/// tier on them, execute, and emit. Called by `run_campaign`. pub(crate) fn run_campaign_by_id( campaign_id: &str, env: &Env, - presentation: RunPresentation, parallel_instruments: NonZeroUsize, overrides: &[(String, Scalar)], ) -> Result { let run = run_campaign_returning(campaign_id, env, parallel_instruments, overrides)?; - present_campaign(run, presentation, env) + present_campaign(run, env) } /// The one campaign executor path from a resolved content id up to (not @@ -206,6 +194,15 @@ pub(crate) fn run_campaign_by_id( /// and BEFORE the referential gate — the stored bytes and the recorded /// `campaign_id` stay those of the ORIGINAL document; only the executed /// run-set changes, its audit record being the raw member manifests. +/// +/// One deliberate exception to "every refusal is an `Err`" (review Minor-4): +/// the injection loop's axis-collision check exits directly with code 2 +/// instead of returning `Err`. That collision is a USAGE-class fault (C14) — +/// `--override` naming a path the document already declares as an axis is a +/// malformed invocation, exactly like `--override`'s own malformed-token +/// refusal (`parse_override_tokens`, exit 2) — not a RUNTIME-class refusal +/// (missing environment/recorded state, exit 1), which is what this fn's +/// `Result<_, String>` return otherwise carries end to end. pub(crate) fn run_campaign_returning( campaign_id: &str, env: &Env, @@ -316,15 +313,11 @@ of the campaign; an override overrides a bound value, never an axis" Ok(CampaignRun { outcome, campaign, strategies, server: runner.server() }) } -/// Emit an executed campaign's results per `presentation`: the zero-survivor -/// stderr notes, the emit-gated family/selection lines, the always-on record -/// line (Full only), then trace persistence. Split out of `run_campaign_by_id` -/// so the dissolved-verb sugar can consume the outcome without this stdout tail. -fn present_campaign( - run: CampaignRun, - presentation: RunPresentation, - env: &Env, -) -> Result { +/// Emit an executed campaign's results: the zero-survivor stderr notes, the +/// emit-gated family/selection lines, the always-on record line, then trace +/// persistence. Split out of `run_campaign_by_id` so a forthcoming caller +/// could consume the outcome without this stdout tail. +fn present_campaign(run: CampaignRun, env: &Env) -> Result { let CampaignRun { outcome, campaign, strategies, server } = run; // Zero-survivor stderr notes (exit stays 0 — a null result is a valid @@ -413,23 +406,21 @@ fn present_campaign( } } } - if presentation == RunPresentation::Full { - println!( - "{}", - serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record }) - .expect("campaign run record serializes") - ); - eprintln!( - "aura: campaign run {} recorded: {} cells{}", - outcome.record.run, - outcome.record.cells.len(), - if failed == 0 { - String::new() - } else { - format!(", {failed} failed ({})", fail_labels.join(", ")) - } - ); - } + println!( + "{}", + serde_json::to_string(&CampaignRunLine { campaign_run: &outcome.record }) + .expect("campaign run record serializes") + ); + eprintln!( + "aura: campaign run {} recorded: {} cells{}", + outcome.record.run, + outcome.record.cells.len(), + if failed == 0 { + String::new() + } else { + format!(", {failed} failed ({})", fail_labels.join(", ")) + } + ); // Trace persistence runs LAST, after the always-on record line (0109): // stdout stays data-pure (the record line is the wire contract) and the diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index f85981c..e5668dd 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -642,10 +642,11 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project } else if let Some(target) = cmd.params.as_deref() { // --params (#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 `..` form was retired — - // there is exactly one axis namespace now, so the two discovery - // surfaces agree line-for-line, see `params_lines`'s own doc comment). + // (`validate_campaign_refs`) and `exec --override`'s own resolution + // (`override_paths`, member.rs) resolves against (#328: the wrapped + // `..` form was retired — there is exactly + // one axis namespace now, so the two surfaces agree line-for-line, + // see `params_lines`'s own doc comment). match params_lines(target, env) { Ok(s) => print!("{s}"), Err(m) => { @@ -938,8 +939,9 @@ fn resolve_blueprint_text(target: &str, env: &aura_runner::project::Env) -> Resu /// the same form `introspect --node` already uses for param kinds. Followed /// by one `{name}:{kind:?} default={value}` line per BOUND param (#328): /// `bound_param_space()`'s names are already RAW (#203), so no blueprint-name -/// concatenation is needed — line-identical to the reconciled `--list-axes` -/// bound pass (`list_blueprint_axes`, main.rs), same `render_value` lexicon. +/// concatenation is needed — line-identical to the retired `aura sweep +/// --list-axes`'s own bound pass (`list_blueprint_axes`, main.rs, gone with +/// the #319 sugar retirement), same `render_value` lexicon. /// #331 delta re-review: the FILE target is a FOURTH freshly-authored-file /// intake this fn's own `composite_from_any` call had missed gating — a FILE /// routes through [`composite_from_authored_text`] instead; a content id diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 89a5028..26bf335 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -72,8 +72,8 @@ use aura_runner::reproduce::reproduce_family_in; use aura_runner::measure::run_measurement; // `render_value` is the single source (aura-runner, #295 Task 7): the // campaign trace layout's member-key label and this shell's own -// presentation (reproduce output, `--list-axes` default printing) render a -// scalar identically. +// presentation (reproduce output, `graph introspect --params`'s bound-pass +// default printing) render a scalar identically. use aura_runner::runner::render_value; // Only reached from the test module's stop-regime default tests now (#319 // Task 8 — the quintet's own production callers are retired); test-only, @@ -612,31 +612,30 @@ impl IcReport { } } -/// `aura sweep --list-axes`: one `:` line per open -/// sweepable knob, in `param_space()` order (byte-identical to before #246), -/// followed by one `: default=` line per BOUND param — -/// every bound param IS a sweepable default, re-openable by naming it as an -/// `--axis` (#246), on EVERY blueprint shape (fully open, partially open, or -/// 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 RAW `.` paths (#328: the wrapped `..` -/// form is retired from the surface) — exactly what `--axis` binds. -/// /// Lex `exec --override NODE.PARAM=VALUE` tokens (#319 Task 3/4): the same -/// int-then-float rule `parse_scalar_csv` uses for `--axis` values, shared by +/// int-then-float rule the campaign leg's `--axis` values use, shared by /// both `exec` legs (the blueprint leg's reopen recipe and the campaign /// leg's single-value-axis injection) so the wording and the parse rule /// cannot drift between them. A malformed token refuses at the usage /// boundary (exit 2) — intake, not a per-leg concern. +/// +/// A repeated path (the same NODE.PARAM named twice) likewise refuses here +/// (exit 2), mirroring `--tap`'s own duplicate refusal +/// (`tap_plan_from_args`): downstream, the blueprint leg's `reopen_all` +/// folds the override set onto the signal one path at a time, so a second +/// occurrence of an already-reopened path is no longer bound and panics +/// (`NoSuchBoundParam`) — refusing the duplicate at the lexer, before either +/// leg ever resolves it, is the one gate that protects both. fn parse_override_tokens(tokens: &[String]) -> Vec<(String, Scalar)> { let mut ov = Vec::new(); + let mut seen = BTreeSet::new(); for tok in tokens { let Some((path_s, val_s)) = tok.split_once('=') else { eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`"); std::process::exit(2); }; - if path_s.trim().is_empty() { + let path = path_s.trim(); + if path.is_empty() { eprintln!("aura: exec: --override expects NODE.PARAM=VALUE, got `{tok}`"); std::process::exit(2); } @@ -650,7 +649,11 @@ fn parse_override_tokens(tokens: &[String]) -> Vec<(String, Scalar)> { } }, }; - ov.push((path_s.trim().to_string(), val)); + if !seen.insert(path.to_string()) { + eprintln!("aura: exec: --override names path \"{path}\" twice"); + std::process::exit(2); + } + ov.push((path.to_string(), val)); } ov } @@ -659,15 +662,17 @@ fn parse_override_tokens(tokens: &[String]) -> Vec<(String, Scalar)> { // The declarative argument grammar. clap owns argv tokenizing, scoped `--help`, // `--version`, `--flag=value`, `--`, and long-option abbreviation; the `dispatch_*` // handlers below convert each `*Cmd` into the argument shapes the existing execution -// fns accept, reusing the value helpers (`Selection`, -// `DataSource::from_choice`, `parse_scalar_csv`, `parse_select`, -// `parse_param_cells`). The four subcommands with an optional `[blueprint]` positional -// dispatch on `is_blueprint_file`: a first-positional naming an existing `.json` file -// selects the loaded-blueprint branch. There is no second built-in grammar anymore -// (#159 demo retirement / #220 axis generalization): without a blueprint the -// dispatcher prints a usage error and exits 2; a blueprint with `--real` routes to -// the campaign path. Usage errors exit 2; runtime refusals exit 1; a run that -// completes with ≥1 failed cell exits 3 (#272); a clean run exits 0. +// fns accept. `exec` (#319) is the one document-executing verb: it dispatches on +// `is_blueprint_file` layered with `classify_exec_document_file` (the "kind"-field +// peek) — a first-positional naming an existing `.json` file that parses as JSON +// and carries no top-level "kind" selects the loaded-blueprint leg; a file +// carrying "kind", or a target that is not a readable `.json` file at all (a +// registered content id), selects the campaign leg; a file that does not parse as +// JSON at all refuses neutrally before either leg's own validation runs. +// `--override`/`--tap` tokens are lexed by `parse_override_tokens`/ +// `tap_plan_from_args` above, shared across both legs where applicable. Usage +// errors exit 2; runtime refusals exit 1; a campaign that completes with ≥1 +// failed cell exits 3 (#272); a clean run exits 0. /// The single build-time commit provenance (`option_env!("AURA_COMMIT")`, /// falling back to `"unknown"`): both `--version` (via `version_string`) and @@ -955,7 +960,7 @@ struct ExecCmd { /// The file-target discriminator: a first-positional that names an existing /// `.json` file is a document on disk. Shared by `exec`'s target routing -/// (layered with `is_campaign_document_file`) and `dispatch_graph`. +/// (layered with `classify_exec_document_file`) and `dispatch_graph`. fn is_blueprint_file(arg: &Option) -> Option<&str> { arg.as_deref() .filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file()) @@ -969,11 +974,19 @@ fn is_blueprint_file(arg: &Option) -> Option<&str> { /// token — it cannot tell a campaign document ending in `.json` from a /// blueprint file, both being ordinary readable `.json` files — so `exec`'s /// routing peeks this one cheap key before choosing a leg. -fn is_campaign_document_file(path: &str) -> bool { - std::fs::read_to_string(path) - .ok() - .and_then(|t| serde_json::from_str::(&t).ok()) - .is_some_and(|v| v.get("kind").is_some()) +/// +/// `Ok(true)`/`Ok(false)` classifies a file that parses as JSON at all +/// (campaign vs. not); `Err` means the file does NOT parse as JSON at all — +/// review Minor-3: a file this malformed has no leg to misattribute the +/// fault to (neither leg's own "document is not valid JSON" prose is the +/// right cause when the shape was never determined), so the caller refuses +/// neutrally instead of silently falling through to the blueprint leg. +fn classify_exec_document_file(path: &str) -> Result { + let text = + std::fs::read_to_string(path).map_err(|e| format!("target file is not readable: {e}"))?; + let v: serde_json::Value = serde_json::from_str(&text) + .map_err(|e| format!("target file is not valid JSON: {e}"))?; + Ok(v.get("kind").is_some()) } /// Terminate per a campaign-path result (#272): a String error is a refusal @@ -1017,10 +1030,24 @@ fn tap_plan_from_args(args: &[String]) -> Result { } /// The one executor verb (#319): a campaign document (file or content id) -/// or a signal blueprint (single run). Flag discipline is per-leg. +/// or a signal blueprint (single run). Flag discipline is per-leg. A +/// `.json` file target is read and classified ONCE (review Minor-3): a file +/// that does not parse as JSON at all refuses neutrally here, before either +/// leg's own document-shape validation ever runs — so a malformed campaign +/// document is never misattributed to the blueprint leg's "blueprint +/// document is not valid JSON" prose. fn dispatch_exec(a: ExecCmd, env: &aura_runner::project::Env) { let target = Some(a.target.clone()); - let blueprint_path = is_blueprint_file(&target).filter(|p| !is_campaign_document_file(p)); + let file_path = is_blueprint_file(&target); + let is_campaign_file = match file_path.map(classify_exec_document_file) { + Some(Ok(is_campaign)) => is_campaign, + Some(Err(msg)) => { + eprintln!("aura: exec: {}: {msg}", file_path.expect("Some by the match arm above")); + std::process::exit(2); + } + None => false, + }; + let blueprint_path = file_path.filter(|_| !is_campaign_file); match blueprint_path { Some(path) => exec_blueprint_leg(path, &a.r#override, &a.tap, env), None => { diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 019dfb4..4c5bc05 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -203,7 +203,6 @@ fn sweep_campaign_doc( ) } - /// Property (ported onto a one-cell campaign document, #319 Task 8 — the /// retired `run --real` verb's own hard exit-1/no-stdout contract does not /// survive the campaign path's #272 contained-fault model, but the pip-honesty @@ -391,9 +390,6 @@ fn run_real_nonvetted_symbol_resolves_pip_from_sidecar() { ); } - - - // Note (#159 cut 4 collateral): `--trace` on a single `aura run`/`aura sweep` // is no longer reachable from any CLI surface — the built-in default that // accepted it retired with the last two PIP strategies, and the blueprint @@ -736,8 +732,6 @@ fn no_args_prints_usage_and_exits_two() { assert!(stderr.contains("Usage"), "stderr was: {stderr:?}"); } - - /// Property: `aura graph` emits a single self-contained HTML page — the /// interactive pin-graph viewer with the deterministic model inlined and the /// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through @@ -878,7 +872,6 @@ fn graph_with_bad_blueprint_arg_is_a_usage_fault_not_a_silent_sample() { ); } - #[test] fn runs_families_list_and_per_family_rank_across_invocations() { // Ported onto a one-cell campaign document run twice (#319 Task 8): two @@ -1328,7 +1321,6 @@ fn run_real_ohlc_channel_example_end_to_end_hostless() { ); } - /// The dissolved real-data sweep + reproduce over the OHLC example (#231 /// acceptance), ported onto a one-cell campaign document (#319 Task 8 — /// `--list-axes` itself is dead flag surface, retired with `sweep`; its @@ -1755,24 +1747,27 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() { /// overriding only the `fast.length` axis and leaving `slow.length` bound — /// unlike the walk-forward reproduce sibling /// (`aura_walkforward_over_a_blueprint_reproduces_bit_identically`), which -/// overrides both axes over the same closed blueprint. The walk-forward -/// synthetic route's own `--axis` intake is now RAW (this cycle's tidy fix, -/// #328): the CLI preflight refuses a wrapped name before this run ever starts +/// overrides both axes over the same closed blueprint. Originally modelled on +/// that sibling (the retired synthetic in-process walk-forward dispatch verb — +/// no `--real`), NOT on `reproduce_real_sweep_family_re_derives_bit_identically`: +/// a real-data mint routed through the campaign executor +/// (`CliMemberRunner::run_member` + `validate_and_register_axes`), neither of +/// which carried any #246 override threading — a gap outside that task's file +/// scope (`blueprint_sweep_over`, `reproduce_family_in`) and spanning all four +/// (then-live) dispatch verbs, so exercising it there would have silently +/// expanded into unrelated, unreviewed surface. Back when the synthetic +/// walk-forward dispatch verb existed, its `--axis` intake was RAW (#328): the +/// CLI preflight refused a wrapped name before the run ever started /// (`refuse_wrapped_synthetic_axes`), and `blueprint_sweep_over`'s own -/// `SweepBinder` call translates the RAW name onto its wrapped `param_space()` -/// slot (`wrapped_name_of`) before keying by it. -/// Modelled on that sibling (synthetic in-process walk-forward — no `--real`), -/// NOT on `reproduce_real_sweep_family_re_derives_bit_identically`: a real-data -/// mint routes through the campaign executor (`CliMemberRunner::run_member` + -/// `validate_and_register_axes`), neither of which carries any #246 override -/// threading — a gap outside this task's file scope (`blueprint_sweep_over`, -/// `reproduce_family_in`) and spanning all four dispatch verbs, so exercising -/// it here would silently expand into unrelated, unreviewed surface. The -/// synthetic walk-forward path exercises exactly the threaded functions: -/// `blueprint_sweep_over` (via `blueprint_walkforward_family`) re-opens -/// `fast.length` for the per-window IS re-fit, and -/// `reproduce_family_in` re-opens it again from the recorded manifest params -/// so the re-derivation resolves against the same space the mint used. +/// `SweepBinder` call translated the RAW name onto its wrapped `param_space()` +/// slot (`wrapped_name_of`) before keying by it. That path exercised exactly +/// the threaded functions: `blueprint_sweep_over` (via +/// `blueprint_walkforward_family`) re-opened `fast.length` for the per-window +/// IS re-fit, and `reproduce_family_in` re-opened it again from the recorded +/// manifest params so the re-derivation resolved against the same space the +/// mint used. #319 Task 8 ported this test onto the campaign +/// `std::walk_forward` stage (see the in-body port note below); the property +/// pinned here is unchanged. #[test] fn reproduce_family_with_overridden_bound_param_re_derives() { // Ported onto a `[std::grid, std::walk_forward]` campaign document over @@ -1939,7 +1934,6 @@ fn reproduce_real_walkforward_family_does_not_panic() { // fixture at a single axis value instead.) const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","doc":"high/low SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#; - // Note (#319 Task 8, disposition deviation): `walkforward_synthetic_refuses_a_ // multi_column_blueprint_before_data_access` (the synthetic, no-`--real` // exit-process arm) retired rather than ported — it is a byte-identical twin @@ -2042,12 +2036,6 @@ fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() { } } - - - - - - /// Property (issue #246, task 5): the dissolved real-data `aura sweep` /// campaign path accepts an `--axis` naming a BOUND param of a CLOSED /// blueprint, not only an already-open one. Tasks 3-4 threaded this override @@ -2168,11 +2156,6 @@ fn sweep_over_a_bound_override_matches_the_historical_open_bind() { ); } - - - - - // Note (#159 cut 4 collateral): this removes the `aura chart ` // CLI-integration tests (single-run and family overlay/tap-filter/meta, the // cross-kind write-guard collision + same-kind-overwrite pair) — every one of @@ -2188,12 +2171,6 @@ fn sweep_over_a_bound_override_matches_the_historical_open_bind() { // --- `aura run --harness ` selector (iter-3 Task 3) -------------------- - - - - - - /// Property: `aura generalize` grades a single r-sma candidate across two /// instruments — its stdout carries the per-instrument breakdown + the worst-case /// floor + the sign-agreement, then the persisted CrossInstrument family handle. @@ -2307,17 +2284,20 @@ fn generalize_real_e2e_pins_the_exact_current_grade() { assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {record_line}"); } -/// Property (#220 generalize vertical): `aura generalize` grades ANY sweepable -/// blueprint, not just the historical r-sma candidate — the whole point of -/// dropping `--strategy r-sma`/`--fast`/`--slow` for a positional blueprint + -/// generic `--axis`. Every other generalize E2E test here still exercises -/// `r_sma.json`, so a regression that silently special-cased r-sma paths -/// back in (e.g. resolving axes against a hardcoded `sma_signal.*` namespace) -/// would ship green there while breaking every other blueprint. This runs the -/// unrelated r-breakout candidate (the ganged `channel_length` axis, no -/// `sma_signal` node in sight) end-to-end and asserts a well-formed two- -/// instrument grade. Gated on the shared GER40/USDJPY Sept-2024 archive; skips -/// cleanly on a data refusal. +/// Property (#220 generalize vertical, ported onto the campaign +/// `std::generalize` stage by #319 Task 8): grades ANY sweepable blueprint, +/// not just the historical r-sma candidate — originally the whole point of +/// the retired `aura generalize` verb dropping `--strategy r-sma`/`--fast`/ +/// `--slow` for a positional blueprint + generic `--axis`; the property +/// survives the #319 dissolution onto a `std::sweep, std::generalize` +/// process document unchanged. Every other generalize E2E test here still +/// exercises `r_sma.json`, so a regression that silently special-cased +/// r-sma paths back in (e.g. resolving axes against a hardcoded +/// `sma_signal.*` namespace) would ship green there while breaking every +/// other blueprint. This runs the unrelated r-breakout candidate (the +/// ganged `channel_length` axis, no `sma_signal` node in sight) end-to-end +/// and asserts a well-formed two-instrument grade. Gated on the shared +/// GER40/USDJPY Sept-2024 archive; skips cleanly on a data refusal. #[test] fn generalize_grades_a_non_r_sma_blueprint() { // Ported onto a `[std::sweep, std::generalize]` campaign document @@ -2367,18 +2347,20 @@ fn generalize_grades_a_non_r_sma_blueprint() { } /// Characterization pin (byte-identity anchor for the walkforward dissolution, -/// #210). The current `aura walkforward` runs its per-window IS-refit inline -/// (`walkforward_family` -> `select_winner` -> OOS run -> stitch/pool); the -/// dissolution reroutes the same rolling IS-sweep -> OOS-run -> deflation through -/// the campaign `std::walk_forward` stage, and must retain the OOS pip curve the -/// campaign runner currently discards (`oos_equity: vec![]`) to reproduce +/// #210, ported onto the campaign path by #319 Task 8). The original `aura +/// walkforward` verb ran its per-window IS-refit inline (`walkforward_family` +/// -> `select_winner` -> OOS run -> stitch/pool); the dissolution rerouted the +/// same rolling IS-sweep -> OOS-run -> deflation through the campaign +/// `std::walk_forward` stage, retaining the OOS pip curve the campaign runner +/// used to discard (`oos_equity: vec![]`) so it could still reproduce /// `stitched_total_pips`, plus reconstruct pooled `oos_r`. The multi-point grid /// (fast 3,5 x slow 12,20) makes the per-window winner selection non-degenerate -- /// `param_stability` varies across windows -- so a winner-pick divergence between -/// the inline and campaign refit cannot ship green. This pins the EXACT current -/// grade of the identical invocation over a fixed 2025 GER40 window; after the -/// dissolution the same command must reproduce these bytes (the acceptance gate). -/// Gated on the shared GER40 archive; skips cleanly on a data refusal. +/// the inline and campaign refit could not have shipped green. This pins the +/// EXACT grade of the identical invocation over a fixed 2025 GER40 window that +/// the dissolved `std::walk_forward` stage reproduces bit-for-bit (the +/// acceptance gate the port had to clear). Gated on the shared GER40 archive; +/// skips cleanly on a data refusal. #[test] fn walkforward_real_e2e_pins_the_exact_current_grade() { // Ported onto a `[std::grid, std::walk_forward]` campaign document @@ -2453,21 +2435,24 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() { assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {wf}"); } -/// Property (#220 walkforward vertical): `aura walkforward --real` IS-refits ANY -/// sweepable blueprint through the campaign path, not just the historical r-sma -/// candidate — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow` -/// for a positional blueprint + generic `--axis`. Every other walkforward E2E -/// test here still exercises `r_sma.json`; a regression that silently -/// special-cased the refit's axis lookup back to a hardcoded `sma_signal.*` -/// namespace (e.g. in `walkforward_summary_json_from_reports`'s per-axis -/// `raw_matches_wrapped` search) would ship green there while breaking every -/// other blueprint. This runs the unrelated r-breakout candidate -/// (the ganged `channel_length` axis, no `sma_signal` node in sight) end-to-end -/// and asserts a well-formed multi-window summary whose `param_stability` has -/// exactly one entry per bound axis (1 ganged blueprint axis + the 2 stop columns). -/// The window count (9) is roller/data-derived, not strategy-derived, so it is -/// pinned exactly against the same GER40 2025 span the r-sma anchor uses. -/// Gated on the shared GER40 archive; skips cleanly on a data refusal. +/// Property (#220 walkforward vertical, ported onto the campaign +/// `std::walk_forward` stage by #319 Task 8): IS-refits ANY sweepable +/// blueprint, not just the historical r-sma candidate — originally the +/// whole point of the retired `aura walkforward --real` verb dropping +/// `--strategy r-sma`/`--fast`/`--slow` for a positional blueprint + generic +/// `--axis`; the property survives the #319 dissolution unchanged. Every +/// other walkforward E2E test here still exercises `r_sma.json`; a +/// regression that silently special-cased the refit's axis lookup back to a +/// hardcoded `sma_signal.*` namespace (e.g. in +/// `walkforward_summary_json_from_reports`'s per-axis `raw_matches_wrapped` +/// search) would ship green there while breaking every other blueprint. +/// This runs the unrelated r-breakout candidate (the ganged `channel_length` +/// axis, no `sma_signal` node in sight) end-to-end and asserts a well-formed +/// multi-window summary whose `param_stability` has exactly one entry per +/// bound axis (1 ganged blueprint axis + the 2 stop columns). The window +/// count (9) is roller/data-derived, not strategy-derived, so it is pinned +/// exactly against the same GER40 2025 span the r-sma anchor uses. Gated on +/// the shared GER40 archive; skips cleanly on a data refusal. #[test] fn walkforward_dissolves_a_non_r_sma_blueprint() { // Ported onto a `[std::grid, std::walk_forward]` campaign document @@ -2515,8 +2500,6 @@ fn walkforward_dissolves_a_non_r_sma_blueprint() { assert_eq!(ps.len(), 3, "one entry per bound axis (channel_length, stop_length, stop_k): {wf}"); } - - /// Characterization pin (#215): the same identical invocation as /// `walkforward_real_e2e_pins_the_exact_current_grade` (fixture, real /// instrument, axes, stop, window), but selecting `plateau:worst` instead of @@ -2597,17 +2580,6 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() { assert_eq!(ps[1]["mean"].as_f64(), Some(16.444444444444443), "slow-MA refit mean: {wf}"); } - - - - - - - - - - - // Note (#319 Task 8, disposition finding): `generalize_without_explicit_ // window_resolves_a_shared_window_and_completes` and `generalize_without_ // explicit_window_resolves_the_intersection_of_all_symbols` retired rather @@ -2626,12 +2598,6 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() { // window" property stays fully covered by `generalize_grades_a_candidate_ // across_two_instruments` and its siblings above. - - - - - - /// Property, ported onto a `[std::sweep, std::generalize]` campaign document /// (#319 Task 8 — disposition finding): a successful campaign generalize run /// persists its per-instrument runs as discoverable, but NOT as a merged @@ -2843,7 +2809,6 @@ fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() { assert!(fo.contains("\"members\":4"), "2×2 grid -> four members: {fo}"); } - /// Property (#158/#164, C18/C11/C12): a blueprint sweep content-addresses its topology /// — the canonical blueprint is persisted ONCE under `runs/blueprints/.json`, /// keyed by the very SHA256 every member's manifest carries, holding exactly the bytes @@ -3133,8 +3098,6 @@ fn aura_walkforward_over_a_blueprint_persists_a_family() { assert_eq!(n, 1, "exactly one content-addressed blueprint stored"); } - - /// E2E (#278 decision: same fault class, same verb, same surface => same /// treatment): a member fault on the SYNTHETIC walk-forward path is CONTAINED /// exactly like the real/campaign path contains it (#272) — it surfaces as an @@ -3218,14 +3181,6 @@ fn aura_sweep_synthetic_member_panic_is_contained() { ); } - - - - - - - - /// E2E (negative): `aura reproduce ` is a hard error (exit non-zero + named /// cause on stderr), distinct from `runs family `'s treat-as-empty exit 0. #[test] @@ -3275,13 +3230,6 @@ fn aura_reproduce_rejects_a_missing_stored_blueprint() { assert!(out.contains("missing from store"), "names the cause: {out}"); } - - - - - - - /// `--version` / `-V` surface the crate version on stdout and exit 0 (GNU MUST): /// a version query is a successful query, not a usage error. The expected string /// is the crate version itself (`CARGO_PKG_VERSION`, i.e. the workspace version), @@ -3335,13 +3283,6 @@ fn version_flag_carries_the_engine_commit_from_manifest_provenance() { ); } - - - - - - - /// Characterization pin (byte-identity anchor for the mc R-bootstrap dissolution, /// #210, the last verb; argv since migrated to the generic form by #220). /// `aura mc --real --axis …` pools the per-window OOS trade-R @@ -3715,8 +3656,6 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() { ); } - - /// Property: the one load-bearing decision new to the mc dissolution — `translate_mc` /// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which /// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade, @@ -3962,15 +3901,6 @@ fn generalize_campaign_runs_an_arbitrary_blueprint() { assert!(grade["worst_case"].is_number(), "worst-case floor present: {record_line}"); } - - - - - - - - - /// Property (#273, list reshape): `aura data list` enumerates the archive's /// symbols as NDJSON, sorted — the discovery step a campaign author (or an /// agent operating through the CLI alone, the deployment posture) needs diff --git a/crates/aura-cli/tests/exec.rs b/crates/aura-cli/tests/exec.rs index f60026e..3620add 100644 --- a/crates/aura-cli/tests/exec.rs +++ b/crates/aura-cli/tests/exec.rs @@ -234,6 +234,30 @@ fn exec_campaign_target_outside_project_refuses() { assert!(!dir.join("runs").exists(), "a refused run must not create a store outside a project"); } +/// A malformed FILE target (a trailing comma — not valid JSON at all) must +/// refuse NEUTRALLY at the routing seam, before either leg's own +/// document-shape validation ever runs — not silently fall through to the +/// blueprint leg's "blueprint document is not valid JSON" prose (review +/// Minor-3). The file's shape (blueprint vs. campaign) was never +/// determined, so neither leg's own wording is the right attribution. +#[test] +fn exec_malformed_json_file_refuses_neutrally_not_misrouted() { + let (dir, _fixture) = fresh_project(); + let malformed = r#"{ + "format_version": 1, + "kind": "campaign", + "name": "broken", +}"#; + write_doc(&dir, "broken.campaign.json", malformed); + let (out, code) = run_code_in(&dir, &["exec", "broken.campaign.json"]); + assert_eq!(code, Some(2), "stdout/stderr: {out}"); + assert!(out.contains("target file is not valid JSON"), "stdout/stderr: {out}"); + assert!( + !out.contains("blueprint document is not valid JSON"), + "must not misattribute to the blueprint leg's own prose: {out}" + ); +} + // --------------------------------------------------------------------------- // Task 2: the exec blueprint leg (synthetic single run). // --------------------------------------------------------------------------- @@ -448,7 +472,9 @@ fn exec_override_malformed_token_refuses_with_usage() { } /// An override path naming no param of the blueprint (open or bound) refuses -/// with `override_paths`' own prose (`member.rs:768-771`). +/// with `override_paths`' own prose (`member.rs:768-771`), pointing at the +/// live axis-discovery verb (`aura graph introspect --params`) — the retired +/// `aura sweep --list-axes` no longer exists. #[test] fn exec_override_unknown_path_refuses_with_the_override_prose() { let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "nope.knob=1"]); @@ -457,6 +483,34 @@ fn exec_override_unknown_path_refuses_with_the_override_prose() { out.contains("names no param of this blueprint (open or bound)"), "stdout/stderr: {out}" ); + assert!( + out.contains("aura graph introspect --params"), + "must point at the live discovery verb, not the retired --list-axes: {out}" + ); +} + +/// A repeated `--override` path (the same NODE.PARAM named twice) refuses as +/// a usage error naming the duplicated path — it must NOT fall through to +/// `reopen_all`'s `NoSuchBoundParam` panic (the second reopen of an +/// already-reopened bound param, exit 101): the first occurrence reopens +/// `fast.length`, so a second `override_paths` resolution against the SAME +/// un-reopened bound-name set still succeeds, and only `reopen_all`'s fold +/// discovers the collision, too late to refuse cleanly. Mirrors `--tap`'s own +/// duplicate refusal (`tap_plan_from_args`). +#[test] +fn exec_override_duplicate_path_refuses_with_usage_not_a_panic() { + let (out, code) = run_code(&[ + "exec", + "examples/r_sma.json", + "--override", + "fast.length=5", + "--override", + "fast.length=9", + ]); + assert_ne!(code, Some(101), "must not panic: {out}"); + assert_eq!(code, Some(2), "stdout/stderr: {out}"); + assert!(out.contains("fast.length"), "the duplicated path must be named: {out}"); + assert!(out.contains("--override"), "stdout/stderr: {out}"); } // --------------------------------------------------------------------------- @@ -535,6 +589,47 @@ fn exec_campaign_override_colliding_with_a_document_axis_refuses() { assert!(out.contains("declared axis"), "stdout/stderr: {out}"); } +/// A repeated `--override` path on the campaign leg refuses at the SAME +/// shared lexer (`parse_override_tokens`) both legs use — before the +/// per-strategy injection loop (`run_campaign_returning`) ever runs. Prior +/// to the fix, the first occurrence's own injection made the SECOND +/// occurrence look like a document-declared axis, misattributing the +/// refusal to "collides with a declared axis" (review Minor-1) even though +/// `bias.scale` is not declared by the document at all. +#[test] +fn exec_campaign_override_duplicate_path_refuses_at_the_shared_lexer() { + let (dir, _fixture) = fresh_project_with_data(); + let runs_dir = dir.join("runs"); + std::fs::remove_dir_all(&runs_dir).ok(); + let _cleanup = ScratchGuard(vec![ + ScratchPath::Dir(runs_dir.clone()), + ScratchPath::File(dir.join("ovdup.process.json")), + ScratchPath::File(dir.join("ovdup.campaign.json")), + ]); + let bp_id = seed_blueprint(&dir, "exec-override-dup-seed"); + let proc_id = register_process_doc(&dir, "ovdup.process.json", SWEEP_ONLY_PROCESS_DOC); + let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999)); + write_doc(&dir, "ovdup.campaign.json", &doc); + + let (out, code) = run_code_in( + &dir, + &[ + "exec", + "ovdup.campaign.json", + "--override", + "bias.scale=0.75", + "--override", + "bias.scale=0.9", + ], + ); + assert_eq!(code, Some(2), "stdout/stderr: {out}"); + assert!(out.contains("bias.scale"), "the duplicated path must be named: {out}"); + assert!( + !out.contains("collides with a declared axis"), + "must not misattribute to the axis-collision cause: {out}" + ); +} + /// An override path naming no param of any strategy (a typo'd raw name) /// falls out of `validate_campaign_refs`'s existing `AxisNotInParamSpace` /// did-you-mean prose — pinned literally in `research_docs.rs` (:797-822); diff --git a/crates/aura-engine/src/blueprint.rs b/crates/aura-engine/src/blueprint.rs index a034729..c0e9b9a 100644 --- a/crates/aura-engine/src/blueprint.rs +++ b/crates/aura-engine/src/blueprint.rs @@ -574,7 +574,8 @@ pub enum ReopenError { /// One entry of the aggregated BOUND param surface (#246): the path-qualified /// twin of [`ParamSpec`] carrying the bound value — the default a sweep axis -/// may override, and what `--list-axes` renders as `default=`. +/// may override, and what `aura graph introspect --params` renders as +/// `default=`. #[derive(Debug, Clone, PartialEq)] pub struct BoundSpec { pub name: String, @@ -3376,7 +3377,8 @@ mod tests { } /// #246: bound_param_space mirrors param_space's path qualification and - /// carries the bound value (the default the CLI renders in --list-axes). + /// carries the bound value (the default the CLI renders in + /// `aura graph introspect --params`). #[test] fn bound_param_space_is_path_qualified_with_values() { use aura_strategy::Bias; diff --git a/crates/aura-runner/src/axes.rs b/crates/aura-runner/src/axes.rs index 10c45f4..a883cbf 100644 --- a/crates/aura-runner/src/axes.rs +++ b/crates/aura-runner/src/axes.rs @@ -64,7 +64,8 @@ pub fn raw_matches_wrapped(raw: &str, wrapped: &str) -> bool { /// instead of refusing it. #[derive(Debug, PartialEq, Eq)] pub enum AxisIntake { - /// A legal RAW name (`--list-axes`'s own namespace, #328) — accept as-is. + /// A legal RAW name (`aura graph introspect --params`'s own namespace, + /// #328) — accept as-is. Raw, /// A retired WRAPPED name; the carried string is its translated RAW /// candidate — never applied silently, only ever surfaced in a refusal. diff --git a/crates/aura-runner/src/member.rs b/crates/aura-runner/src/member.rs index fa30a2b..69fd3d6 100644 --- a/crates/aura-runner/src/member.rs +++ b/crates/aura-runner/src/member.rs @@ -659,8 +659,9 @@ pub fn run_blueprint_member( /// against: the loaded signal wrapped in the r-sma scaffolding (stop bound, /// reduce, no cost), taps discarded. `param_space()` on it is the axis /// namespace `--axis` binds; `.axis()` consumes it to seed a sweep. Single -/// source for the sweep terminal, the MC closed-check, AND `--list-axes`, so -/// the listed names track the swept names by construction (incl. across #159's +/// source for the sweep terminal, the MC closed-check, AND +/// `aura graph introspect --params`, so the listed names track the swept +/// names by construction (incl. across #159's /// harness retirement). The reload is infallible under the SAME /// dispatch-boundary contract the callers already rely on: the doc is /// `blueprint_from_json`-validated at the `["sweep", ..]` / `["mc", ..]` @@ -673,7 +674,8 @@ pub fn blueprint_axis_probe(doc: &str, env: &Env) -> Composite { /// The axis probe with a #246 override set re-opened on the strategy BEFORE /// wrapping — probe and per-member reloads must re-open identically so points /// resolve against one space. The empty-set form is byte-equal to the old -/// probe (mc/run closed-checks and the open `--list-axes` lines read that). +/// probe (mc/run closed-checks and the open `aura graph introspect --params` +/// lines read that). pub fn blueprint_axis_probe_reopened(doc: &str, env: &Env, overrides: &[String]) -> Composite { let signal = blueprint_from_json(doc, &|t| env.resolve(t)) .expect("doc parse-validated at the dispatch boundary; reload is infallible"); @@ -767,7 +769,7 @@ pub fn override_paths( None => { return Err(format!( "axis {name}: names no param of this blueprint (open or bound) — \ - see `aura sweep --list-axes`" + see `aura graph introspect --params `" )); } } diff --git a/docs/design/contracts/c14-headless-two-faces.history.md b/docs/design/contracts/c14-headless-two-faces.history.md index 2b1a2a6..afe01f9 100644 --- a/docs/design/contracts/c14-headless-two-faces.history.md +++ b/docs/design/contracts/c14-headless-two-faces.history.md @@ -36,3 +36,18 @@ execution layer is unchanged (arg-plumbing via thin `*_from` adapters). The machine-first help surface (JSON/manifest help, stdin op-scripts) is deferred to the #157 / C21 track, distinct from this human/GNU-convention compliance." + +**Current-state "Single document grammar" section's first-draft routing +sentence (superseded by the #319 sugar retirement, 2026-07-25):** "one verb, +`exec `, dispatches on `is_blueprint_file` (a first-positional +naming an existing `.json` file selects the loaded-blueprint leg; anything +else — a campaign file or a registered content id — the campaign leg)." This +undersold the routing by one layer: the code always also peeked the file's +top-level `"kind"` field (`is_campaign_document_file`) to tell a campaign +document ending in `.json` from a blueprint file — both being ordinary +readable `.json` files `is_blueprint_file` alone cannot distinguish — and, +after an independent review found a malformed campaign file was silently +misrouted to the blueprint leg's own JSON-parse error prose, gained a +neutral not-valid-JSON refusal ahead of the leg choice (review Minor-3, +2026-07-25's fix cycle). The current contract's "Single document grammar" +section names this real routing. diff --git a/docs/design/contracts/c14-headless-two-faces.md b/docs/design/contracts/c14-headless-two-faces.md index e36f53b..232dae0 100644 --- a/docs/design/contracts/c14-headless-two-faces.md +++ b/docs/design/contracts/c14-headless-two-faces.md @@ -62,11 +62,16 @@ caller branches on the failure class without parsing stderr. **Single document grammar (#319, 2026-07-25).** The five dual-grammar subcommands (run/sweep/walkforward/mc/generalize) that used to keep two grammars under one token are retired; one verb, `exec `, dispatches -on `is_blueprint_file` (a first-positional naming an existing `.json` file -selects the loaded-blueprint leg; anything else — a campaign file or a -registered content id — the campaign leg). The execution layer this -collapses onto is unchanged in kind (arg-plumbing via thin adapters). The -machine-first help surface (JSON/manifest help, stdin op-scripts) is +on `is_blueprint_file` layered with a "kind"-field peek +(`classify_exec_document_file`): a first-positional naming an existing +`.json` file that parses as JSON and carries no top-level `"kind"` selects +the loaded-blueprint leg; a file carrying `"kind"`, or a target that is not a +readable `.json` file at all (a registered content id), selects the campaign +leg. A file that does not parse as JSON at all refuses neutrally (exit 2) +before either leg's own document-shape validation runs, rather than +silently choosing a leg (review Minor-3, 2026-07-25). The execution layer +this collapses onto is unchanged in kind (arg-plumbing via thin adapters). +The machine-first help surface (JSON/manifest help, stdin op-scripts) is deferred to the #157 / [C21](c21-world.md) track, distinct from this human/GNU-convention compliance. diff --git a/docs/design/contracts/c20-strategy-harness.history.md b/docs/design/contracts/c20-strategy-harness.history.md index f3668aa..0e49538 100644 --- a/docs/design/contracts/c20-strategy-harness.history.md +++ b/docs/design/contracts/c20-strategy-harness.history.md @@ -34,3 +34,16 @@ scaffolding's last data weld — `wrap_r`'s hard-wired `price`←close role and `M1Field::Close`-only open sites — is retired; a strategy's input roles now bind archive columns by name (C26). `wrap_r`'s remaining R-scaffolding retirement stays #159.] + +**Current-state "Pre-C24 scaffolding retired" paragraph's family-builder +naming (superseded by the #319 sugar retirement, 2026-07-25):** "The family +builders are now generic and blueprint-driven: `blueprint_sweep_family`, +`blueprint_walkforward_family`, `blueprint_mc_family` (and +`blueprint_sweep_over`) in `crates/aura-runner/src/family.rs`." Those four +functions are themselves retired by #319 (2026-07-25): the campaign executor +(`aura_campaign::exec::execute`) builds every family now, driven by the +process document's stage pipeline; `family.rs` keeps only the synthetic- +stream helpers `aura_runner::reproduce` calls to re-derive a recorded family +(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`, +`synthetic_walk_sources`, `DataSource`/`DataChoice`). The current contract's +"Pre-C24 scaffolding retired" paragraph names this real state. diff --git a/docs/design/contracts/c20-strategy-harness.md b/docs/design/contracts/c20-strategy-harness.md index 2fdb2ea..651bed2 100644 --- a/docs/design/contracts/c20-strategy-harness.md +++ b/docs/design/contracts/c20-strategy-harness.md @@ -71,13 +71,20 @@ rather than a hand-enumerated menu. Persisted experiment *intent* is the campaig document (C25/C18). **Pre-C24 scaffolding retired.** The pre-C24 scaffolding — `HarnessKind` and the -per-strategy `*_sweep_family` functions — is **gone**. The family builders are now -generic and blueprint-driven: `blueprint_sweep_family`, `blueprint_walkforward_family`, -`blueprint_mc_family` (and `blueprint_sweep_over`) in -`crates/aura-runner/src/family.rs`. The R-evaluator scaffold `wrap_r` -(defined in `crates/aura-runner/src/member.rs`; imported and called from -`runner.rs`) survives; its last hard data weld — the -hard-wired `price`←close role and the `M1Field::Close`-only open sites — is retired +per-strategy `*_sweep_family` functions — is **gone**. The generic blueprint-driven +family builders that first replaced it (`blueprint_sweep_family`, +`blueprint_walkforward_family`, `blueprint_mc_family`, `blueprint_sweep_over`) are, +in turn, retired by the #319 sugar retirement (2026-07-25): the **campaign +executor** (`aura_campaign::exec::execute`) now builds every family, driven by +the process document's stage pipeline (`std::sweep`/`std::grid`, +`std::walk_forward`, `std::monte_carlo`). `crates/aura-runner/src/family.rs` +retains only the synthetic-stream helpers serving `aura_runner::reproduce` +(`showcase_prices`, `walkforward_prices`, `walkforward_window_source`, +`synthetic_walk_sources`, `DataSource`/`DataChoice`) — reproduction's own +re-derivation path, not a family-minting path. The R-evaluator scaffold +`wrap_r` (defined in `crates/aura-runner/src/member.rs`; imported and called +from `runner.rs`) survives; its last hard data weld — the hard-wired +`price`←close role and the `M1Field::Close`-only open sites — is retired (a strategy's input roles now bind archive columns by name, C26), and `wrap_r`'s remaining R-scaffolding retirement is tracked at #159.