diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index 49e8a34..9a327c7 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -615,7 +615,14 @@ fn campaign_introspect_block_risk_describes_the_regime_shape() { out.contains("list of stop regimes { vol: { length, k } }; absent = one default regime"), "the block description must name the regime shape: {out}" ); - assert!(out.contains("risk"), "stdout/stderr: {out}"); + assert!( + out.contains("optional, list of stop regimes { vol: { length, k } }; absent = one default regime"), + "the risk slot must render as optional: {out}" + ); + assert!( + out.contains("std::risk — campaign section: the structural risk axis"), + "stdout/stderr: {out}" + ); } /// #216: a complete-but-risk-less campaign document (the exact shape a @@ -639,6 +646,63 @@ fn campaign_introspect_unwired_lists_the_risk_slot_when_absent() { ); } +/// #216: the bare-envelope probe (`{}` — the issue's exact reproduction) +/// names the optional risk axis alongside the required envelope slots. +#[test] +fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() { + let dir = temp_cwd("campaign-introspect-unwired-risk-bare"); + write_doc(&dir, "bare.json", "{}"); + let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "bare.json"]); + assert_eq!(code, Some(0)); + assert!( + out.contains("open slot: risk (optional, list of stop regimes { vol: { length, k } }; absent = one default regime)"), + "stdout/stderr: {out}" + ); +} + +/// #216: a bound (non-empty) risk list closes the slot — a complete document +/// with risk reports no open slots at all. +#[test] +fn campaign_introspect_unwired_omits_a_bound_risk_slot() { + let dir = temp_cwd("campaign-introspect-unwired-risk-bound"); + let with_risk = CAMPAIGN_DOC.replacen( + "\"seed\": 1,", + "\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],", + 1, + ); + assert_ne!(with_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); + write_doc(&dir, "bound.campaign.json", &with_risk); + let (out, code) = + run_code_in(&dir, &["campaign", "introspect", "--unwired", "bound.campaign.json"]); + assert_eq!(code, Some(0)); + assert!(!out.contains("open slot: risk"), "bound risk must not list: {out}"); + assert!(out.contains("no open slots"), "stdout/stderr: {out}"); +} + +/// #216: an EXPLICIT `"risk": []` (key present, array empty) is still an open +/// slot — same as the key being absent entirely. The probe distinguishes +/// "bound" from "unbound" by non-emptiness, not by key presence +/// (`v.get("risk").and_then(|r| r.as_array()).is_some_and(|a| !a.is_empty())`); +/// a probe that instead branched on `is_none()` would wrongly treat an +/// explicit empty list as closed, and no other test in this file exercises +/// that branch (the absent-key and non-empty-array cases are covered above). +#[test] +fn campaign_introspect_unwired_lists_the_risk_slot_when_explicitly_empty() { + let dir = temp_cwd("campaign-introspect-unwired-risk-explicit-empty"); + let explicit_empty = CAMPAIGN_DOC.replacen("\"seed\": 1,", "\"seed\": 1,\n \"risk\": [],", 1); + assert_ne!(explicit_empty, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); + write_doc(&dir, "empty-risk.campaign.json", &explicit_empty); + let (out, code) = + run_code_in(&dir, &["campaign", "introspect", "--unwired", "empty-risk.campaign.json"]); + assert_eq!(code, Some(0), "stdout/stderr: {out}"); + assert!( + out.contains( + "open slot: risk (optional, list of stop regimes { vol: { length, k } }; absent = one default regime)" + ), + "an explicit empty risk array must still count as an open slot: {out}" + ); +} + #[test] fn campaign_introspect_unwired_reports_the_spec_example_slots() { let dir = temp_cwd("campaign-introspect-unwired"); diff --git a/docs/authoring-guide.md b/docs/authoring-guide.md index f0d06fb..fff4f75 100644 --- a/docs/authoring-guide.md +++ b/docs/authoring-guide.md @@ -266,13 +266,14 @@ open slot: format_version (required, must be 1) open slot: kind (required, must be "campaign") open slot: name (required, string) open slot: data (required section: instruments + windows) +open slot: risk (optional, list of stop regimes { vol: { length, k } }; absent = one default regime) open slot: strategies (required, non-empty list of { ref, axes }) open slot: process.ref (required, content id of a process document) open slot: seed (required, non-negative integer) open slot: presentation (required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit) ``` -### Worked example: two instruments, one strategy, four axis points +### Worked example: two instruments, one strategy, four axis points, two stop regimes ```json { @@ -284,6 +285,10 @@ open slot: presentation (required section: persist_taps (equity | exposure | r_e "instruments": ["GER40", "FRA40"], "windows": [ { "from_ms": 1725148800000, "to_ms": 1727740800000 } ] }, + "risk": [ + { "vol": { "length": 3, "k": 1.5 } }, + { "vol": { "length": 3, "k": 3.0 } } + ], "strategies": [ { "ref": { "content_id": "597d719b7ac607158cda3e68cd497387620397a5e93087e23da512876dafba9a" }, @@ -306,11 +311,17 @@ Each axis name (`fast.length`, …) must name an open param of the referenced blueprint (`aura graph introspect --params`, §1) and declare that param's `ScalarKind`. +The optional `risk` list is the campaign's structural risk axis: every cell +runs under every listed stop regime, so cells differ by execution discipline, +never by signal — the regime's stop defines the risk unit R. Absent or empty, +the matrix runs one implicit default regime (the same vol regime the +orchestration verbs bind when their stop flags are omitted). + ### Validate — three tiers, honest degradation ``` $ aura campaign validate mra_3_campaign_full_v2.json # outside any project -campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 window(s) +campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 2 instrument(s), 1 window(s), 2 regime(s) — 4 cell(s) referential checks skipped (no Aura.toml found up from /home/…) ``` @@ -319,7 +330,7 @@ registered, the same command runs two further tiers: ``` $ aura campaign validate mra_3_campaign_full_v2.json # inside a project, refs registered -campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 1 window(s) +campaign document valid (intrinsic): 1 strategy(ies), 3 axes (4 points), 2 instrument(s), 1 window(s), 2 regime(s) — 4 cell(s) campaign document valid (referential): all references resolve, axes are in the param space campaign document valid (executable): pipeline shape and static guards pass ``` diff --git a/docs/glossary.md b/docs/glossary.md index deba6ea..15861a4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -211,6 +211,14 @@ The per-symbol composite turning a bias into a managed position, **in R**: `stop **Avoid:** — A node that converts a finer stream to a coarser bar stream, emitting a completed bar only at the boundary so no partial bar ever leaks (enforcing no look-ahead). Clock-sensitive. +### risk regime +**Avoid:** stop regime, risk section +One entry of a campaign document's structural risk axis (`risk`): a +serializable protective-stop regime (sole variant `vol{length,k}`) the matrix +runs every cell under, so cells differ by execution discipline, never by +signal. Absent or empty = one implicit default regime; the regime's stop +defines the risk unit R. + ### run **Avoid:** — One execution recorded in the run registry as a manifest + metrics — the registry-record framing of an execution. Distinct from `sim` (the executable unit) and `backtest` (the replay framing).