fix(aura-registry): Registry::load tolerates legacy pre-0047 bare-float params
The cycle-0047 typed-Scalar params migration (86746e3) changed each RunManifest param value on the C18 flat runs store from a bare JSON number (2.0) to an externally-tagged Scalar ({"F64":2.0}), but did not migrate the persisted runs.jsonl. Registry::load could no longer deserialize any pre-0047 line, so `aura runs list` / `runs rank` errored out on a researcher's existing run history (bug F5, surfaced by the "The World, part II" milestone fieldtest). Fix: a localized read-side back-compat mirror (src/compat.rs). load() now reads each line via RunReportRead, whose ScalarRead accepts BOTH the tagged form AND a bare JSON number (coerced to Scalar::f64), then converts into the canonical RunReport. The forward write path (append / RunReport::to_json) is untouched — it still emits the tagged form. This is a one-directional read widening, not a format change. The secondary clause of #83 (parse error exits 0) was triaged as not-a-bug: runs list/rank already exit 2 via load_runs_or_exit (main.rs:742); the "(exit 0)" in the fieldtest transcript was a pipe measurement artifact ($? read head's exit code, not the binary's). RED test (committed20511d5) now green; full workspace suite + clippy green. closes #83
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
//! Back-compatible read shapes for the C18 flat runs store.
|
||||
//!
|
||||
//! [`Registry::load`](crate::Registry::load) is the durable read-path and must
|
||||
//! stay tolerant of the param-value wire shape across the cycle-0047 typed-`Scalar`
|
||||
//! migration (`86746e3`). The current format tags each param value as an
|
||||
//! externally-tagged [`Scalar`] (`{"F64":2.0}` / `{"I64":10}`); a `runs.jsonl`
|
||||
//! line written *before* that migration carries the value as a **bare JSON
|
||||
//! number** (`2.0`). These mirror types deserialize either shape and convert into
|
||||
//! the canonical [`RunReport`] — the read side only. The forward write path
|
||||
//! (`Registry::append`, `RunReport::to_json`) is untouched and still emits the
|
||||
//! tagged form, so this is a one-directional widening, not a format change.
|
||||
|
||||
use aura_engine::{RunManifest, RunMetrics, RunReport, Scalar, Timestamp};
|
||||
use serde::de::{self, Deserializer};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// A `RunReport` whose param values read back from either the tagged or the
|
||||
/// legacy bare-float shape. Field-for-field structural mirror of `RunReport`;
|
||||
/// only [`ScalarRead`] differs from the engine type.
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RunReportRead {
|
||||
manifest: RunManifestRead,
|
||||
metrics: RunMetrics,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RunManifestRead {
|
||||
commit: String,
|
||||
params: Vec<(String, ScalarRead)>,
|
||||
window: (Timestamp, Timestamp),
|
||||
seed: u64,
|
||||
broker: String,
|
||||
}
|
||||
|
||||
/// A param value that accepts BOTH the current tagged [`Scalar`] form
|
||||
/// (`{"F64":2.0}` / `{"I64":10}`) and the legacy pre-0047 bare JSON number
|
||||
/// (`2.0`, coerced to `Scalar::F64` per the documented `f64` coercion).
|
||||
struct ScalarRead(Scalar);
|
||||
|
||||
impl<'de> Deserialize<'de> for ScalarRead {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
// serde_json's untagged `Value` lets us peek the shape without committing
|
||||
// the parse: an object/string deserializes as the tagged `Scalar`; a bare
|
||||
// number is the legacy form, coerced to `Scalar::f64`.
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
match value {
|
||||
serde_json::Value::Number(n) => {
|
||||
let f = n.as_f64().ok_or_else(|| {
|
||||
de::Error::custom(format!("legacy bare-float param value out of f64 range: {n}"))
|
||||
})?;
|
||||
Ok(ScalarRead(Scalar::f64(f)))
|
||||
}
|
||||
other => Scalar::deserialize(other)
|
||||
.map(ScalarRead)
|
||||
.map_err(de::Error::custom),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RunReportRead> for RunReport {
|
||||
fn from(r: RunReportRead) -> Self {
|
||||
let RunManifestRead { commit, params, window, seed, broker } = r.manifest;
|
||||
RunReport {
|
||||
manifest: RunManifest {
|
||||
commit,
|
||||
params: params.into_iter().map(|(name, v)| (name, v.0)).collect(),
|
||||
window,
|
||||
seed,
|
||||
broker,
|
||||
},
|
||||
metrics: r.metrics,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use aura_engine::{RunReport, SweepFamily, SweepPoint};
|
||||
|
||||
mod compat;
|
||||
|
||||
mod lineage;
|
||||
pub use lineage::{
|
||||
group_families, mc_member_reports, sweep_member_reports, walkforward_member_reports, Family,
|
||||
@@ -64,8 +66,13 @@ impl Registry {
|
||||
if raw.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let report =
|
||||
serde_json::from_str(raw).map_err(|source| RegistryError::Parse { line: i + 1, source })?;
|
||||
// Read via the back-compat mirror: tolerant of both the current
|
||||
// tagged-`Scalar` param shape (`{"F64":2.0}`) and the legacy pre-0047
|
||||
// bare-float shape (`2.0`). The forward write path (`append`,
|
||||
// `RunReport::to_json`) is unchanged — it still emits the tagged form.
|
||||
let report: RunReport = serde_json::from_str::<compat::RunReportRead>(raw)
|
||||
.map_err(|source| RegistryError::Parse { line: i + 1, source })?
|
||||
.into();
|
||||
reports.push(report);
|
||||
}
|
||||
Ok(reports)
|
||||
|
||||
Reference in New Issue
Block a user