feat(cli): chart resolves a --trace family by its chosen campaign name (#238)

The NotFound arm of emit_chart now queries the recorded campaign runs:
records with a persisted trace_name whose stored campaign document
carries name == ARG. A unique match re-dispatches the existing charting
path onto the resolved handle; several matches refuse listing the
candidate handles in append order (refuse-don't-guess — re-running a
named invocation legitimately yields {campaign8}-0, -1, ...); zero
matches keep the not-found message verbatim. The exact trace-store
handle always wins; the name never becomes a store key (C18 untouched).
Guide §3 and the glossary tap entry document the name alternative.

closes #238
This commit is contained in:
2026-07-11 18:04:09 +02:00
parent c70b4cc2d2
commit 18f8e72946
4 changed files with 185 additions and 10 deletions
+82 -8
View File
@@ -401,7 +401,19 @@ fn filter_to_tap(data: ChartData, tap: &str) -> Result<ChartData, String> {
/// (stderr + exit 1), never a panic.
fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode, env: &project::Env) {
let store = env.trace_store();
match store.name_kind(name) {
render_chart_by_kind(name, store.name_kind(name), tap, mode, env, &store);
}
/// Render (or refuse) a chart for a name whose [`NameKind`] is already known —
/// the shared tail `emit_chart` calls directly for the original name, and
/// [`resolve_campaign_name`]'s `NotFound` arm re-enters with the resolved
/// trace-store handle's own kind (never the un-resolved name), so the
/// handle-charted path (Run/Family) is exercised exactly once either way.
fn render_chart_by_kind(
name: &str, kind: NameKind, tap: Option<&str>, mode: ChartMode, env: &project::Env,
store: &aura_registry::TraceStore,
) {
match kind {
NameKind::Run => {
let traces = match store.read(name) {
Ok(t) => t,
@@ -441,15 +453,77 @@ fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode, env: &project::Env
let data = decimate(data, CHART_DECIMATE_BUCKETS);
print!("{}", render::render_chart_html(&data, mode));
}
NameKind::NotFound => {
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)"
);
std::process::exit(1);
NameKind::NotFound => match resolve_campaign_name(name, env) {
NameResolution::Unique(handle) => {
let resolved_kind = store.name_kind(&handle);
render_chart_by_kind(&handle, resolved_kind, tap, mode, env, store);
}
NameResolution::Ambiguous(handles) => {
eprintln!(
"aura: the campaign name '{name}' names {} recorded runs ({}) — \
chart one of these handles directly",
handles.len(),
handles.join(", "),
);
std::process::exit(1);
}
NameResolution::None => {
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)"
);
std::process::exit(1);
}
},
}
}
/// The outcome of resolving a `chart <NAME>` argument against the recorded
/// campaign runs (#238): `NAME` is not a trace-store handle, but it may be the
/// `--trace <NAME>` the user chose when the family was produced, which lands
/// only in the campaign document's `name` field (`put_campaign`), never in the
/// trace-store layout itself.
enum NameResolution {
/// Exactly one recorded campaign run's stored document carries `name ==
/// NAME` and a persisted `trace_name` — the trace-store handle to chart.
Unique(String),
/// More than one recorded run's document carries `name == NAME` — refuse
/// rather than silently pick one; the candidate handles, in the campaign
/// store's file (append) order, so the refusal is deterministic.
Ambiguous(Vec<String>),
/// No recorded run's document carries `name == NAME` — NAME is genuinely
/// unknown, not a chosen campaign name either.
None,
}
/// Resolve a `chart` argument against `env.registry()`'s campaign-run records:
/// keep every record whose `trace_name` is `Some` (a family was actually
/// persisted for it) AND whose stored campaign document (`get_campaign`)
/// parses with `name == name`. Malformed/missing campaign documents are
/// skipped rather than propagated — a resolution failure downgrades to "not a
/// campaign name", not a hard error, since the trace-store NotFound message is
/// still an honest fallback.
fn resolve_campaign_name(name: &str, env: &project::Env) -> NameResolution {
let registry = env.registry();
let records = match registry.load_campaign_runs() {
Ok(r) => r,
Err(_) => return NameResolution::None,
};
let mut candidates = Vec::new();
for record in &records {
let Some(trace_name) = &record.trace_name else { continue };
let Ok(Some(doc_json)) = registry.get_campaign(&record.campaign) else { continue };
let Ok(doc) = aura_research::parse_campaign(&doc_json) else { continue };
if doc.name == name {
candidates.push(trace_name.clone());
}
}
match candidates.len() {
0 => NameResolution::None,
1 => NameResolution::Unique(candidates.remove(0)),
_ => NameResolution::Ambiguous(candidates),
}
}
/// What `--real` parsing yields: the synthetic default, or a real symbol + an