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:
@@ -30,17 +30,39 @@ pub struct ReproduceReport {
|
|||||||
/// load failure) — the single place `reproduce_family` and `reproduce_family_in`
|
/// 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
|
/// resolve a family, so the two exit-1 error phrasings can't drift out of sync
|
||||||
/// between the call sites.
|
/// 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> {
|
pub fn load_family(reg: &Registry, id: &str) -> Result<Family, RunnerError> {
|
||||||
let members = reg.load_family_members().map_err(|e| RunnerError {
|
let members = reg.load_family_members().map_err(|e| RunnerError {
|
||||||
exit_code: 1,
|
exit_code: 1,
|
||||||
message: format!("{e}"),
|
message: format!("{e}"),
|
||||||
})?;
|
})?;
|
||||||
group_families(members)
|
let families = group_families(members);
|
||||||
.into_iter()
|
if let Some(family) = families.iter().find(|f| f.id == id) {
|
||||||
.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
|
// 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).
|
// 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
|
/// 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);
|
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(®, "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(®, "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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
(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
|
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
|
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
|
dedup). `aura graph introspect --content-id` exposes the same id for an op-script, via the
|
||||||
|
|||||||
Reference in New Issue
Block a user