fix(aura-research, aura-cli, aura-runner): milestone-fieldtest bugs — the unwired ramp hole + three diagnostics
RED-first fixes for the four M42 A/B findings. campaign introspect --unwired now drills into a present-but-empty presentation section exactly as it drills into data, listing persist_taps/emit as open slots — the miss caused three of the cold start's five dead ends (the process sibling has no analogous nested-required shape, verified). The override panic catch wraps known non-diagnostic payloads (capacity overflow) in domain prose while passing real constructor messages through byte-identical; a kind-mismatched override renders as prose naming path, expected and got kinds with a write-this example instead of the ParamKindMismatch debug struct; chart's unknown-handle refusal names the current trace surface (exec --tap / presentation.persist_taps) instead of the retired --trace flag. closes #319
This commit is contained in:
@@ -430,7 +430,8 @@ fn render_chart_by_kind(
|
||||
eprintln!(
|
||||
"aura: no recorded run or family '{name}' under runs/traces \
|
||||
(check the handle a sweep/walk-forward/campaign run printed for a typo — \
|
||||
re-running with this handle as `--trace` will not create it)"
|
||||
a trace is produced by `aura exec --tap <NODE.FIELD>=<FOLD>` on a blueprint \
|
||||
or a campaign's `presentation.persist_taps` section, not by naming a handle here)"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -1069,6 +1070,23 @@ document declares its persisted taps itself"
|
||||
}
|
||||
}
|
||||
|
||||
/// Bug 2 (#319 fieldtest cycle 2): `catch_member_panic`'s payload is either a
|
||||
/// node's own constructor message (e.g. `Sma::new`'s "SMA length must be >= 1")
|
||||
/// — already a real diagnostic, rendered verbatim (existing tests pin it
|
||||
/// byte-identical) — or a raw library/allocator panic string that names no
|
||||
/// domain rule of any node at all (a negative length overflows the ring-buffer
|
||||
/// allocation before the SMA's own check ever runs). The latter is not a
|
||||
/// diagnostic; wrap only the known non-diagnostic payloads in prose that
|
||||
/// tells the caller what actually happened, keeping the raw string as
|
||||
/// parenthetical detail rather than the whole message.
|
||||
fn panic_refusal_prose(msg: &str) -> String {
|
||||
if msg == "capacity overflow" {
|
||||
format!("param value out of domain for this node (panic: {msg}) — check the param's valid range")
|
||||
} else {
|
||||
msg.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The `exec` blueprint leg (#319): a synthetic single run of a fully-bound
|
||||
/// blueprint envelope, composed 1:1 from `dispatch_run`'s `.json`-branch
|
||||
/// building blocks — same intake gate, same shape dispatch (a `bias` output
|
||||
@@ -1199,7 +1217,7 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au
|
||||
run_signal_r(signal, ¶ms, RunData::Synthetic, 0, env, tap_plan)
|
||||
})
|
||||
.unwrap_or_else(|msg| {
|
||||
eprintln!("aura: {msg}");
|
||||
eprintln!("aura: {}", panic_refusal_prose(&msg));
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!("{}", report.to_json());
|
||||
|
||||
@@ -421,6 +421,31 @@ fn chart_unknown_name_refuses_with_exit_1_and_message() {
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// BUG 4 (#319 fieldtest cycle 2): the not-found remedy still names `--trace`
|
||||
/// — a flag no verb carries anymore (verb dissolution, #220). It must point
|
||||
/// at the CURRENT trace-production surface instead: `exec --tap` for a
|
||||
/// single blueprint, or a campaign's `presentation.persist_taps` section.
|
||||
#[test]
|
||||
fn chart_unknown_name_refusal_names_the_current_trace_surface_not_trace_flag() {
|
||||
let cwd = temp_cwd("chart-unknown-trace-surface");
|
||||
|
||||
let out =
|
||||
Command::new(BIN).args(["chart", "ghost"]).current_dir(&cwd).output().expect("spawn chart ghost");
|
||||
assert_eq!(out.status.code(), Some(1), "unknown name must exit 1: {:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
!stderr.contains("--trace"),
|
||||
"the retired --trace flag must not appear anywhere in the refusal: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("--tap") && stderr.contains("persist_taps"),
|
||||
"the refusal must point at the current trace-production surface \
|
||||
(exec --tap / a campaign's presentation.persist_taps): {stderr}"
|
||||
);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// Property (milestone fieldtest B1): a sweep / walk-forward `--trace` family is
|
||||
/// chartable by the very handle the producing run prints. Those verbs fan a
|
||||
/// selection-free sweep out per member into the #224 depth-2 layout
|
||||
|
||||
@@ -546,6 +546,48 @@ fn exec_override_negative_value_refuses_not_panics() {
|
||||
assert!(out.contains("capacity overflow"), "stdout/stderr: {out}");
|
||||
}
|
||||
|
||||
/// BUG 2 (#319 fieldtest cycle 2): a negative override value still exits 1
|
||||
/// (the earlier fix caught the panic), but the message rendered the raw
|
||||
/// allocator string verbatim (`aura: capacity overflow`) — a library panic
|
||||
/// payload, not a user-facing diagnostic naming any node's domain rule. The
|
||||
/// known non-diagnostic payload must be wrapped in domain-refusal prose (the
|
||||
/// raw string kept as parenthetical detail, never the entire message), while
|
||||
/// a real constructor message (`SMA length must be >= 1`,
|
||||
/// `exec_override_out_of_domain_value_refuses_not_panics` above) stays
|
||||
/// byte-identical.
|
||||
#[test]
|
||||
fn exec_override_negative_value_wraps_the_raw_panic_in_domain_prose() {
|
||||
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=-1"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("out of domain"),
|
||||
"the raw allocator string must be wrapped in a domain refusal: {out}"
|
||||
);
|
||||
assert_ne!(
|
||||
out.trim(),
|
||||
"aura: capacity overflow",
|
||||
"the bare allocator string must not be the whole message: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// BUG 3 (#319 fieldtest cycle 2): a kind-mismatched override (`bias.scale`
|
||||
/// is F64, `2` lexes as I64) reaches the exec leg's compile boundary as a
|
||||
/// raw `CompileError::ParamKindMismatch { .. }` Debug struct. It must render
|
||||
/// as prose naming the param path and both kinds, mirroring the campaign
|
||||
/// leg's `ref_fault_prose` treatment of the same fault class.
|
||||
#[test]
|
||||
fn exec_override_kind_mismatch_refuses_with_prose_not_the_debug_struct() {
|
||||
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "bias.scale=2"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
!out.contains("ParamKindMismatch"),
|
||||
"the Debug struct must not leak: {out}"
|
||||
);
|
||||
assert!(out.contains("bias.scale"), "the param path must be named: {out}");
|
||||
assert!(out.contains("F64"), "the expected kind must be named: {out}");
|
||||
assert!(out.contains("I64"), "the got kind must be named: {out}");
|
||||
}
|
||||
|
||||
/// BUG B2 (fieldtest, #319 exec --override leg): the retired pre-#328
|
||||
/// WRAPPED override form (`<rootname>.<node>.<param>`) panics at the
|
||||
/// params-zip `.expect` in `exec_blueprint_leg` instead of refusing:
|
||||
|
||||
@@ -1309,14 +1309,31 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
||||
if v.get("seed").and_then(|s| s.as_u64()).is_none() {
|
||||
slots.push(open("seed", "required, non-negative integer"));
|
||||
}
|
||||
if v.get("presentation").is_none() {
|
||||
slots.push(open(
|
||||
match v.get("presentation") {
|
||||
None => slots.push(open(
|
||||
"presentation",
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
));
|
||||
)),
|
||||
Some(p) => {
|
||||
if p.get("persist_taps").and_then(|x| x.as_array()).is_none() {
|
||||
slots.push(open(
|
||||
"presentation.persist_taps",
|
||||
format!(
|
||||
"required, list of: {}",
|
||||
tap_vocabulary().iter().map(|t| t.id).collect::<Vec<_>>().join(" | ")
|
||||
),
|
||||
));
|
||||
}
|
||||
if p.get("emit").and_then(|x| x.as_array()).is_none() {
|
||||
slots.push(open(
|
||||
"presentation.emit",
|
||||
format!("required, list of: {}", emit_vocabulary().join(" | ")),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(slots)
|
||||
}
|
||||
@@ -2294,6 +2311,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #319 fieldtest bug 1: a present-but-empty `presentation: {}` must drill
|
||||
/// down into its two required sub-slots exactly as a present-but-empty
|
||||
/// `data: {}` drills into `instruments`/`windows` — the walker must not
|
||||
/// stop at "the section exists" without checking what's inside it.
|
||||
#[test]
|
||||
fn presentation_drills_into_an_empty_section_like_data_does() {
|
||||
let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft",
|
||||
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
|
||||
"strategies": [ { "ref": { "content_id": "9f3a" },
|
||||
"axes": { "slow": { "kind": "I64", "values": [10] } } } ],
|
||||
"process": { "ref": { "content_id": "4e2d" } },
|
||||
"seed": 1,
|
||||
"presentation": {} }"#;
|
||||
let slots = open_slots_campaign(draft).unwrap();
|
||||
assert!(
|
||||
!slots.iter().any(|s| s.path == "presentation"),
|
||||
"an empty-but-present presentation is not the same open slot as an absent one: {slots:?}"
|
||||
);
|
||||
assert!(
|
||||
slots.iter().any(|s| s.path == "presentation.persist_taps"
|
||||
&& s.hint == "required, list of: equity | exposure | r_equity | net_r_equity"),
|
||||
"presentation.persist_taps must be named as its own open slot: {slots:?}"
|
||||
);
|
||||
assert!(
|
||||
slots.iter().any(|s| s.path == "presentation.emit"
|
||||
&& s.hint == "required, list of: family_table | selection_report"),
|
||||
"presentation.emit must be named as its own open slot: {slots:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #256 fork B: the fieldless enumerate-only stage round-trips through
|
||||
/// the schema-strict parser, and any slot on it is refused (its slot
|
||||
/// list is empty, so the generic unknown-slot check covers every key).
|
||||
|
||||
@@ -15,8 +15,8 @@ use std::sync::{mpsc, Arc, LazyLock};
|
||||
use aura_composites::{cost_graph, risk_executor, StopRule};
|
||||
use aura_core::{zip_params, Cell, Firing, ParamSpec, PrimitiveBuilder, Scalar, ScalarKind, Timestamp};
|
||||
use aura_engine::{
|
||||
blueprint_from_json, window_of, BlueprintNode, Composite, GraphBuilder, Harness, RunManifest,
|
||||
VecSource,
|
||||
blueprint_from_json, window_of, BlueprintNode, CompileError, Composite, GraphBuilder, Harness,
|
||||
RunManifest, VecSource,
|
||||
};
|
||||
use aura_backtest::{
|
||||
summarize, summarize_r, RunMetrics, RunReport, SimBroker, PM_FIELD_NAMES, PM_RECORD_KINDS,
|
||||
@@ -471,6 +471,49 @@ pub fn resolve_run_data(
|
||||
}
|
||||
}
|
||||
|
||||
/// The variant-name string for a `ScalarKind`, matching the wire/CLI spelling
|
||||
/// used across the codebase (`F64`/`I64`/`Bool`/`Timestamp`).
|
||||
fn scalar_kind_name(kind: ScalarKind) -> &'static str {
|
||||
match kind {
|
||||
ScalarKind::F64 => "F64",
|
||||
ScalarKind::I64 => "I64",
|
||||
ScalarKind::Bool => "Bool",
|
||||
ScalarKind::Timestamp => "Timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
/// Bug 3 (#319 fieldtest cycle 2): an `--override` value whose lexed kind
|
||||
/// does not match the param's declared kind (e.g. the bare literal `2` for
|
||||
/// an F64 param) reaches this compile boundary as a raw
|
||||
/// `CompileError::ParamKindMismatch { slot, expected, got }` — `slot` is a
|
||||
/// flat param-space index, meaningless to a caller. `names`/`params` are the
|
||||
/// SAME order `compile_with_params` was called with (built from `signal`
|
||||
/// before it was consumed by `wrap_r`, `C11`: composites inline, preserving
|
||||
/// order), so `slot` indexes both. This names the param path and both kinds,
|
||||
/// mirroring the campaign leg's own kind-fault prose
|
||||
/// (`ref_fault_prose`'s `AxisKindMismatch` arm, `research_docs.rs`). Every
|
||||
/// other `CompileError` variant keeps the existing Debug fallback — out of
|
||||
/// this bug's scope, and `names`/`params` only reconstruct the one variant
|
||||
/// that carries an injected value.
|
||||
fn compile_error_prose(e: &CompileError, names: &[String], params: &[Scalar]) -> String {
|
||||
let CompileError::ParamKindMismatch { slot, expected, got } = e else {
|
||||
return format!("this blueprint does not compile to a runnable harness: {e:?}");
|
||||
};
|
||||
let path = names.get(*slot).map(String::as_str).unwrap_or("<unknown>");
|
||||
let example = match (expected, params.get(*slot)) {
|
||||
(ScalarKind::F64, Some(Scalar::I64(v))) => format!("{v}.0"),
|
||||
(ScalarKind::F64, _) => "a decimal literal, e.g. 2.0".to_string(),
|
||||
(ScalarKind::I64, _) => "a bare integer literal, e.g. 2".to_string(),
|
||||
(ScalarKind::Bool, _) => "true or false".to_string(),
|
||||
(ScalarKind::Timestamp, _) => "an integer epoch-ns literal".to_string(),
|
||||
};
|
||||
format!(
|
||||
"--override {path}: expects {}, got {} — write {example}",
|
||||
scalar_kind_name(*expected),
|
||||
scalar_kind_name(*got)
|
||||
)
|
||||
}
|
||||
|
||||
/// Run a signal blueprint through the R scaffolding: hash the signal,
|
||||
/// wrap it (broker + equity/exposure/R sinks), compile with `params`, bootstrap,
|
||||
/// run over `data`, and build the RunReport (manifest carries topology_hash).
|
||||
@@ -514,7 +557,7 @@ pub fn run_signal_r(
|
||||
let (sources, window, pip_size) = resolve_run_data(&data, env, &binding);
|
||||
let wrapped = wrap_r(signal, tx_eq, tx_ex, tx_r, tx_req, StopRule::Vol { length: R_SMA_STOP_LENGTH, k: R_SMA_STOP_K }, false, pip_size, &binding, None);
|
||||
let mut flat = wrapped.compile_with_params(params).unwrap_or_else(|e| {
|
||||
eprintln!("aura: this blueprint does not compile to a runnable harness: {e:?}");
|
||||
eprintln!("aura: {}", compile_error_prose(&e, &names, params));
|
||||
std::process::exit(1);
|
||||
});
|
||||
// Bind each declared tap per the plan's subscription, before bootstrap
|
||||
|
||||
Reference in New Issue
Block a user