refactor(aura-registry): split FamilyRunRecord family_id into {family, run} + derived id
Behaviour-preserving normalisation of the lineage record. The fused
`family_id: String` ("{name}-{counter}") is split into its two factors —
`family: String` + `run: usize` — and the user-facing handle becomes a derived
`family_id()` method ("{family}-{run}"), never stored or parsed.
Why: the fused field forced the counter logic to generate-and-check candidate
strings (to stay robust to '-' inside a name) rather than read it; with `run` a
plain int, `next_run` is a clean numeric max+1 over the field. The split is more
normalised (one fact per field) and the only property the fused token bought — a
single paste-able CLI handle — is recovered by deriving it. Storage and all
user-visible output stay byte-identical: append_family still returns
"{name}-{run}", Family.id is still "{name}-{run}", and the CLI prints the same
"family_id" lines (the json! key + the returned String, both unchanged).
Internal-only: FamilyRunRecord and group_families (now keyed on the (family,
run) tuple, first-seen order preserved) change; lib.rs re-exports, the
extractors, and all of aura-cli are untouched (everything downstream goes
through Family.id + append_family's returned String). The on-disk
families.jsonl key changes ({"family","run"} vs {"family_id"}) — a gitignored
runtime artifact with no committed fixture, and tests round-trip within a temp
dir, so no migration is owed. Doc reconciliation: the lineage/lib module headers
and the C18 ledger realization note now describe the split shape.
Verification: a workflow ran the refactor in an implementer agent, then three
adversarial lenses (behaviour-preservation, new-logic correctness,
completeness/doc-lag) tried to refute it — the first two found nothing, the
third flagged the four doc-lag sites now fixed here. Self-run gates:
cargo test --workspace green (registry 11, incl. a strengthened round-trip test
pinning the split fields + derived handle), cargo clippy --workspace
--all-targets -D warnings clean, cargo doc clean.
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
//!
|
||||
//! Orchestration **families** (sweep / Monte-Carlo / walk-forward, C12/C21) are
|
||||
//! stored as related records in a sibling family store (`families.jsonl`): each
|
||||
//! member is a `RunReport` stamped with a shared `family_id`, re-derived as a unit
|
||||
//! by [`group_families`]. The flat runs store and its API are untouched.
|
||||
//! member is a `RunReport` stamped with its `family` name + `run` index (the
|
||||
//! `family_id` handle is derived from the pair), re-derived as a unit by
|
||||
//! [`group_families`]. The flat runs store and its API are untouched.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
@@ -317,6 +318,10 @@ mod tests {
|
||||
let fam = &families[0];
|
||||
assert_eq!(fam.id, "f-0");
|
||||
assert_eq!(fam.kind, FamilyKind::MonteCarlo);
|
||||
// the split (family, run) fields are stamped on each member, and the
|
||||
// user-facing handle is derived from them (id is "{family}-{run}").
|
||||
assert_eq!((fam.members[0].family.as_str(), fam.members[0].run), ("f", 0));
|
||||
assert_eq!(fam.members[0].family_id(), "f-0");
|
||||
// members re-derived as a unit, ordinal-ordered, equal to the stamped reports
|
||||
let member_reports: Vec<_> = fam.members.iter().map(|m| m.report.clone()).collect();
|
||||
assert_eq!(member_reports, reports);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
//! Registry lineage for orchestration families (C18/C21): a family — a sweep,
|
||||
//! Monte-Carlo, or walk-forward run — is a set of run records sharing one
|
||||
//! `family_id` (the parent reference). This module adds the persisted lineage
|
||||
//! parent identity — the `(family, run)` pair, from which the user-facing
|
||||
//! `family_id` handle (`"{family}-{run}"`) is derived. This module adds the
|
||||
//! persisted lineage
|
||||
//! unit ([`FamilyRunRecord`]), the family-store accessors on [`Registry`]
|
||||
//! ([`Registry::append_family`] / [`Registry::load_family_members`]), and the
|
||||
//! round-trip that re-derives a [`Family`] from the stored links
|
||||
//! ([`group_families`]). The family store is a sibling JSONL of the flat runs
|
||||
//! store (`families.jsonl`), so the flat `runs.jsonl` path and API are untouched.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
@@ -25,18 +27,30 @@ pub enum FamilyKind {
|
||||
}
|
||||
|
||||
/// One persisted family member: a run record stamped with its lineage link.
|
||||
/// `family_id` is the shared parent reference; `ordinal` is the member's position
|
||||
/// in the family's canonical order (odometer / seed-input / roll).
|
||||
/// The parent reference is split into its two factors — `family` (the parent
|
||||
/// name) and `run` (the per-name run index) — from which the user-facing
|
||||
/// `family_id` handle (`"{family}-{run}"`) is derived rather than stored.
|
||||
/// `ordinal` is the member's position in the family's canonical order
|
||||
/// (odometer / seed-input / roll).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FamilyRunRecord {
|
||||
pub family_id: String,
|
||||
pub family: String,
|
||||
pub run: usize,
|
||||
pub kind: FamilyKind,
|
||||
pub ordinal: usize,
|
||||
pub report: RunReport,
|
||||
}
|
||||
|
||||
/// A re-derived family: the members sharing one `family_id`, ordinal-sorted — the
|
||||
/// round-trip result of [`group_families`].
|
||||
impl FamilyRunRecord {
|
||||
/// The user-facing family handle — `"{family}-{run}"`, derived, never stored or parsed.
|
||||
pub fn family_id(&self) -> String {
|
||||
format!("{}-{}", self.family, self.run)
|
||||
}
|
||||
}
|
||||
|
||||
/// A re-derived family: the members sharing one `(family, run)` key (and thus one
|
||||
/// derived `family_id`), ordinal-sorted — the round-trip result of
|
||||
/// [`group_families`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Family {
|
||||
pub id: String,
|
||||
@@ -45,22 +59,20 @@ pub struct Family {
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
/// Assign a fresh `family_id = "{name}-{counter}"` (counter = the smallest `k`
|
||||
/// for which `"{name}-{k}"` is absent in the family store — a per-name run
|
||||
/// index), stamp `reports` as ordinal-ordered members, and append them as N
|
||||
/// JSONL lines to the family store (a sibling of the flat runs store,
|
||||
/// `families.jsonl`, leaving the flat path untouched). Returns the assigned id.
|
||||
/// Reads the store once to pick the counter (a read-before-write; single-process
|
||||
/// CLI invocations do not race).
|
||||
/// Assign a fresh per-name `run` index (one past the highest `run` already
|
||||
/// stored for `name`, or `0` if unseen), stamp `reports` as ordinal-ordered
|
||||
/// members carrying `name` + that `run`, and append them as N JSONL lines to
|
||||
/// the family store (a sibling of the flat runs store, `families.jsonl`,
|
||||
/// leaving the flat path untouched). Returns the derived id `"{name}-{run}"`.
|
||||
/// Reads the store once to pick the run index (a read-before-write;
|
||||
/// single-process CLI invocations do not race).
|
||||
pub fn append_family(
|
||||
&self,
|
||||
name: &str,
|
||||
kind: FamilyKind,
|
||||
reports: &[RunReport],
|
||||
) -> Result<String, RegistryError> {
|
||||
let existing: HashSet<String> =
|
||||
self.load_family_members()?.into_iter().map(|r| r.family_id).collect();
|
||||
let family_id = next_family_id(name, &existing);
|
||||
let run = next_run(name, &self.load_family_members()?);
|
||||
|
||||
let path = self.path.with_file_name("families.jsonl");
|
||||
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
|
||||
@@ -69,16 +81,16 @@ impl Registry {
|
||||
let mut file = fs::OpenOptions::new().create(true).append(true).open(&path)?;
|
||||
for (ordinal, report) in reports.iter().enumerate() {
|
||||
let record = FamilyRunRecord {
|
||||
family_id: family_id.clone(),
|
||||
family: name.to_string(),
|
||||
run,
|
||||
kind,
|
||||
ordinal,
|
||||
report: report.clone(),
|
||||
};
|
||||
// a finite RunReport wrapped in a FamilyRunRecord serializes infallibly.
|
||||
let line = serde_json::to_string(&record).expect("a finite FamilyRunRecord serializes");
|
||||
writeln!(file, "{line}")?;
|
||||
}
|
||||
Ok(family_id)
|
||||
Ok(format!("{name}-{run}"))
|
||||
}
|
||||
|
||||
/// Parse every stored family member, in file order. A missing file is an empty
|
||||
@@ -104,32 +116,36 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
|
||||
/// The next free `"{name}-{counter}"` id: the smallest `k >= 0` for which
|
||||
/// `"{name}-{k}"` is not in `existing`. Robust to `-` inside `name` — it tests
|
||||
/// membership of generated candidates, never parses the stored strings. Module-
|
||||
/// private: the id form is an `append_family` implementation detail.
|
||||
fn next_family_id(name: &str, existing: &HashSet<String>) -> String {
|
||||
(0usize..)
|
||||
.map(|k| format!("{name}-{k}"))
|
||||
.find(|id| !existing.contains(id))
|
||||
.expect("the usize counter space is effectively unbounded")
|
||||
/// The next per-name run index: one past the highest `run` already stored for
|
||||
/// `name`, or `0` if the name is unseen. A clean numeric max over the `run`
|
||||
/// field — no string parsing (the old fused-id form had to generate-and-check to
|
||||
/// stay robust to `-` inside a name; the split removes that concern).
|
||||
fn next_run(name: &str, records: &[FamilyRunRecord]) -> usize {
|
||||
records
|
||||
.iter()
|
||||
.filter(|r| r.family == name)
|
||||
.map(|r| r.run + 1)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Group member records into families by `family_id` (first-seen family order;
|
||||
/// each family's members ordinal-sorted). The round-trip: `append_family(...)`
|
||||
/// then `group_families(load_family_members())` recovers the families.
|
||||
/// Group member records into families by their `(family, run)` key (first-seen
|
||||
/// family order; each family's members ordinal-sorted), deriving the family's
|
||||
/// `id` once at construction. The round-trip: `append_family(...)` then
|
||||
/// `group_families(load_family_members())` recovers the families.
|
||||
pub fn group_families(records: Vec<FamilyRunRecord>) -> Vec<Family> {
|
||||
let mut order: Vec<String> = Vec::new();
|
||||
let mut by_id: HashMap<String, Family> = HashMap::new();
|
||||
let mut order: Vec<(String, usize)> = Vec::new();
|
||||
let mut by_key: HashMap<(String, usize), Family> = HashMap::new();
|
||||
for rec in records {
|
||||
let fam = by_id.entry(rec.family_id.clone()).or_insert_with(|| {
|
||||
order.push(rec.family_id.clone());
|
||||
Family { id: rec.family_id.clone(), kind: rec.kind, members: Vec::new() }
|
||||
let key = (rec.family.clone(), rec.run);
|
||||
let fam = by_key.entry(key.clone()).or_insert_with(|| {
|
||||
order.push(key.clone());
|
||||
Family { id: rec.family_id(), kind: rec.kind, members: Vec::new() }
|
||||
});
|
||||
fam.members.push(rec);
|
||||
}
|
||||
let mut families: Vec<Family> =
|
||||
order.into_iter().map(|id| by_id.remove(&id).expect("id was inserted")).collect();
|
||||
order.into_iter().map(|k| by_key.remove(&k).expect("key was inserted")).collect();
|
||||
for fam in &mut families {
|
||||
fam.members.sort_by_key(|m| m.ordinal);
|
||||
}
|
||||
|
||||
@@ -664,12 +664,12 @@ the reverse.
|
||||
**Realization (cycle 0045 — lineage as related records, #70).** The *lineage*
|
||||
depth is now realized as a **family store**: a sweep / Monte-Carlo / walk-forward
|
||||
run (the C12 axes) is persisted as a *set of related records* — each a
|
||||
`FamilyRunRecord` (a `RunReport` stamped with a shared `family_id` + `kind` +
|
||||
`FamilyRunRecord` (a `RunReport` stamped with its `family` + `run` + `kind` +
|
||||
`ordinal`) — in a sibling JSONL (`families.jsonl`), leaving the flat `runs.jsonl`
|
||||
path and its `append`/`load`/`rank_by`/`optimize` API byte-for-byte unchanged.
|
||||
`family_id = "{name}-{counter}"` (an author-supplied name plus a per-name
|
||||
generate-and-check run index — not a content hash; re-running the same family
|
||||
mints a fresh id). `group_families` is the round-trip that re-derives a family
|
||||
the user-facing `family_id = "{family}-{run}"` handle is **derived** from the
|
||||
stored `family` name plus a per-name `run` index (assigned as a numeric max+1 —
|
||||
not a content hash; re-running the same family mints a fresh id). `group_families` is the round-trip that re-derives a family
|
||||
from the stored links (re-listable / rankable as a unit — C21). The manifest *is*
|
||||
the re-derivation recipe (#71): no input-stream blob / path / payload enters a
|
||||
record, and a member's window is **producer-supplied** via
|
||||
|
||||
Reference in New Issue
Block a user