feat: the document-first surface closes — show read-back, typed stops, bare plateau
The #300 keystone cycle: the residual run-quintet flags were already document vocabulary on the --real arm (#210/#220); this closes the loop and ratifies the surface. - `aura process|campaign show <content-id>` prints a registered document's canonical bytes (print!, no framing — #164), so generate -> retrieve -> hand-extend -> re-register needs no direct store filesystem access. Refusals reuse the store-hint prose, exit 1. - walkforward/mc --stop-length/--stop-k become clap-typed i64/f64 (generalize's form); stop_knob_or and its parse_csv_list helper are deleted (multi-value/invalid input is now clap's exit-2 rejection — the two dissolved-verb refusal tests retarget, the type-dead unit test is deleted). - --select accepts bare `plateau` as the documented default plateau:mean (#227 carry); document schema untouched — the two spellings generate content-id-identical campaigns (test-pinned). - family.rs's quadruplicated demo stop literal collapses onto one DEFAULT_STOP const (maintainer-lens drift finding). - Ledger: C25's "which projection next" line resolved (document-first delivered, per-verb identity re-ratified — F8; host/MCP stay demand-driven); C18 records the read-back. Design triage, fork decisions F1-F8, and the auto-signed spec live on the reference issue. Verification: full workspace suite 1482 passed / 0 failed with the real GER40 archive exercised (the byte-identity round-trips ran, not skipped); clippy -D warnings clean. closes #300
This commit is contained in:
+42
-91
@@ -487,12 +487,13 @@ fn resolve_campaign_name(name: &str, env: &aura_runner::project::Env) -> NameRes
|
||||
// `aura_runner::family` (#295 Task 8) — imported below by plain name so
|
||||
// call sites are unchanged.
|
||||
|
||||
/// Parse a `--select` token: `argmax` | `plateau:mean` | `plateau:worst`. Unknown
|
||||
/// tokens are a usage error (the caller maps `Err(())` to exit 2).
|
||||
/// Parse a `--select` token: `argmax` | `plateau` (alias for `plateau:mean`) |
|
||||
/// `plateau:mean` | `plateau:worst`. Unknown tokens are a usage error (the
|
||||
/// caller maps `Err(())` to exit 2).
|
||||
fn parse_select(s: &str) -> Result<Selection, ()> {
|
||||
match s {
|
||||
"argmax" => Ok(Selection::Argmax),
|
||||
"plateau:mean" => Ok(Selection::Plateau(PlateauMode::Mean)),
|
||||
"plateau" | "plateau:mean" => Ok(Selection::Plateau(PlateauMode::Mean)),
|
||||
"plateau:worst" => Ok(Selection::Plateau(PlateauMode::Worst)),
|
||||
_ => Err(()),
|
||||
}
|
||||
@@ -508,22 +509,6 @@ fn select_rule_of(sel: Selection) -> aura_research::SelectRule {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a comma-separated list of `T` (each item parsed via `FromStr`), rejecting
|
||||
/// any item that fails to parse — the shared validator for the r-sma grid flags
|
||||
/// (#137). The caller folds the `Err` into the subcommand `usage()` so a malformed
|
||||
/// `--fast 2,x` is the strict usage path, not a downstream panic. `str::split(',')`
|
||||
/// always yields at least one item, and an empty item (`""`, or a trailing comma)
|
||||
/// fails its per-item `parse`, so a non-empty result needs no separate empty-list
|
||||
/// check — the empty/blank inputs are rejected by the per-item parse itself.
|
||||
fn parse_csv_list<T: std::str::FromStr>(s: &str) -> Result<Vec<T>, ()> {
|
||||
let items: Vec<&str> = s.split(',').collect();
|
||||
let mut out = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
out.push(item.parse::<T>().map_err(|_| ())?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Render a family-member stdout line: the assigned `family_id` plus the embedded
|
||||
/// `RunReport`. The report is emitted in its own declaration key order (manifest
|
||||
/// leads with `commit`, C18) so the line is byte-identical to the stored
|
||||
@@ -1001,7 +986,7 @@ fn parse_scalar_csv(csv: &str) -> Option<Vec<Scalar>> {
|
||||
// `--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_csv_list`, `parse_select`,
|
||||
// `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
|
||||
@@ -1358,13 +1343,14 @@ struct WalkforwardCmd {
|
||||
/// mutually exclusive with --name).
|
||||
#[arg(long)]
|
||||
trace: Option<String>,
|
||||
/// Campaign-path stop length (single value; --real mode).
|
||||
/// Campaign-path stop length (--real mode).
|
||||
#[arg(long)]
|
||||
stop_length: Option<String>,
|
||||
/// Campaign-path stop-k multiple (single value; --real mode).
|
||||
stop_length: Option<i64>,
|
||||
/// Campaign-path stop-k multiple (--real mode).
|
||||
#[arg(long)]
|
||||
stop_k: Option<String>,
|
||||
/// In-sample winner selection: argmax | plateau:mean | plateau:worst (default argmax).
|
||||
stop_k: Option<f64>,
|
||||
/// In-sample winner selection: argmax | plateau (alias for plateau:mean) |
|
||||
/// plateau:mean | plateau:worst (default argmax).
|
||||
#[arg(long)]
|
||||
select: Option<String>,
|
||||
/// Blueprint IS-refit axis `<name>=<csv>` (repeatable, >=1 required; .json mode).
|
||||
@@ -1395,12 +1381,12 @@ struct McCmd {
|
||||
/// Blueprint IS-refit axis `<name>=<csv>` (repeatable, >=1 required; --real mode).
|
||||
#[arg(long)]
|
||||
axis: Vec<String>,
|
||||
/// Campaign-path stop length (single value; --real mode).
|
||||
/// Campaign-path stop length (--real mode).
|
||||
#[arg(long)]
|
||||
stop_length: Option<String>,
|
||||
/// Campaign-path stop-k multiple (single value; --real mode).
|
||||
stop_length: Option<i64>,
|
||||
/// Campaign-path stop-k multiple (--real mode).
|
||||
#[arg(long)]
|
||||
stop_k: Option<String>,
|
||||
stop_k: Option<f64>,
|
||||
/// Moving-block bootstrap block length (R-bootstrap; default 1).
|
||||
#[arg(long)]
|
||||
block_len: Option<usize>,
|
||||
@@ -1442,41 +1428,16 @@ fn run_data_from(real: Option<&str>, from: Option<i64>, to: Option<i64>) -> RunD
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a single optional stop knob (`--stop-length`/`--stop-k`): an absent flag
|
||||
/// defaults to `default`; a present one parses via [`parse_csv_list`] and refuses
|
||||
/// unless it holds exactly one value (the stop is a risk regime, not a swept axis).
|
||||
/// Single-sourced across `walkforward_args_from`/`mc_args_from` (#217 follow-up) —
|
||||
/// `generalize_args_from`'s typed `Option<i64>`/`Option<f64>` fields need no parse
|
||||
/// step, so it keeps its own plain `unwrap_or`.
|
||||
fn stop_knob_or<T: Copy + std::str::FromStr>(
|
||||
raw: Option<&str>,
|
||||
default: T,
|
||||
regime: &impl Fn() -> String,
|
||||
) -> Result<T, String> {
|
||||
match raw {
|
||||
None => Ok(default),
|
||||
Some(s) => {
|
||||
let v: Vec<T> = parse_csv_list(s).map_err(|()| regime())?;
|
||||
if v.len() != 1 {
|
||||
return Err(regime());
|
||||
}
|
||||
Ok(v[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `WalkforwardCmd` into the resolved argument shape the walkforward sugar
|
||||
/// consumes (the dissolved `.json --real` branch). Single instrument, single-value
|
||||
/// stop (Fork A: the stop is a risk regime, not a swept axis); `parse_csv_list` is
|
||||
/// generic over `T: FromStr`, so the same helper parses the i64 length and the f64
|
||||
/// k. `--stop-length`/`--stop-k` are each OPTIONAL (#217): a missing flag defaults
|
||||
/// independently to the single-sourced [`R_SMA_STOP_LENGTH`]/[`R_SMA_STOP_K`]
|
||||
/// regime; a present-but-multi-value stop still refuses (the local `regime`
|
||||
/// closure, captureless → `Copy`, is reused across both arms). `--name`/`--trace`
|
||||
/// are mutually exclusive, matching the flag's own documented contract and the
|
||||
/// inline path's `name_persist` refusal; an omitted flag defaults the family name
|
||||
/// to "walkforward". The IS-refit `--axis` grid is parsed separately at the
|
||||
/// dispatch site, not here.
|
||||
/// stop (Fork A: the stop is a risk regime, not a swept axis); `--stop-length`/
|
||||
/// `--stop-k` are typed `Option<i64>`/`Option<f64>` (clap rejects a multi-value
|
||||
/// spelling itself, "invalid value") and each OPTIONAL (#217): a missing flag
|
||||
/// defaults independently to the single-sourced [`R_SMA_STOP_LENGTH`]/
|
||||
/// [`R_SMA_STOP_K`] regime. `--name`/`--trace` are mutually exclusive, matching
|
||||
/// the flag's own documented contract and the inline path's `name_persist`
|
||||
/// refusal; an omitted flag defaults the family name to "walkforward". The
|
||||
/// IS-refit `--axis` grid is parsed separately at the dispatch site, not here.
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn walkforward_args_from(
|
||||
a: &WalkforwardCmd,
|
||||
@@ -1485,9 +1446,8 @@ fn walkforward_args_from(
|
||||
None | Some("") => return Err("walkforward dissolves only over --real <SYMBOL>".to_string()),
|
||||
Some(s) => s.to_string(),
|
||||
};
|
||||
let regime = || "walkforward: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
||||
let stop_length = stop_knob_or(a.stop_length.as_deref(), R_SMA_STOP_LENGTH, ®ime)?;
|
||||
let stop_k = stop_knob_or(a.stop_k.as_deref(), R_SMA_STOP_K, ®ime)?;
|
||||
let stop_length = a.stop_length.unwrap_or(R_SMA_STOP_LENGTH);
|
||||
let stop_k = a.stop_k.unwrap_or(R_SMA_STOP_K);
|
||||
let (name, trace) = match (a.name.as_deref(), a.trace.as_deref()) {
|
||||
(Some(_), Some(_)) => {
|
||||
return Err("walkforward: --name and --trace are mutually exclusive".to_string());
|
||||
@@ -1501,16 +1461,16 @@ fn walkforward_args_from(
|
||||
|
||||
/// Convert `McCmd` into the resolved argument shape the mc sugar consumes (the
|
||||
/// `--real` campaign branch). Single instrument, single-value stop (Fork A: the
|
||||
/// stop is a risk regime, not a swept axis); `--stop-length`/`--stop-k` are each
|
||||
/// OPTIONAL (#217), independently defaulting to the single-sourced
|
||||
/// [`R_SMA_STOP_LENGTH`]/[`R_SMA_STOP_K`] regime when omitted — a present-but-
|
||||
/// multi-value stop still refuses. `--block-len`/`--resamples`/`--seed`
|
||||
/// default to `1`/`1000`/`1` — the same defaults the retired real-R dispatch
|
||||
/// used, so an omitted flag produces the same document either way. The
|
||||
/// usize->u32 conversion lands HERE, at the argv boundary where the CLI's
|
||||
/// `usize` meets the document's `u32` vocabulary (`StageBlock::MonteCarlo`).
|
||||
/// `--name`/`--trace` are rejected: the R-bootstrap records without a family
|
||||
/// name (the campaign name is a constant "mc").
|
||||
/// stop is a risk regime, not a swept axis); `--stop-length`/`--stop-k` are typed
|
||||
/// `Option<i64>`/`Option<f64>` (clap rejects a multi-value spelling itself,
|
||||
/// "invalid value") and each OPTIONAL (#217), independently defaulting to the
|
||||
/// single-sourced [`R_SMA_STOP_LENGTH`]/[`R_SMA_STOP_K`] regime when omitted.
|
||||
/// `--block-len`/`--resamples`/`--seed` default to `1`/`1000`/`1` — the same
|
||||
/// defaults the retired real-R dispatch used, so an omitted flag produces the
|
||||
/// same document either way. The usize->u32 conversion lands HERE, at the argv
|
||||
/// boundary where the CLI's `usize` meets the document's `u32` vocabulary
|
||||
/// (`StageBlock::MonteCarlo`). `--name`/`--trace` are rejected: the R-bootstrap
|
||||
/// records without a family name (the campaign name is a constant "mc").
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn mc_args_from(
|
||||
a: &McCmd,
|
||||
@@ -1522,9 +1482,8 @@ fn mc_args_from(
|
||||
if a.name.is_some() || a.trace.is_some() {
|
||||
return Err("mc --real: --name/--trace are not accepted (the R-bootstrap records without a family name)".to_string());
|
||||
}
|
||||
let regime = || "mc: the stop is a single risk regime; --stop-length and --stop-k take one value each".to_string();
|
||||
let stop_length = stop_knob_or(a.stop_length.as_deref(), R_SMA_STOP_LENGTH, ®ime)?;
|
||||
let stop_k = stop_knob_or(a.stop_k.as_deref(), R_SMA_STOP_K, ®ime)?;
|
||||
let stop_length = a.stop_length.unwrap_or(R_SMA_STOP_LENGTH);
|
||||
let stop_k = a.stop_k.unwrap_or(R_SMA_STOP_K);
|
||||
let block_len = a.block_len.map(|v| v as u32).unwrap_or(1);
|
||||
let resamples = a.resamples.map(|v| v as u32).unwrap_or(1000);
|
||||
let seed = a.seed.unwrap_or(1);
|
||||
@@ -2319,7 +2278,7 @@ fn dispatch_sweep(a: SweepCmd, env: &aura_runner::project::Env) {
|
||||
/// #220: arbitrary user blueprint + axes, formerly the welded r-sma branch).
|
||||
fn dispatch_walkforward(a: WalkforwardCmd, env: &aura_runner::project::Env) {
|
||||
// Single-sourced: both arms below read this one closure (house style, #179).
|
||||
let usage = || format!("Usage: aura walkforward <blueprint.json> --axis <name>=<csv> [--axis …] [--select <argmax|plateau:mean|plateau:worst>] [--name <n>] | aura walkforward <blueprint.json> --real <SYMBOL> --axis <name>=<csv> [--axis …] [--stop-length <n> (default {R_SMA_STOP_LENGTH})] [--stop-k <x> (default {R_SMA_STOP_K:.1})] [--from <ms>] [--to <ms>] [--name <n> | --trace <n>]");
|
||||
let usage = || format!("Usage: aura walkforward <blueprint.json> --axis <name>=<csv> [--axis …] [--select <argmax|plateau|plateau:mean|plateau:worst>] [--name <n>] | aura walkforward <blueprint.json> --real <SYMBOL> --axis <name>=<csv> [--axis …] [--stop-length <n> (default {R_SMA_STOP_LENGTH})] [--stop-k <x> (default {R_SMA_STOP_K:.1})] [--from <ms>] [--to <ms>] [--name <n> | --trace <n>]");
|
||||
match is_blueprint_file(&a.blueprint) {
|
||||
Some(path) => {
|
||||
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
@@ -2763,6 +2722,10 @@ mod tests {
|
||||
assert!(matches!(parse_select("argmax").unwrap(), Selection::Argmax), "default is argmax");
|
||||
assert!(matches!(parse_select("plateau:mean").unwrap(), Selection::Plateau(PlateauMode::Mean)));
|
||||
assert!(matches!(parse_select("plateau:worst").unwrap(), Selection::Plateau(PlateauMode::Worst)));
|
||||
assert!(matches!(
|
||||
parse_select("plateau").unwrap(),
|
||||
Selection::Plateau(PlateauMode::Mean)
|
||||
));
|
||||
assert!(parse_select("bogus").is_err(), "unknown --select token is a usage error");
|
||||
}
|
||||
|
||||
@@ -3820,18 +3783,6 @@ mod tests {
|
||||
assert_eq!(stop_k, R_SMA_STOP_K, "omitted --stop-k defaults to the regime");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mc_args_from_refuses_a_multi_value_stop() {
|
||||
let a = McCmd {
|
||||
real: Some("GER40".to_string()),
|
||||
stop_length: Some("14,20".to_string()),
|
||||
stop_k: Some("2.0".to_string()),
|
||||
..bare_mc_cmd()
|
||||
};
|
||||
let err = mc_args_from(&a).unwrap_err();
|
||||
assert!(err.contains("single risk regime"), "stop-regime refusal: {err}");
|
||||
}
|
||||
|
||||
/// Property (#217): a missing `--stop-length`/`--stop-k` no longer refuses —
|
||||
/// each independently defaults to the single-sourced [`R_SMA_STOP_LENGTH`]/
|
||||
/// [`R_SMA_STOP_K`] regime (length 3, k 2.0).
|
||||
|
||||
@@ -29,6 +29,8 @@ pub enum ProcessSub {
|
||||
Introspect(DocIntrospectCmd),
|
||||
/// Register a valid process document into the store under the runs root.
|
||||
Register { file: PathBuf },
|
||||
/// Print a registered process document's canonical bytes to stdout.
|
||||
Show { id: String },
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
@@ -208,6 +210,7 @@ pub fn process_cmd(cmd: ProcessCmd, env: &Env) {
|
||||
introspect_process(i)
|
||||
}
|
||||
ProcessSub::Register { file } => register_process(file, env),
|
||||
ProcessSub::Show { id } => show_process(id, env),
|
||||
};
|
||||
if let Err(m) = result {
|
||||
eprintln!("aura: {m}");
|
||||
@@ -259,6 +262,21 @@ fn register_process(file: &PathBuf, env: &Env) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_process(id: &str, env: &Env) -> Result<(), String> {
|
||||
let registry = env.registry();
|
||||
match registry.get_process(id).map_err(|e| e.to_string())? {
|
||||
Some(bytes) => {
|
||||
// `print!`, not `println!`: `bytes` is the content-addressed canonical
|
||||
// form the store keyed `id` on — the CLI is a transport and must not
|
||||
// frame the canonical bytes with a trailing newline (#164, mirroring
|
||||
// `graph_construct::build_cmd`).
|
||||
print!("{bytes}");
|
||||
Ok(())
|
||||
}
|
||||
None => Err(format!("process {id} not found in the project store{}", prefix_hint(id))),
|
||||
}
|
||||
}
|
||||
|
||||
fn introspect_process(cmd: &DocIntrospectCmd) -> Result<(), String> {
|
||||
if cmd.metrics {
|
||||
print_metric_roster();
|
||||
@@ -341,6 +359,8 @@ pub enum CampaignSub {
|
||||
/// List stored campaign realizations, or dump one campaign's records
|
||||
/// (the bare store lines, not the run-emit wrapper).
|
||||
Runs { campaign: Option<String>, run: Option<usize> },
|
||||
/// Print a registered campaign document's canonical bytes to stdout.
|
||||
Show { id: String },
|
||||
}
|
||||
|
||||
/// The one authoring trap the referential tier can name outright: a ref that
|
||||
@@ -460,6 +480,7 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
}
|
||||
CampaignSub::Register { file } => register_campaign(file, env),
|
||||
CampaignSub::Runs { campaign, run } => campaign_runs(campaign.as_deref(), *run, env),
|
||||
CampaignSub::Show { id } => show_campaign(id, env),
|
||||
CampaignSub::Run { .. } => unreachable!("handled above"),
|
||||
};
|
||||
if let Err(m) = result {
|
||||
@@ -562,6 +583,19 @@ fn register_campaign(file: &PathBuf, env: &Env) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_campaign(id: &str, env: &Env) -> Result<(), String> {
|
||||
let registry = env.registry();
|
||||
match registry.get_campaign(id).map_err(|e| e.to_string())? {
|
||||
Some(bytes) => {
|
||||
// `print!`, not `println!`: see `show_process` — the CLI must not
|
||||
// frame the canonical bytes with a trailing newline (#164).
|
||||
print!("{bytes}");
|
||||
Ok(())
|
||||
}
|
||||
None => Err(format!("campaign {id} not found in the project store{}", prefix_hint(id))),
|
||||
}
|
||||
}
|
||||
|
||||
fn introspect_campaign(cmd: &DocIntrospectCmd) -> Result<(), String> {
|
||||
if cmd.metrics {
|
||||
print_metric_roster();
|
||||
|
||||
Reference in New Issue
Block a user