feat(cli): dissolve the real-data blueprint sweep into the campaign path (#210 T4)

aura sweep <bp.json> --real ... --axis ... now runs as thin sugar over
its generated campaign document through the one campaign executor: the
dispatch registers the canonical blueprint (topology_hash IS
content_id_of over the same canonical bytes, so the strategy ref
resolves against the existing single store write), strips the CLI's
wrapped axis names to the raw campaign namespace (#203 bind
convention), converts the probed archive window back through the
ingest seam's own epoch_ns_to_unix_ms (made pub; C3 — the seam owns
the ms/ns convention, never an inline division), and hands off to
verb_sugar::run_sweep_sugar. The inline real-data arm is gone; the
synthetic arm and --list-axes are untouched.

campaign_run gains RunPresentation {Full, MemberLinesOnly}:
run_campaign keeps Full (record line unchanged, project gate
unchanged); the sugar path runs MemberLinesOnly — the generated doc's
emit already limits stdout to family_table member lines, the mode
suppresses only the final record line. The record append is identical
in both modes; no project gate on the by-id path (the dissolved verb
works project-less, exactly as before).

The characterization pin survives the re-cut with exactly the one
sanctioned assertion flip (instrument-absence -> instrument==GER40)
and now also pins the generated artifacts: one process + one campaign
document auto-registered (dedup on a second identical invocation),
raw axis keys in the stored document, the stored window a ms sub-range
of the request, C1 at the member-report level with the registry's
run-suffix increment isolated.

Suite 1055/0, clippy clean, gated real-data paths executed locally.

refs #210
This commit is contained in:
2026-07-04 19:01:27 +02:00
parent fd0b21d070
commit b7aaa0ba59
5 changed files with 284 additions and 35 deletions
+143 -16
View File
@@ -1403,22 +1403,22 @@ fn sweep_real_is_byte_deterministic_across_runs() {
const GER40_SEPT2024_FROM_MS: &str = "1725148800000";
const GER40_SEPT2024_TO_MS: &str = "1727740799999";
/// Property (docs/specs/sweep-dissolution.md, Testing strategy 1 — the
/// characterization pin ahead of the sweep-dissolution re-cut): today's inline
/// `aura sweep <blueprint.json> --real SYM --axis …` path (`run_blueprint_sweep`)
/// Property: the real-data blueprint sweep path (`aura sweep <blueprint.json>
/// --real SYM --axis …`, dispatched as sugar over the one campaign executor)
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
/// `--axis` varies slowest), each line JSON with a `family_id` and a `report`
/// whose `manifest.params` carries every swept param binding — and, the
/// load-bearing pin the dissolution cycle must preserve unchanged, **`report.manifest`
/// carries NO `instrument` key at all** (the inline path leaves `RunManifest::instrument`
/// `None`, and `#[serde(skip_serializing_if = "Option::is_none")]` omits the key
/// entirely from the wire form — contrast the campaign substrate's stamped sibling,
/// `campaign_run_real_e2e_sweep_gate_walkforward` in `research_docs.rs`). Also pins
/// that the run persists exactly one new `FamilyKind::Sweep` family with the grid's
/// member count. Gated on the local GER40 archive (the project's skip-on-no-data
/// convention); skips cleanly when absent.
/// sanctioned additive delta over the synthetic path, **`report.manifest`
/// carries a stamped `instrument` key** (the campaign substrate stamps every
/// member's manifest with the instrument under test, exactly like the sibling
/// `campaign_run_real_e2e_sweep_gate_walkforward` in `research_docs.rs`). Also
/// pins that the run persists exactly one new `FamilyKind::Sweep` family with
/// the grid's member count, and that the underlying generated process/campaign
/// documents and campaign-run record are durably auto-registered. Gated on the
/// local GER40 archive (the project's skip-on-no-data convention); skips
/// cleanly when absent.
#[test]
fn sweep_real_blueprint_member_lines_pin_the_inline_contract() {
fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
@@ -1466,10 +1466,12 @@ fn sweep_real_blueprint_member_lines_pin_the_inline_contract() {
.and_then(|p| p[1]["I64"].as_i64());
assert_eq!(bound, Some(value), "manifest carries the swept binding {name}={value}: {line}");
}
// the load-bearing pin: the inline real-data path never stamps `instrument`.
assert!(
manifest.get("instrument").is_none(),
"the inline blueprint sweep must NOT stamp an instrument key: {line}"
// the sanctioned additive delta: the campaign substrate stamps every
// member's manifest with the instrument.
assert_eq!(
manifest["instrument"].as_str(),
Some("GER40"),
"the dissolved sweep stamps the instrument: {line}"
);
}
@@ -1485,6 +1487,131 @@ fn sweep_real_blueprint_member_lines_pin_the_inline_contract() {
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
assert!(fams_out.contains("\"members\":4"), "2x2 grid -> four members: {fams_out}");
// Auto-registration: generated documents are durable intent — exactly
// one process doc, one campaign doc, one campaign run record.
let count = |sub: &str| {
std::fs::read_dir(dir.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "one generated process document registered");
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
let runs_log = std::fs::read_to_string(dir.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
// `CampaignRunRecord` (aura-registry's lineage.rs)
// carries no name field at all (campaign id/process id/run/seed/cells/
// generalizations/trace_name only) — the `--name` handle is NOT part of
// the run-log line. It IS part of the generated CAMPAIGN DOCUMENT
// (`translate_sweep` sets `campaign.name = name`), so the handle is
// checked on that persisted document instead.
let campaigns_dir = dir.join("runs").join("campaigns");
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
.expect("campaigns dir exists")
.next()
.expect("exactly one campaign document")
.expect("readable dir entry")
.path();
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
assert!(
campaign_doc_json.contains("\"name\":\"brp\""),
"the --name handle lands on the generated campaign document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam, docs/specs/sweep-dissolution.md Task 4
// Step 3): the CLI's `--axis` names are typed against the WRAPPED probe
// namespace (`sma_signal.fast.length`), but a campaign document's
// `strategies[].axes` speak the RAW `param_space` namespace
// (`fast.length`) — `bind_axes`'s convention (#203). `dispatch_sweep`
// strips the wrapper's one leading node segment before generating the
// document; a regression that stopped stripping (or stripped one segment
// too many/few) would persist the wrong axis keys and
// `validate_campaign_refs` would refuse every axis as unknown before any
// member ran — this pin catches that at the artifact level, directly.
let campaign_doc: serde_json::Value =
serde_json::from_str(&campaign_doc_json).expect("generated campaign doc parses as JSON");
let axes = campaign_doc["strategies"][0]["axes"]
.as_object()
.expect("strategies[0].axes is an object");
assert!(
axes.contains_key("fast.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
axes.contains_key("slow.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
!axes.contains_key("sma_signal.fast.length") && !axes.contains_key("sma_signal.slow.length"),
"the wrapped CLI probe name must never leak into the stored document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam: the ms<->ns crossing, C3): the probed
// archive window is carried through `DataSource::full_window` (aura's
// native epoch-ns `Timestamp`, clamped to the archive's actual first/last
// bar inside the requested range) and converted back to Unix-ms via
// `aura_ingest::epoch_ns_to_unix_ms` exactly once before landing in the
// generated document's `data.windows[0]`. A regression that skipped the
// conversion (or applied it twice, or swapped ms<->ns) would persist a
// window off by a factor of ~1e6 from the requested `--from`/`--to` —
// silently, since the executor would still run (over the wrong or an
// empty range) rather than refuse outright. Pin the sane invariant that
// survives the archive's exact clamp: the stored window is a
// (from <= to) sub-range of the requested Unix-ms bounds, not a value
// orders of magnitude away.
let from_req = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap();
let to_req = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap();
let window = &campaign_doc["data"]["windows"][0];
let from_stored = window["from_ms"].as_i64().expect("from_ms is an integer");
let to_stored = window["to_ms"].as_i64().expect("to_ms is an integer");
assert!(
from_req <= from_stored && from_stored <= to_stored && to_stored <= to_req,
"the stored window is a ms sub-range of the requested [{from_req}, {to_req}]: {campaign_doc_json}"
);
// Dedup: a second identical invocation reuses both documents (content-
// addressed) and appends a second run record.
let out2 = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.output()
.unwrap();
assert!(out2.status.success(), "second run stderr: {}", String::from_utf8_lossy(&out2.stderr));
// `family_id` carries a per-execution uniqueness
// suffix ("the '-{run}' suffix is appended by the registry",
// aura-campaign/src/exec.rs `run_cell`) — a fresh family record is
// written on every execution even when the campaign/process documents
// themselves dedupe, so two identical invocations are NOT literally
// byte-identical on stdout: only the trailing run-suffix (0 -> 1)
// differs. C1 determinism is checked at the member-report level instead
// (every report field byte-identical), plus the run-suffix increment.
let out2_stdout = String::from_utf8_lossy(&out2.stdout).into_owned();
let lines1: Vec<&str> = stdout.lines().collect();
let lines2: Vec<&str> = out2_stdout.lines().collect();
assert_eq!(lines2.len(), lines1.len(), "same member count on the dedup run: {out2_stdout}");
for (l1, l2) in lines1.iter().zip(&lines2) {
let v1: serde_json::Value = serde_json::from_str(l1).expect("first run member line parses");
let v2: serde_json::Value = serde_json::from_str(l2).expect("second run member line parses");
assert_eq!(v1["report"], v2["report"], "identical invocations reproduce identical member reports (C1): {l1} vs {l2}");
let (fam1, fam2) = (v1["family_id"].as_str().unwrap(), v2["family_id"].as_str().unwrap());
assert_eq!(
fam1.trim_end_matches(|c: char| c.is_ascii_digit()),
fam2.trim_end_matches(|c: char| c.is_ascii_digit()),
"same family name up to the run-suffix: {fam1} vs {fam2}"
);
assert!(fam1.ends_with("-0") && fam2.ends_with("-1"), "run-suffix increments 0 -> 1: {fam1} vs {fam2}");
}
assert_eq!(count("processes"), 1, "second identical run dedupes the process doc");
assert_eq!(count("campaigns"), 1, "second identical run dedupes the campaign doc");
let _ = std::fs::remove_dir_all(&dir);
}