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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user