34987be389
Iteration stderr-markers-1, tasks 1-2 of the stderr-class-markers plan (spec signed via grounding-check PASS, decisions logged on #278). The two stderr diagnostic classes become machine-separable (refs #278): a new crate-internal diag module owns the grammar as note!/warning! macros (`aura: note: <text>` benign continuing-run diagnostic, `aura: warning: <text>` recorded fault the run survives); the four undifferentiated sites are retagged — gate-emptied cell -> note, failed cell / walkforward cell / mc cell -> warning — and the four scaffold literals migrate onto the shared macro (output bytes unchanged). The always-on record summary line gains the bare `aura: ` namespace it was missing; bare-prefix stays the vocabulary for error lines accompanying a non-zero exit (C14 partition) and plain info lines. Exit codes unchanged throughout. Verification: RED->GREEN warning pin on the per-cell-fault e2e; new gate-emptied e2e (impossible n_trades threshold) pins the note marker, exit 0, and the summary-line prefix; scaffold e2es and the full workspace suite green; clippy -D warnings clean on aura-cli. The C28 shell-module roster test gained the diag.rs entry. Known residue for the next slices: aura-runner literals (stale-dylib warning, three tap/no-nominee notes) and the #313 zero-trade notice follow in tasks 3-5. The verb_sugar warning call sites remain e2e- uncovered (a hostless walkforward fault fixture is structurally unreachable — single-instrument validation refuses before the cell loop); their grammar is pinned via the shared macro and the campaign warning e2e. refs #278
532 lines
23 KiB
Rust
532 lines
23 KiB
Rust
//! `aura campaign run` — the driver that turns a stored campaign document into
|
|
//! a realized run-set (#198). The execution *semantics* (preflight, cell loop,
|
|
//! stage sequencing, selection, registry writes) live in `aura-campaign`; the
|
|
//! [`MemberRunner`](aura_campaign::MemberRunner) implementation over the shipped loaded-blueprint machinery
|
|
//! and the trace-persistence disk-layout writers live in `aura-runner`'s
|
|
//! `runner` module (#295 Task 7 — `DefaultMemberRunner` +
|
|
//! `persist_campaign_traces`). This module owns what is CLI-specific: target
|
|
//! resolution (file sugar vs content id), the project + referential gates,
|
|
//! fault prose (`exec_fault_prose` lives HERE, beside its consumer, not in
|
|
//! `research_docs.rs` — it phrases `aura_campaign` types the doc-tier module
|
|
//! deliberately does not import), and stdout/stderr emission.
|
|
//!
|
|
//! The member-run recipe (`blueprint_axis_probe`, `run_blueprint_member`,
|
|
//! `wrap_r`, …) and the axis/translate helpers live in `aura-runner` (#295)
|
|
//! and are reached by plain name via the `use` imports below. Remaining
|
|
//! main.rs-root items (`family_member_line`, …) are still reached via
|
|
//! `crate::` — the `graph_construct` submodule idiom: main.rs is the crate
|
|
//! root, so its private items are visible to child modules without promotion.
|
|
|
|
use std::num::NonZeroUsize;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::Arc;
|
|
|
|
use aura_campaign::ExecFault;
|
|
use aura_core::Scalar;
|
|
use aura_engine::FamilySelection;
|
|
use aura_registry::CampaignRunRecord;
|
|
use aura_research::{
|
|
campaign_to_json, content_id_of, parse_campaign, parse_process, validate_campaign,
|
|
validate_process, CampaignDoc, DocRef,
|
|
};
|
|
|
|
use aura_runner::axes::is_content_id;
|
|
use aura_runner::project::Env;
|
|
use aura_runner::runner::{persist_campaign_traces, DefaultMemberRunner};
|
|
use crate::research_docs::{
|
|
doc_error_prose, doc_fault_prose, fault_block, parse_valid_campaign, ref_fault_prose,
|
|
};
|
|
|
|
/// Phrase an [`ExecFault`] for stderr — Debug-leak-free, path-addressed
|
|
/// (stage index + block id), the `doc_fault_prose`/`ref_fault_prose` register.
|
|
pub(crate) fn exec_fault_prose(f: &ExecFault) -> String {
|
|
match f {
|
|
ExecFault::PipelineShape { detail } => {
|
|
format!("process pipeline is not executable: {detail}")
|
|
}
|
|
ExecFault::UnrankableMetric { stage, metric } => format!(
|
|
"process stage {stage}: metric \"{metric}\" is not rankable (winner \
|
|
selection needs one of the registry's rankable metrics; see: aura \
|
|
process introspect --metrics)"
|
|
),
|
|
ExecFault::GateMetricNotPerMember { stage, metric } => format!(
|
|
"process stage {stage}: gate metric \"{metric}\" is not a per-member \
|
|
scalar (selection annotations cannot gate members; see: aura process \
|
|
introspect --metrics)"
|
|
),
|
|
ExecFault::PlateauInWalkForward { stage } => format!(
|
|
"process stage {stage}: walk_forward cannot use a plateau select (a \
|
|
gated survivor subset has no parameter lattice)"
|
|
),
|
|
ExecFault::DeflatePlateauConflict { stage } => format!(
|
|
"process stage {stage}: sweep deflate: true composes only with select \"argmax\""
|
|
),
|
|
ExecFault::SelectionFreeSweepNotTerminal { stage } => format!(
|
|
"process stage {stage}: a sweep without a selection group (metric + \
|
|
select) must be the last stage of its process"
|
|
),
|
|
ExecFault::GeneralizeNeedsInstruments { available } => format!(
|
|
"campaign has {available} instrument(s); std::generalize needs at least 2"
|
|
),
|
|
ExecFault::GeneralizeNonRMetric { metric } => format!(
|
|
"std::generalize metric \"{metric}\" is not in the rankable R-expectancy \
|
|
family (expectancy_r, net_expectancy_r, sqn, sqn_normalized generalize; \
|
|
see: aura process introspect --metrics)"
|
|
),
|
|
ExecFault::ZeroBootstrapParam { stage, field } => format!(
|
|
"process stage {stage}: monte_carlo {field} must be > 0"
|
|
),
|
|
ExecFault::Window { stage, detail } => format!(
|
|
"process stage {stage}: walk_forward windows do not fit the campaign \
|
|
window: {detail}"
|
|
),
|
|
ExecFault::Member(m) => aura_campaign::member_fault_prose(m),
|
|
ExecFault::Registry(e) => e.to_string(),
|
|
}
|
|
}
|
|
|
|
/// stdout wire form of one selection-bearing stage. Field order is the wire
|
|
/// contract (serde derives declaration order).
|
|
#[derive(serde::Serialize)]
|
|
struct SelectionReportLine<'a> {
|
|
selection_report: SelectionReportBody<'a>,
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct SelectionReportBody<'a> {
|
|
family_id: &'a str,
|
|
stage: usize,
|
|
block: &'a str,
|
|
winner_ordinal: usize,
|
|
params: &'a [(String, Scalar)],
|
|
selection: &'a FamilySelection,
|
|
}
|
|
|
|
/// The always-on final line: the stored realization record under one key.
|
|
#[derive(serde::Serialize)]
|
|
struct CampaignRunLine<'a> {
|
|
campaign_run: &'a CampaignRunRecord,
|
|
}
|
|
|
|
/// Stdout shape of a campaign run. `Full` is `aura campaign run` (emit-gated
|
|
/// member/selection lines + the always-on final `campaign_run` record line).
|
|
/// `MemberLinesOnly` is dissolved-verb sugar: the generated document's
|
|
/// `emit: ["family_table"]` already limits emission to member lines; the mode
|
|
/// additionally suppresses the final record line so the verb's stdout
|
|
/// contract is reproduced byte-for-byte. The record append is identical in
|
|
/// both modes — presentation changes, the record does not.
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
pub(crate) enum RunPresentation {
|
|
Full,
|
|
MemberLinesOnly,
|
|
}
|
|
|
|
/// `aura campaign run <target>`: resolve, gate, execute, emit. Every `Err`
|
|
/// is a refusal `campaign_cmd` renders as `aura: {msg}` + exit 1; an `Ok`
|
|
/// carries the failed-cell count (#272) so the caller can exit 3 on a
|
|
/// completed-with-failures run via `exit_on_campaign_result`.
|
|
pub(crate) fn run_campaign(
|
|
target: &str,
|
|
env: &Env,
|
|
parallel_instruments: NonZeroUsize,
|
|
) -> Result<usize, String> {
|
|
// Project gate FIRST: nothing (not even the file-sugar registration)
|
|
// touches a store outside a project.
|
|
if env.provenance().is_none() {
|
|
let cwd = std::env::current_dir()
|
|
.map(|d| d.display().to_string())
|
|
.unwrap_or_default();
|
|
return Err(format!(
|
|
"campaign run needs a project: strategies resolve against the project \
|
|
store and vocabulary (no Aura.toml found up from {cwd})"
|
|
));
|
|
}
|
|
let registry = env.registry();
|
|
|
|
// Target resolution: a readable file is register-then-run sugar; a bare
|
|
// 64-hex token addresses the store directly; anything else refuses naming
|
|
// both readings. A CLI arg is ephemeral input, so a leading `content:`
|
|
// display prefix is tolerated here (#194) — doc ref FIELDS stay bare-only
|
|
// (canonical-form byte stability).
|
|
let target = target.strip_prefix("content:").filter(|t| is_content_id(t)).unwrap_or(target);
|
|
let campaign_id = if Path::new(target).is_file() {
|
|
let doc = parse_valid_campaign(&PathBuf::from(target))?;
|
|
registry
|
|
.put_campaign(&campaign_to_json(&doc))
|
|
.map_err(|e| e.to_string())?
|
|
} else if is_content_id(target) {
|
|
target.to_string()
|
|
} else {
|
|
return Err(format!(
|
|
"'{target}' is neither a readable .json file nor a 64-hex content id"
|
|
));
|
|
};
|
|
|
|
run_campaign_by_id(&campaign_id, env, RunPresentation::Full, parallel_instruments)
|
|
}
|
|
|
|
/// An executed campaign plus the context its presenter and the dissolved-verb
|
|
/// sugar consume: `outcome` (records + per-cell realizations) and the
|
|
/// `campaign` / `strategies` / `server` the emit + trace-persistence tail need.
|
|
/// Returned by [`run_campaign_returning`] so a caller can read the outcome
|
|
/// without the stdout tail (a forthcoming dissolved-verb sugar path — the
|
|
/// generalize translator's own runner lands in a later task).
|
|
pub(crate) struct CampaignRun {
|
|
pub outcome: aura_campaign::CampaignOutcome,
|
|
pub campaign: CampaignDoc,
|
|
pub strategies: Vec<(String, String)>,
|
|
pub server: Arc<data_server::DataServer>,
|
|
}
|
|
|
|
/// The one campaign executor path from a resolved content id onward: fetch
|
|
/// the stored canonical bytes by id (so file addressing and id addressing
|
|
/// produce the same realization by construction) and re-run the intrinsic
|
|
/// tier on them, execute, and emit per `presentation`. Shared by
|
|
/// `run_campaign` (`RunPresentation::Full`) and the dissolved-verb sugar path
|
|
/// (`verb_sugar::run_sweep_sugar`, `RunPresentation::MemberLinesOnly`) — no
|
|
/// project gate here: the sugar path must work project-less exactly as the
|
|
/// inline verb it replaces did (store mechanics run against `env.registry()`
|
|
/// in both cases).
|
|
pub(crate) fn run_campaign_by_id(
|
|
campaign_id: &str,
|
|
env: &Env,
|
|
presentation: RunPresentation,
|
|
parallel_instruments: NonZeroUsize,
|
|
) -> Result<usize, String> {
|
|
let run = run_campaign_returning(campaign_id, env, parallel_instruments)?;
|
|
present_campaign(run, presentation, env)
|
|
}
|
|
|
|
/// The one campaign executor path from a resolved content id up to (not
|
|
/// including) presentation: fetch the stored canonical bytes by id (so file
|
|
/// addressing and id addressing produce the same realization by construction),
|
|
/// re-run the intrinsic tier, resolve strategies, execute — returning the
|
|
/// outcome bundled with the context the presenter needs. Shared by
|
|
/// `run_campaign_by_id` (which then presents) and a forthcoming dissolved-verb
|
|
/// sugar path that reads the outcome and self-prints (lands in a later task,
|
|
/// alongside the generalize translator's own runner).
|
|
pub(crate) fn run_campaign_returning(
|
|
campaign_id: &str,
|
|
env: &Env,
|
|
parallel_instruments: NonZeroUsize,
|
|
) -> Result<CampaignRun, String> {
|
|
let registry = env.registry();
|
|
let campaign_text = registry
|
|
.get_campaign(campaign_id)
|
|
.map_err(|e| e.to_string())?
|
|
.ok_or_else(|| format!("no campaign {campaign_id} in the project store"))?;
|
|
let campaign = parse_campaign(&campaign_text)
|
|
.map_err(|e| doc_error_prose("stored campaign document", &e))?;
|
|
let faults = validate_campaign(&campaign);
|
|
if !faults.is_empty() {
|
|
return Err(fault_block(
|
|
"campaign document invalid:",
|
|
faults.iter().map(doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
|
|
// Referential gate: zero faults or refuse (the campaign-validate seam).
|
|
let resolve = |t: &str| env.resolve(t);
|
|
let ref_faults = registry
|
|
.validate_campaign_refs(&campaign, &resolve)
|
|
.map_err(|e| e.to_string())?;
|
|
if !ref_faults.is_empty() {
|
|
return Err(fault_block(
|
|
"campaign references do not resolve:",
|
|
ref_faults.iter().map(ref_fault_prose).collect(),
|
|
));
|
|
}
|
|
|
|
// Process fetch + intrinsic validation (stored text, not a file path —
|
|
// parse_valid_process is file-based, so its constituents run here).
|
|
let DocRef::ContentId(process_id) = &campaign.process.r#ref else {
|
|
// validate_campaign already refuses identity process refs; defensive.
|
|
return Err("process.ref: a process is referenced by content id in this version".into());
|
|
};
|
|
let process_text = registry
|
|
.get_process(process_id)
|
|
.map_err(|e| e.to_string())?
|
|
.ok_or_else(|| format!("no process {process_id} in the project store"))?;
|
|
let process = parse_process(&process_text)
|
|
.map_err(|e| doc_error_prose("stored process document", &e))?;
|
|
let process_faults = validate_process(&process);
|
|
if !process_faults.is_empty() {
|
|
return Err(fault_block(
|
|
"process document invalid:",
|
|
process_faults.iter().map(doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
|
|
// Strategies: canonical bytes from the store, index-aligned with the doc.
|
|
// The recorded id is the content id of those bytes (== the ref id for
|
|
// content refs; computed for identity refs).
|
|
let mut strategies: Vec<(String, String)> = Vec::with_capacity(campaign.strategies.len());
|
|
for entry in &campaign.strategies {
|
|
let canonical = match &entry.r#ref {
|
|
DocRef::ContentId(id) => registry
|
|
.get_blueprint(id)
|
|
.map_err(|e| e.to_string())?
|
|
.ok_or_else(|| format!("strategy {id} not found in the blueprint store"))?,
|
|
DocRef::IdentityId(id) => registry
|
|
.find_blueprint_by_identity(id, &resolve)
|
|
.map_err(|e| e.to_string())?
|
|
.ok_or_else(|| format!("identity id {id} matches no stored blueprint"))?,
|
|
};
|
|
strategies.push((content_id_of(&canonical), canonical));
|
|
}
|
|
|
|
let runner = DefaultMemberRunner::new(
|
|
env,
|
|
Arc::new(data_server::DataServer::new(env.data_path())),
|
|
campaign.data.bindings.clone(),
|
|
campaign.cost.clone(),
|
|
);
|
|
let outcome = aura_campaign::execute(
|
|
campaign_id,
|
|
&campaign,
|
|
&process,
|
|
&strategies,
|
|
&runner,
|
|
®istry,
|
|
parallel_instruments,
|
|
)
|
|
.map_err(|f| exec_fault_prose(&f))?;
|
|
|
|
Ok(CampaignRun { outcome, campaign, strategies, server: runner.server() })
|
|
}
|
|
|
|
/// Emit an executed campaign's results per `presentation`: the zero-survivor
|
|
/// stderr notes, the emit-gated family/selection lines, the always-on record
|
|
/// line (Full only), then trace persistence. Split out of `run_campaign_by_id`
|
|
/// so the dissolved-verb sugar can consume the outcome without this stdout tail.
|
|
fn present_campaign(
|
|
run: CampaignRun,
|
|
presentation: RunPresentation,
|
|
env: &Env,
|
|
) -> Result<usize, String> {
|
|
let CampaignRun { outcome, campaign, strategies, server } = run;
|
|
|
|
// Zero-survivor stderr notes (exit stays 0 — a null result is a valid
|
|
// research result, #198 decision 8). Addressed by the fields the record
|
|
// already carries (strategy/instrument/window_ms), not by re-deriving a
|
|
// doc-order ordinal from the loop-nesting the executor happens to use
|
|
// today — that duplicated invariant would silently drift if the executor
|
|
// ever reorders its cell loop.
|
|
for cell in &outcome.record.cells {
|
|
for (stage_ix, st) in cell.stages.iter().enumerate() {
|
|
if matches!(&st.survivor_ordinals, Some(v) if v.is_empty()) {
|
|
crate::diag::note!(
|
|
"cell {}/{}/[{}, {}]: gate at stage {stage_ix} left no \
|
|
survivors; cell realization truncated",
|
|
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// #272: failed-cell notes (the gate-empty idiom's sibling) — a failed
|
|
// cell never aborts the campaign; its fault is recorded and the run
|
|
// continues over the remaining cells.
|
|
let mut failed = 0usize;
|
|
let mut fail_labels: Vec<String> = Vec::new();
|
|
for cell in &outcome.record.cells {
|
|
if let Some(f) = &cell.fault {
|
|
failed += 1;
|
|
fail_labels.push(format!("{}: {}", cell.instrument, cell_fault_kind_label(f.kind)));
|
|
crate::diag::warning!(
|
|
"cell ({}, {}, [{}, {}]) failed at stage {}: {} — recorded, campaign continues",
|
|
cell.strategy, cell.instrument, cell.window_ms.0, cell.window_ms.1, f.stage, f.detail
|
|
);
|
|
}
|
|
}
|
|
|
|
// Emission: emit-gated family/selection lines per cell, then the
|
|
// always-on final record line. Matched by NAME against the two closed-
|
|
// vocabulary terms (self-evident at the call site, unlike positional
|
|
// indexing into the vocab slice); the debug_assert keeps the names
|
|
// honest against `aura_research::emit_vocabulary()` so a #190
|
|
// rename/extend of the closed set fails loudly here instead of silently
|
|
// misrouting emission.
|
|
debug_assert!(
|
|
aura_research::emit_vocabulary().contains(&"family_table")
|
|
&& aura_research::emit_vocabulary().contains(&"selection_report"),
|
|
"emit_vocabulary drifted from the names campaign_run matches by"
|
|
);
|
|
let emit_family = campaign.presentation.emit.iter().any(|e| e == "family_table");
|
|
let emit_selection = campaign.presentation.emit.iter().any(|e| e == "selection_report");
|
|
for cell_out in &outcome.cells {
|
|
if emit_family {
|
|
for fam in &cell_out.families {
|
|
for report in &fam.reports {
|
|
println!("{}", crate::family_member_line(&fam.family_id, report));
|
|
}
|
|
}
|
|
}
|
|
if emit_selection {
|
|
for sel in &cell_out.selections {
|
|
let line = SelectionReportLine {
|
|
selection_report: SelectionReportBody {
|
|
family_id: &sel.family_id,
|
|
stage: sel.stage,
|
|
block: sel.block,
|
|
winner_ordinal: sel.winner_ordinal,
|
|
params: &sel.params,
|
|
selection: &sel.selection,
|
|
},
|
|
};
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string(&line).expect("selection report serializes")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
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(", "))
|
|
}
|
|
);
|
|
}
|
|
|
|
// Trace persistence runs LAST, after the always-on record line (0109):
|
|
// stdout stays data-pure (the record line is the wire contract) and the
|
|
// trace surface is a stderr concern; the realization record is already
|
|
// stored by `execute`, so a trace failure exits 1 without un-recording it.
|
|
if let Some(trace_name) = &outcome.record.trace_name {
|
|
persist_campaign_traces(
|
|
trace_name,
|
|
&campaign.presentation.persist_taps,
|
|
&outcome,
|
|
&campaign,
|
|
&strategies,
|
|
&server,
|
|
env,
|
|
)?;
|
|
}
|
|
Ok(failed)
|
|
}
|
|
|
|
/// #272: the fault-kind label the failed-cell summary line names — the same
|
|
/// closed `CellFaultKind` vocabulary an aggregate over `campaign_runs.jsonl`
|
|
/// counts by. Debug-leak-free (a fixed lowercase-snake label, not a Debug
|
|
/// frame of the enum variant).
|
|
fn cell_fault_kind_label(kind: aura_registry::CellFaultKind) -> &'static str {
|
|
use aura_registry::CellFaultKind as K;
|
|
match kind {
|
|
K::NoData => "no_data",
|
|
K::Bind => "bind",
|
|
K::Run => "run",
|
|
K::Panic => "panic",
|
|
K::Window => "window",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
/// #197 (fieldtest 0107 F9): the executor's preflight refusal for a
|
|
/// non-rankable selection metric points the author at the verb that
|
|
/// enumerates the applicable roster, so the exists-but-not-rankable split
|
|
/// (a metric emitted in every member's block yet refused for winner
|
|
/// selection) is resolvable without trial and error. The intrinsic message
|
|
/// is unchanged; only a discovery pointer is appended.
|
|
fn unrankable_metric_prose_points_at_the_metrics_introspection() {
|
|
let prose = exec_fault_prose(&ExecFault::UnrankableMetric {
|
|
stage: 0,
|
|
metric: "profit_factor".into(),
|
|
});
|
|
assert!(prose.contains("is not rankable"), "intrinsic message kept: {prose}");
|
|
assert!(
|
|
prose.contains("aura process introspect --metrics"),
|
|
"the refusal must point at the enumeration verb: {prose}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// #197: the twin refusal — a gate predicate over a non-per-member scalar
|
|
/// (a selection annotation) — likewise points at the enumeration verb.
|
|
fn gate_metric_not_per_member_prose_points_at_the_metrics_introspection() {
|
|
let prose = exec_fault_prose(&ExecFault::GateMetricNotPerMember {
|
|
stage: 1,
|
|
metric: "deflated_score".into(),
|
|
});
|
|
assert!(prose.contains("is not a per-member"), "intrinsic message kept: {prose}");
|
|
assert!(
|
|
prose.contains("aura process introspect --metrics"),
|
|
"the refusal must point at the enumeration verb: {prose}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
/// generalize's static instrument-arity refusal is Debug-free and
|
|
/// names the available count plus the required floor.
|
|
fn generalize_needs_instruments_prose_names_the_shortfall() {
|
|
let prose = exec_fault_prose(&ExecFault::GeneralizeNeedsInstruments { available: 1 });
|
|
assert_eq!(prose, "campaign has 1 instrument(s); std::generalize needs at least 2");
|
|
assert!(!prose.contains("GeneralizeNeedsInstruments"), "Debug leak: {prose}");
|
|
}
|
|
|
|
#[test]
|
|
/// #207 (fieldtest 0108 F8): generalize's refusal names the REAL rule — the
|
|
/// rankable R-expectancy family that generalizes — and points at the roster
|
|
/// verb, instead of the old "pip metrics do not" mislabel (which was wrong
|
|
/// for R-denominated-but-unranked names like max_r_drawdown). The four
|
|
/// accepted names are enumerated in the prose as a fail-safe hint; the
|
|
/// durable source is the `(see: …)` roster pointer.
|
|
fn generalize_non_r_metric_prose_names_the_metric() {
|
|
let prose = exec_fault_prose(&ExecFault::GeneralizeNonRMetric {
|
|
metric: "total_pips".into(),
|
|
});
|
|
assert_eq!(
|
|
prose,
|
|
"std::generalize metric \"total_pips\" is not in the rankable R-expectancy \
|
|
family (expectancy_r, net_expectancy_r, sqn, sqn_normalized generalize; \
|
|
see: aura process introspect --metrics)"
|
|
);
|
|
assert!(!prose.contains("GeneralizeNonRMetric"), "Debug leak: {prose}");
|
|
}
|
|
|
|
#[test]
|
|
/// the zero-bootstrap-param refusal is path-addressed (stage index)
|
|
/// and names the offending field.
|
|
fn zero_bootstrap_param_prose_names_stage_and_field() {
|
|
let prose = exec_fault_prose(&ExecFault::ZeroBootstrapParam {
|
|
stage: 3,
|
|
field: "block_len",
|
|
});
|
|
assert_eq!(prose, "process stage 3: monte_carlo block_len must be > 0");
|
|
assert!(!prose.contains("ZeroBootstrapParam"), "Debug leak: {prose}");
|
|
}
|
|
|
|
#[test]
|
|
/// #272: the summary label is a CHECKED mirror of the persisted serde form
|
|
/// — an aggregate over `campaign_runs.jsonl` counts by the serialized
|
|
/// `CellFaultKind` string, so the human-facing label must be exactly that
|
|
/// string (no second source of truth that could silently drift).
|
|
fn cell_fault_kind_label_matches_the_serialized_kind() {
|
|
use aura_registry::CellFaultKind as K;
|
|
for kind in [K::NoData, K::Bind, K::Run, K::Panic, K::Window] {
|
|
let serde_form = serde_json::to_string(&kind).expect("serialize kind");
|
|
let serde_form = serde_form.trim_matches('"');
|
|
assert_eq!(
|
|
cell_fault_kind_label(kind),
|
|
serde_form,
|
|
"the summary label must equal the persisted serde string"
|
|
);
|
|
}
|
|
}
|
|
}
|