feat(cli): process|campaign introspect --metrics — the roster split becomes discoverable (0107 F9)
The 17-name metric vocabulary lists with applicability tags derived from aura-campaign's existing pub rosters (rankable | gate | annotation) — no fourth roster site (#190); the mode rides the shared introspect struct and answers for both doc families. The three metric refusals (intrinsic unknown-metric, executor unrankable and gate-not-per-member) now point at the mode, so the exists-but-not-rankable split (profit_factor is emitted per member yet not selectable) is no longer trial-and-error. RED-first (tdd-author handoff): six same-seam pins (roster lines + tags, one-mode guard incl. the extended usage line, both-families parity, three refusal pointers) observed failing, then the mode + hints landed. The worked-examples half of #197 stays open (docwriter territory post-stability, per the issue itself). Gates: workspace 1001/0, clippy -D warnings clean. refs #197
This commit is contained in:
@@ -53,11 +53,13 @@ fn exec_fault_prose(f: &ExecFault) -> String {
|
||||
}
|
||||
ExecFault::UnrankableMetric { stage, metric } => format!(
|
||||
"process stage {stage}: metric \"{metric}\" is not rankable (winner \
|
||||
selection needs one of the registry's rankable metrics)"
|
||||
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)"
|
||||
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 \
|
||||
@@ -448,6 +450,40 @@ mod tests {
|
||||
ParamSpec { name: name.to_string(), kind: ScalarKind::I64 }
|
||||
}
|
||||
|
||||
#[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]
|
||||
/// The only shape treated as a direct store address is a bare 64-char
|
||||
/// lowercase-hex token; anything else (wrong length, uppercase, non-hex)
|
||||
|
||||
@@ -45,6 +45,9 @@ pub struct DocIntrospectCmd {
|
||||
/// Print the content id of a valid document
|
||||
#[arg(long, value_name = "FILE")]
|
||||
pub content_id: Option<PathBuf>,
|
||||
/// List the metric vocabulary with applicability tags
|
||||
#[arg(long)]
|
||||
pub metrics: bool,
|
||||
}
|
||||
|
||||
impl DocIntrospectCmd {
|
||||
@@ -53,6 +56,7 @@ impl DocIntrospectCmd {
|
||||
+ usize::from(self.block.is_some())
|
||||
+ usize::from(self.unwired.is_some())
|
||||
+ usize::from(self.content_id.is_some())
|
||||
+ usize::from(self.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,12 +65,35 @@ impl DocIntrospectCmd {
|
||||
fn guard_one_mode(cmd: &DocIntrospectCmd, family: &str) {
|
||||
if cmd.selected_modes() != 1 {
|
||||
eprintln!(
|
||||
"aura: Usage: aura {family} introspect --vocabulary | --block <ID> | --unwired <FILE> | --content-id <FILE>"
|
||||
"aura: Usage: aura {family} introspect --vocabulary | --block <ID> | --unwired <FILE> | --content-id <FILE> | --metrics"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
/// `--metrics`: the 17-name metric vocabulary, tagged by where a name may be
|
||||
/// used — `rankable` (sweep/walk_forward `select` metrics), `gate`
|
||||
/// (per-member gate predicates), `annotation` (selection annotations,
|
||||
/// neither). The tags derive from aura-campaign's existing pub rosters, so
|
||||
/// this mode adds no fourth roster site (#190); metrics are process-side
|
||||
/// vocabulary, but the mode rides the shared introspect struct and answers
|
||||
/// for both families.
|
||||
fn print_metric_roster() {
|
||||
for name in aura_research::metric_vocabulary() {
|
||||
let rankable = aura_campaign::RANKABLE_METRICS.contains(name);
|
||||
let gate = aura_campaign::PER_MEMBER_METRICS.contains(name);
|
||||
let tag = match (rankable, gate) {
|
||||
(true, true) => "rankable | gate",
|
||||
(false, true) => "gate",
|
||||
// Unreachable today (the rankable roster is a subset of the
|
||||
// per-member roster) — kept total so a roster shift stays honest.
|
||||
(true, false) => "rankable",
|
||||
(false, false) => "annotation",
|
||||
};
|
||||
println!("{name:<24} {tag}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn doc_error_prose(what: &str, e: &DocError) -> String {
|
||||
match e {
|
||||
DocError::NotJson(msg) => format!("{what} is not JSON: {msg}"),
|
||||
@@ -88,7 +115,9 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
|
||||
match f {
|
||||
DocFault::EmptyPipeline => "the pipeline is empty — a process needs at least one stage".into(),
|
||||
DocFault::UnknownMetric { stage, metric } => {
|
||||
format!("stage {stage}: unknown metric \"{metric}\"")
|
||||
format!(
|
||||
"stage {stage}: unknown metric \"{metric}\" (see: aura process introspect --metrics)"
|
||||
)
|
||||
}
|
||||
DocFault::EmptyConjunction { stage } => {
|
||||
format!("stage {stage}: a gate needs at least one predicate")
|
||||
@@ -186,6 +215,10 @@ fn register_process(file: &PathBuf, env: &Env) -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn introspect_process(cmd: &DocIntrospectCmd) -> Result<(), String> {
|
||||
if cmd.metrics {
|
||||
print_metric_roster();
|
||||
return Ok(());
|
||||
}
|
||||
if cmd.vocabulary {
|
||||
for b in process_vocabulary() {
|
||||
println!("{:<18} {}", b.id, b.doc);
|
||||
@@ -365,6 +398,10 @@ fn register_campaign(file: &PathBuf, env: &Env) -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn introspect_campaign(cmd: &DocIntrospectCmd) -> Result<(), String> {
|
||||
if cmd.metrics {
|
||||
print_metric_roster();
|
||||
return Ok(());
|
||||
}
|
||||
if cmd.vocabulary {
|
||||
for b in campaign_vocabulary() {
|
||||
println!("{:<18} {}", b.id, b.doc);
|
||||
@@ -443,6 +480,22 @@ mod tests {
|
||||
assert!(!bare.contains("drop the 'content:' prefix"), "bare id must not carry the hint: {bare}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// #197 (fieldtest 0107 F9): the intrinsic `unknown metric` refusal points
|
||||
/// the author at the verb that enumerates the valid names, instead of
|
||||
/// leaving the 17-name vocabulary discoverable only via the glossary. The
|
||||
/// name itself is still quoted (the existing contract, pinned by the e2e
|
||||
/// `process_validate_refuses_unknown_metric_*` test); only a discovery
|
||||
/// pointer is appended.
|
||||
fn unknown_metric_prose_points_at_the_metrics_introspection() {
|
||||
let prose = doc_fault_prose(&DocFault::UnknownMetric { stage: 0, metric: "netto_r".into() });
|
||||
assert!(prose.contains("unknown metric \"netto_r\""), "name still quoted: {prose}");
|
||||
assert!(
|
||||
prose.contains("aura process introspect --metrics"),
|
||||
"the refusal must point at the enumeration verb: {prose}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_walk_forward_length_prose_is_path_addressed_and_debug_free() {
|
||||
let prose = doc_fault_prose(&DocFault::ZeroWalkForwardLength { stage: 2, field: "step_ms" });
|
||||
|
||||
Reference in New Issue
Block a user