fix: load_family resolves the enumerated family name, refusing ambiguity (#298)

GREEN for 78e68e6's RED. aura_runner::reproduce::load_family resolved
only the derived C18 handle '{family}-{run}' while the registry's
enumeration exposes the bare name factor — the natural
list-then-reproduce library workflow dead-ended (found by the #295
fieldtest; predates the extraction byte-identically). Resolution now:
exact-handle match keeps first precedence; a bare name naming exactly
one stored run resolves as fallback; an ambiguous name (several runs,
or a name/handle collision) refuses without guessing, listing the
candidate handles. Two additional tests pin the ambiguity refusal and
the handle-precedence-on-collision semantics.

Ledger record-reality rider: the C18 #158 paragraph claimed reproduce
refuses an unknown id with exit 2; the code has always exited 1 —
correct per C14's partition (the id names missing recorded state, a
runtime failure, not a usage error). The paragraph now records the
actual behaviour and the new name-fallback semantics.

Verification: cargo test --workspace green (1477 passed, 0 failed);
clippy --workspace --all-targets -D warnings clean.

closes #298
This commit is contained in:
2026-07-21 07:34:41 +02:00
parent 78e68e6002
commit 4ed6455b64
2 changed files with 70 additions and 5 deletions
+64 -4
View File
@@ -30,17 +30,39 @@ pub struct ReproduceReport {
/// load failure) — the single place `reproduce_family` and `reproduce_family_in`
/// resolve a family, so the two exit-1 error phrasings can't drift out of sync
/// between the call sites.
///
/// `id` is tried first against the derived `family_id` handle (`"{family}-{run}"`,
/// exact match, first precedence). If that misses, it falls back to the bare
/// enumeration `family` name (`FamilyRunRecord::family` / `Registry::
/// load_family_members()`'s natural list-then-reproduce workflow, #298): a name
/// that names exactly one stored run resolves to it, but a name with more than
/// one stored run is ambiguous and refused rather than guessed, listing the
/// candidate handles.
pub fn load_family(reg: &Registry, id: &str) -> Result<Family, RunnerError> {
let members = reg.load_family_members().map_err(|e| RunnerError {
exit_code: 1,
message: format!("{e}"),
})?;
group_families(members)
.into_iter()
.find(|f| f.id == id)
let families = group_families(members);
if let Some(family) = families.iter().find(|f| f.id == id) {
return Ok(family.clone());
}
let by_name: Vec<&Family> =
families.iter().filter(|f| f.members.iter().any(|m| m.family == id)).collect();
match by_name.as_slice() {
[only] => Ok((*only).clone()),
// reproduce is an action, not a lookup: an unknown id is a hard error (distinct
// from `runs family <id>`'s treat-as-empty exit 0).
.ok_or_else(|| RunnerError { exit_code: 1, message: format!("no such family '{id}'") })
[] => Err(RunnerError { exit_code: 1, message: format!("no such family '{id}'") }),
many => {
let candidates =
many.iter().map(|f| f.id.clone()).collect::<Vec<_>>().join(", ");
Err(RunnerError {
exit_code: 1,
message: format!("ambiguous family '{id}': candidates {candidates}"),
})
}
}
}
/// Re-derive every member of a persisted sweep family from the content-addressed store
@@ -297,4 +319,42 @@ mod tests {
});
assert_eq!(family.id, canonical);
}
/// A name shared by more than one stored run must refuse rather than guess
/// (#298 fix): `load_family` lists every candidate handle in the refusal
/// message so the caller can pick the intended one, instead of silently
/// resolving to whichever run happens first.
#[test]
fn load_family_refuses_a_name_with_more_than_one_stored_run() {
let reg = temp_registry("ambiguous-name");
reg.append_family("f", FamilyKind::Sweep, &[minimal_report()])
.expect("append family f, run 0");
reg.append_family("f", FamilyKind::Sweep, &[minimal_report()])
.expect("append family f, run 1");
let err = load_family(&reg, "f")
.expect_err("a name naming two stored runs must not resolve silently");
assert!(err.message.contains("ambiguous family 'f'"), "message: {}", err.message);
assert!(err.message.contains("f-0"), "message: {}", err.message);
assert!(err.message.contains("f-1"), "message: {}", err.message);
}
/// The derived handle keeps first precedence over the name fallback (#298
/// fix): when a family named `"f-0"` collides with the canonical handle of
/// another family `"f"`'s run 0, looking up `"f-0"` resolves to the family
/// whose HANDLE is `"f-0"`, not the family whose NAME happens to match.
#[test]
fn load_family_exact_handle_precedes_a_name_collision() {
let reg = temp_registry("handle-name-collision");
reg.append_family("f", FamilyKind::Sweep, &[minimal_report()])
.expect("append family f, run 0 -> handle f-0");
reg.append_family("f-0", FamilyKind::Sweep, &[minimal_report()])
.expect("append family f-0, run 0 -> handle f-0-0, name f-0");
let family = load_family(&reg, "f-0").unwrap_or_else(|e| {
panic!("the exact handle 'f-0' must resolve: {}", e.message)
});
assert_eq!(family.id, "f-0");
assert_eq!(family.members[0].family, "f");
}
}
+6 -1
View File
@@ -1512,7 +1512,12 @@ the integrity check). `aura reproduce <family-id>` re-derives every persisted me
the member's blueprint by its `topology_hash`, reconstruct the sweep point from the
recorded params, re-run through the **same** `run_blueprint_member` the live sweep uses
(so bit-identity is by construction, C1), and compare metrics — refuse-don't-guess on an
unknown id / missing stored blueprint (exit 2), DIVERGED → exit 1. The manifest + the
unknown id / missing stored blueprint (exit 1 — recorded state is missing, C14's
runtime-failure class; this paragraph over-claimed exit 2 until #298 recorded the code's
actual, C14-consistent behaviour), DIVERGED → exit 1. The id resolves first as the
derived `{family}-{run}` handle; a bare enumeration name naming exactly one stored run
resolves as fallback, an ambiguous name refuses listing the candidate handles (#298
the list-then-reproduce seam). The manifest + the
content-addressed store + the commit are the complete re-derivation recipe (C18). One
blueprint is stored per family (all members share the signal `topology_hash` — C11/C12
dedup). `aura graph introspect --content-id` exposes the same id for an op-script, via the