spec: 0094 content-addressed reproduction (boss-signed)

Restore C18 reproducibility for generated (blueprint-sweep) runs. Since cycle
0093 a sweep member's topology is a generated data value, so commit alone no
longer identifies the graph and the manifest is not a complete re-derivation
recipe (#158).

Design: content-address the topology. The canonical blueprint JSON is stored
once, keyed by the topology_hash the manifest already carries (cycle 0092),
under runs/blueprints/<hash>.json (a dumb bytes-by-key store in aura-registry,
no sha2 dep — the reproduce path's bit-identical compare is the integrity
check). `aura reproduce <family-id>` loads each member's blueprint by hash,
re-bootstraps with the member's params/window/seed, re-runs, and asserts the
metrics are bit-identical (C1). acc-1 (two paths -> same content id) via a
minimal `graph introspect --content-id`; acc-3 (Tier-1-stable id) composes the
#156 omit-defaults pin.

Derived forks (logged on #158): content-address (not carry-inline) — keeps the
manifest tiny (C18) and dedups one topology across a family (C11/C12); hash
domain = signal-only this cycle (the scaffolding is not yet blueprint-data,
C24), boundary documented, whole-harness id deferred (#156/#159).

Engine untouched (C9); store + hash off the frozen engine (invariant 8).
boss-signed on a grounding-check PASS (9/9 current-behaviour assumptions
ratified by green tests).

refs #158
This commit is contained in:
2026-07-01 01:15:56 +02:00
parent 4751d4b590
commit 4b6bc52794
@@ -0,0 +1,285 @@
# Content-addressed reproduction of a generated run — Design Spec
**Date:** 2026-07-01
**Status:** Draft — awaiting user spec review
**Authors:** orchestrator + Claude
## Goal
Restore the C18 reproducibility guarantee for **generated** runs. Since cycle 0093 a
sweep member's topology is a *generated data value* (a `Composite` loaded from a blueprint
file, gridded over named axes), so `commit` alone no longer identifies the graph and the
member's manifest is no longer a complete re-derivation recipe — a C18 violation (#158).
This cycle closes that gap by **content-addressing the topology**: the canonical blueprint
JSON is stored once, keyed by its `topology_hash` (already on the manifest since cycle
0092), and a reproduce path re-derives a persisted family's runs from the store and proves
each is bit-identical to the stored result (C1). The three #158 acceptance properties are
realized as tests; the user-facing capability is `aura reproduce <family-id>`.
The hash domain is **signal-only** this cycle — the fixed Stage-1-R scaffolding
(`wrap_stage1r`: broker / sinks / RiskExecutor / cost-graph) is not yet blueprint-data
(deliberately out of the round-trippable set, C24) so the content-id covers the signal and
the scaffolding stays commit-identified. The boundary is documented; whole-harness
content-addressing is deferred (#156 / #159 / project-as-crate).
## Architecture
Three layers, each at its correct home:
1. **Content-addressed blueprint store (aura-registry).** The registry owns the on-disk
`runs/` layout (`runs.jsonl`, `families.jsonl` sibling, `runs/traces/<name>/`). It gains
a sibling content-addressed store `runs/blueprints/<hash>.json` and two dumb
bytes-by-key methods: `put_blueprint(hash, canonical_json)` (write-once: identical
content re-writes the identical bytes, a no-op in effect) and
`get_blueprint(hash) -> Option<String>`. The registry does **not** parse, hash, or
validate the bytes — it stores and retrieves a String keyed by a String. No `sha2`
dependency enters the registry: the caller (the CLI) computes the key, and the
reproduce path's bit-identical metric comparison is itself the integrity check (a
corrupted blueprint re-derives different metrics → reported as a divergence).
2. **Hash + canonical bytes + reproduce (aura-cli).** The CLI already owns `topology_hash`
(`sha2` over `blueprint_to_json`, off the frozen engine, invariant 8). On a blueprint
sweep it writes the canonical blueprint once (the bytes whose SHA256 *is* the shared
`topology_hash`); the reproduce path reads a stored family, loads each member's
blueprint by hash, reconstructs the member's sweep point from its recorded params,
re-bootstraps through the cycle-1 `wrap_stage1r` seam, re-runs, and compares.
3. **Cross-path content-id (aura graph introspect).** A minimal read-only addition
`aura graph introspect --content-id` prints the SHA256 of the canonical blueprint an
op-script (#157) builds — making "do these two authoring paths describe the same
topology?" answerable from the CLI, and giving acceptance (1) its surface.
C9 holds throughout: the engine is untouched (`blueprint_to_json` / `blueprint_from_json`
already exist in `aura-engine::blueprint_serde`); the new store and reproduce logic live in
the research-side crates that already depend on the engine.
## Concrete code shapes
### Worked user-facing capability — `aura reproduce <family-id>`
The research user persisted a sweep family and wants to verify it still reproduces from its
manifest (the C18 "re-derives full results on demand" promise):
```console
$ aura sweep stage1_signal_open.json --axis stage1_signal.slow.length=4,6,8,10 --name smacross
smacross-1 stage1_signal.slow.length=4 expectancy_r=1.2710 n_trades=3
smacross-1 stage1_signal.slow.length=6 expectancy_r=... n_trades=...
...
# topology stored once at runs/blueprints/<hash>.json; 4 member manifests share that hash.
$ aura reproduce smacross-1
smacross-1 member stage1_signal.slow.length=4 reproduced: bit-identical
smacross-1 member stage1_signal.slow.length=6 reproduced: bit-identical
smacross-1 member stage1_signal.slow.length=8 reproduced: bit-identical
smacross-1 member stage1_signal.slow.length=10 reproduced: bit-identical
reproduced 4/4 members bit-identically from runs/blueprints/<hash>.json
$ echo $?
0
# a divergence (or a missing stored blueprint) prints the member + exits non-zero.
```
This is the empirical evidence for the feature-acceptance criterion: the capability a
research user reaches for, built on the manifest + the content-addressed store + the commit
as the complete re-derivation recipe.
### Store write at the sweep persist seam (aura-cli `run_blueprint_sweep`, ~main.rs:3063)
Before (today — the family is persisted, the topology is not stored):
```rust
fn run_blueprint_sweep(doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, persist: bool, data: DataSource) {
let _ = persist;
let family = blueprint_sweep_family(doc, axes, &data).unwrap_or_else(|e| { eprintln!("aura: {e:?}"); std::process::exit(2); });
let id = match default_registry().append_family(name, FamilyKind::Sweep, &sweep_member_reports(&family)) { /**/ };
for pt in &family.points { println!("{}", family_member_line(&id, &pt.report)); }
}
```
After (the shared topology is content-addressed once, beside the family record):
```rust
fn run_blueprint_sweep(doc: &str, axes: &[(String, Vec<Scalar>)], name: &str, persist: bool, data: DataSource) {
let _ = persist;
let family = blueprint_sweep_family(doc, axes, &data).unwrap_or_else(|e| { eprintln!("aura: {e:?}"); std::process::exit(2); });
let reg = default_registry();
// store the canonical blueprint once, keyed by the family's shared topology_hash —
// exactly the bytes whose SHA256 the members already carry (#164 byte-canonical form).
let topo = family.points[0].report.manifest.topology_hash.clone()
.expect("a blueprint sweep stamps every member's topology_hash");
let canonical = blueprint_to_json(&blueprint_from_json(doc, &|t| std_vocabulary(t))
.expect("doc parse-validated at the dispatch boundary"))
.expect("a loaded blueprint re-serializes");
reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(2); });
let id = match reg.append_family(name, FamilyKind::Sweep, &sweep_member_reports(&family)) { /* … unchanged … */ };
for pt in &family.points { println!("{}", family_member_line(&id, &pt.report)); }
}
```
### Content-addressed store (aura-registry, new methods on `Registry`)
```rust
impl Registry {
/// The content-addressed blueprint store dir, a sibling of the runs store:
/// `<runs.jsonl>.with_file_name("blueprints")`.
fn blueprints_dir(&self) -> PathBuf { self.path.with_file_name("blueprints") }
/// Write-once content-addressed put: `runs/blueprints/<hash>.json` = canonical_json.
/// Idempotent — the same hash always addresses identical bytes, so an overwrite is a
/// no-op in effect. The registry does not verify `sha256(bytes) == hash`; the caller
/// owns the hash, and reproduction's bit-identical compare is the integrity check.
pub fn put_blueprint(&self, hash: &str, canonical_json: &str) -> Result<(), RegistryError> {
let dir = self.blueprints_dir();
std::fs::create_dir_all(&dir).map_err(/* RegistryError::Io */)?;
std::fs::write(dir.join(format!("{hash}.json")), canonical_json).map_err(/**/)?;
Ok(())
}
/// Read a stored blueprint by content id; `Ok(None)` if absent (treat-as-empty
/// discipline, like `load` on a missing store).
pub fn get_blueprint(&self, hash: &str) -> Result<Option<String>, RegistryError> {
match std::fs::read_to_string(self.blueprints_dir().join(format!("{hash}.json"))) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(/* RegistryError::Io */),
}
}
}
```
### Reproduce path (aura-cli, new)
```rust
/// Re-derive every member of a persisted sweep family from the content-addressed store and
/// compare to the stored result. Determinism (C1) makes a match bit-identical.
fn reproduce_family(id: &str, data: &DataSource) -> ReproduceReport {
let reg = default_registry();
let family = group_families(reg.load_family_members().expect("load"))
.into_iter().find(|f| f.id == id)
.unwrap_or_else(|| { eprintln!("aura: no such family '{id}'"); std::process::exit(2); });
let mut outcomes = Vec::new();
for member in &family.members {
let hash = member.manifest.topology_hash.as_deref()
.expect("a generated member carries a topology_hash");
let doc = reg.get_blueprint(hash).expect("store read")
.unwrap_or_else(|| { eprintln!("aura: blueprint {hash} missing from store"); std::process::exit(2); });
// reconstruct the sweep point: param_space order of the reloaded signal, values
// read from the member's recorded named params (the inverse of zip_params).
let signal = blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("stored blueprint parses");
let point = point_from_params(&signal, &member.manifest.params); // Vec<Cell> in param_space order
let rerun = run_signal_point(signal, &point, data, member.manifest.seed); // wrap_stage1r + bootstrap + run, reduce-mode
outcomes.push((member_label(member), rerun.metrics == member.metrics));
}
ReproduceReport { id: id.to_string(), outcomes }
}
```
`point_from_params` walks the reloaded signal's `param_space()` and, for each knob, reads
the value the member's manifest recorded under that knob's name, yielding the `Vec<Cell>`
`bootstrap_with_cells` consumes — the inverse of the `zip_params(&space, point)` the sweep
used to record them.
### Cross-path content-id surface (aura graph introspect, ~main.rs introspect arm)
```rust
// `aura graph introspect --content-id` over an op-script on stdin: build the blueprint
// (the #157 replay), print the SHA256 of its canonical form — the same hash `aura run`
// stamps as topology_hash. Reuses topology_hash(&composite); no new hashing.
["graph", "introspect", "--content-id"] => {
let blueprint = build_from_op_script(read_stdin())?; // existing #157 replay → Composite
println!("{}", topology_hash(&blueprint));
}
```
## Components
- **`Registry::put_blueprint` / `get_blueprint` / `blueprints_dir`** (aura-registry) — the
content-addressed bytes store under `runs/blueprints/`. Dumb, no engine-blueprint parse,
no `sha2`.
- **Store write in `run_blueprint_sweep`** (aura-cli) — one canonical-blueprint write per
sweep, keyed by the family's shared `topology_hash`.
- **`reproduce_family` + `point_from_params` + the `aura reproduce <id>` dispatch arm**
(aura-cli) — load family → load blueprint by hash → reconstruct point → re-run → compare.
- **`graph introspect --content-id`** (aura-cli) — the op-script content-id surface for
acceptance (1).
## Data flow
Persist: `aura sweep <bp.json>``blueprint_sweep_family` (members stamped with the shared
`topology_hash`) → `run_blueprint_sweep` writes `runs/blueprints/<hash>.json` once →
`append_family` writes the member records to `families.jsonl`.
Reproduce: `aura reproduce <id>``group_families` finds the family → per member: read
`topology_hash``get_blueprint(hash)``blueprint_from_json``point_from_params`
`wrap_stage1r` + `bootstrap_with_cells(point)` + `run` (reduce-mode, exactly the sweep
member path) → `summarize_r` → compare re-run `metrics` to the stored member `metrics`.
The manifest carries `params`, `window`, `seed`, `topology_hash`; the data source is
synthetic (deterministic) for this cycle — recorded-dataset reproduction rides the
DataServer seam (#124), out of scope.
## Error handling
- **Missing stored blueprint** (`get_blueprint``None`): the family was recorded before
the store existed, or the store dir was deleted — print `blueprint <hash> missing from
store` and exit non-zero (refuse-don't-guess, C10/C18). Never re-run against a guessed
topology.
- **Unknown family id**: `aura reproduce <id>` with no matching family prints `no such
family '<id>'` and exits non-zero (a deliberate non-zero, distinct from the treat-as-empty
`runs family <id>` *lookup* — reproduce is an action that needs a real target).
- **Divergence** (`rerun.metrics != stored`): report the offending member and exit
non-zero. This is the integrity check; on synthetic data a divergence is a real bug, not
an expected outcome.
- **Store write failure** (I/O): propagate as `RegistryError`, surfaced as `aura: <e>` +
exit non-zero at the persist seam.
## Testing strategy
- **acc 2 (the heart) — reproduce is bit-identical, including open-at-end.** A test
persists a blueprint sweep whose grid includes the **open-at-end** param (fast=2/slow=4 is
open-at-end, `n_open_at_end=1`), then `reproduce_family` re-derives every member and
asserts `rerun.metrics == stored.metrics` for each. This exercises the open-at-end path
end-to-end through the real reproduce loop — store round-trip + C1 determinism: a stored
member re-derives bit-identically (a reduce-vs-reduce re-run, the same path the sweep
recorded). The reduce(`GatedRecorder`) vs full(`Recorder`) R-equivalence the cycle-0093
implementer flagged is a *separate* property, already pinned by the existing keystone
`blueprint_sweep_member_equals_single_run_and_shares_topology_hash` (a reduce member ==
a full single run at fast=2/slow=4, an open-at-end param) plus two unit tests — see #166;
this cycle adds no new claim there, it reuses the open-at-end param for thorough reproduce
coverage.
- **acc 2 — round-trip store.** A unit test: `put_blueprint(h, bytes)` then
`get_blueprint(h)` returns `Some(bytes)` byte-identically; `get_blueprint(absent)` is
`Ok(None)`.
- **acc 1 — two command paths, same content id.** A test builds the SMA-cross signal via
the op-script path (`graph introspect --content-id`, or hashing `graph build` output) and
via `stage1_signal`/`topology_hash`, and asserts the two content ids are equal for the
same topology and differ for a different one (e.g. fast=2 vs fast=3).
- **acc 3 — Tier-1 stability of the content id.** A test pins that `topology_hash` is
unchanged when a Tier-1 optional field the blueprint does not use is added to the format
— composing with `unknown_optional_field_is_tolerated_byte_identically` (#156): the
canonical omit-defaults form omits an absent optional, so the hashed bytes (and the id)
are byte-identical.
- **E2E (CLI):** `aura sweep <bp> --name X` then `aura reproduce X` over the built binary —
asserts the `reproduced N/N members bit-identically` line and exit 0; a missing-store and
an unknown-id path each exit non-zero with the named cause on stderr.
## Acceptance criteria
1. **Two command paths that build the same topology produce the same content id.** The
op-script path and `aura run`'s `stage1_signal` agree on `topology_hash` for the same
signal, and disagree for a different one. (`graph introspect --content-id` exposes it.)
2. **A serialized manifest re-derives the same FlatGraph and a bit-identical run (C1).**
`aura reproduce <family-id>` loads each member's blueprint from the content-addressed
store by its manifest's `topology_hash`, re-bootstraps with the member's params / window
/ seed, re-runs, and the re-run metrics are bit-identical to the stored metrics —
including the open-at-end param.
3. **A Tier-1 format addition the blueprint does not use leaves its content id unchanged.**
An absent optional does not enter the canonical form, so `topology_hash` is byte-stable
across the addition.
4. **Boundary documented.** The content-id covers the **signal** only; the Stage-1-R
scaffolding stays commit-identified this cycle (recorded in the ledger), with
whole-harness content-addressing deferred.
5. **Invariants preserved.** Engine untouched (C9); the store + hash are off the frozen
engine (invariant 8); reproduction is deterministic on synthetic data (C1);
refuse-don't-guess on a missing stored blueprint (C10/C18).