feat(cli,docs): the r_channel OHLC example + ledger amendment C26 (#231 tasks 6-7)

The acceptance proof: hl_channel — a causal Donchian channel (Delay(1)
excludes the current bar, C2) consuming high/low/close roles, built from
rostered vocabulary, shipped as the r_channel/r_channel_open example
pair (builder + regenerator + serialize pins, the r_* pattern). Proven
at three layers: a non-gated loaded-vs-carve equivalence over inline
sources (non-zero-bias hardened per review), the unconditional synthetic
multi-column refusal, and archive-gated CLI e2e — aura run on GER40
end-to-end plus a sweep whose members run and reproduce 2/2
bit-identically.

Ledger: new C26 entry (the binding vocabulary — closed column set +
price alias, name-driven default + campaign data.bindings override,
canonical order = the C4 tie-break, Blockly-litmus argument, the #71
extension point for recorded non-price sources) and the C20/C24
scaffolding-clause annotations: the single-price data weld is retired,
wrap_r's remaining R-scaffolding retirement stays #159.

Verified: all proof tests green, full workspace suite green, clippy -D
warnings clean; independent quality review approved.

closes #231
This commit is contained in:
2026-07-11 05:10:05 +02:00
parent 43b1c7ff5d
commit 7748b53a10
5 changed files with 328 additions and 2 deletions
+119
View File
@@ -1010,6 +1010,125 @@ fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
}
}
/// Acceptance (harness-input-binding spec, #231): an OHLC-consuming strategy
/// blueprint runs end to end on real archive data through `aura run` — the
/// role names bind columns, three real sources open in canonical order, the
/// close column feeds broker/executor, and the report carries the R summary.
/// Gated on the local GER40 archive; skips cleanly when absent.
#[test]
fn run_real_ohlc_channel_example_end_to_end() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let cwd = temp_cwd("ohlc-run-real");
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"run", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
])
.current_dir(&cwd)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("run report parses as JSON");
assert!(
v["manifest"]["topology_hash"].as_str().is_some(),
"report carries the signal hash: {stdout}"
);
assert!(v["metrics"]["r"].is_object(), "the R summary is computed: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// The synthetic walk generates a close series only: a multi-column blueprint
/// without `--real` refuses honestly (exit 1, columns + remedy named). NOT
/// gated — the refusal precedes any data access.
#[test]
fn run_synthetic_refuses_a_multi_column_blueprint() {
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args(["run", &fixture])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("consumes columns beyond close"), "names the shape: {stderr}");
assert!(stderr.contains("--real"), "names the remedy: {stderr}");
}
/// The dissolved real-data sweep + reproduce over the OHLC example (#231
/// acceptance): `--list-axes` pins the ganged channel knob's wrapped axis
/// name, the sweep runs one member per grid point over three real columns,
/// and every member re-derives bit-identically from the store (#229 real-data
/// reproduce, now multi-column). Gated on the local GER40 archive.
#[test]
fn sweep_real_ohlc_channel_members_run_and_reproduce() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_channel_open.json", env!("CARGO_MANIFEST_DIR"));
let axes = std::process::Command::new(BIN)
.args(["sweep", &fixture, "--list-axes"])
.current_dir(dir)
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&axes.stdout).trim(),
"hl_channel.channel_length:I64",
"the ganged channel knob is the one sweepable axis; stderr: {}",
String::from_utf8_lossy(&axes.stderr)
);
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "hl_channel.channel_length=3,5",
"--name", "chan",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
assert_eq!(stdout.lines().count(), 2, "one member line per channel length: {stdout}");
for line in stdout.lines() {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
assert_eq!(
v["report"]["manifest"]["instrument"].as_str(),
Some("GER40"),
"campaign member manifest stamps the instrument: {line}"
);
}
let family_id = serde_json::from_str::<serde_json::Value>(stdout.lines().next().unwrap())
.expect("first member line parses")["family_id"]
.as_str()
.expect("member line carries family_id")
.to_string();
let rep = std::process::Command::new(BIN)
.args(["reproduce", &family_id])
.current_dir(dir)
.output()
.unwrap();
assert!(
rep.status.success(),
"reproduce stderr: {}",
String::from_utf8_lossy(&rep.stderr)
);
assert!(
String::from_utf8_lossy(&rep.stdout).contains("reproduced 2/2 members bit-identically"),
"stdout: {}",
String::from_utf8_lossy(&rep.stdout)
);
}
/// 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