feat(research,cli): make the risk axis discoverable — probe, roster, validate counts (#216 slice 1)
The structural risk-regime axis worked end to end but was invisible on the public surface (refs #216): the --unwired probe swept only required slots (risk is #[serde(default)]), no vocabulary block described RiskRegime, and the validate summary counted axes/windows but no regimes or cells. - open_slots_campaign gains its one optional-slot probe: a risk-less (or empty/non-array) document lists 'open slot: risk (optional, list of stop regimes { vol: { length, k } }; absent = one default regime)'; a bound list closes the slot. Never a required flip — content-id parity of risk-less documents is load-bearing. - CAMPAIGN_SECTIONS gains std::risk (SlotKind::Regimes, required: false) so --vocabulary lists it and --block std::risk describes the regime shape. - The intrinsic validate summary now reports the realized matrix, mirroring the exec cross-product exactly (strategies x instruments x windows x regimes; empty risk = one default cell, marked '(default)'). Points stay a grid property, deliberately not a cell factor. Sanctioned migrations (recorded on #216): the two unit pins encoding the old invisibility — open_slots_report_partial_documents_by_path now pins exactly one open slot on the complete risk-less fixture (doubling as the probe's unit pin; fixture bytes unchanged), and the presentation-hint test migrates from full-vector equality to locating its entry (its purpose is the hint text, not the vector length). Verified: cargo test -p aura-research (29/0), -p aura-cli --test research_docs (60/0), clippy -D warnings clean.
This commit is contained in:
@@ -158,6 +158,7 @@ pub enum SlotKind {
|
||||
EmitKinds,
|
||||
/// The closed tap vocabulary (`tap_vocabulary()` entries).
|
||||
TapKinds,
|
||||
Regimes,
|
||||
}
|
||||
|
||||
/// One typed slot of a block.
|
||||
@@ -819,6 +820,11 @@ pub const CAMPAIGN_SECTIONS: &[BlockSchema] = &[
|
||||
SlotInfo { name: "windows", kind: SlotKind::Windows, required: true },
|
||||
],
|
||||
},
|
||||
BlockSchema {
|
||||
id: "std::risk",
|
||||
doc: "campaign section: the structural risk axis — stop regimes the matrix runs each cell under (absent = one default regime)",
|
||||
slots: &[SlotInfo { name: "risk", kind: SlotKind::Regimes, required: false }],
|
||||
},
|
||||
BlockSchema {
|
||||
id: "std::strategy",
|
||||
doc: "campaign section: strategy ref (content or identity id) + param axes",
|
||||
@@ -859,6 +865,9 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
|
||||
SlotKind::Axes => "map: param name -> { kind, values }",
|
||||
SlotKind::EmitKinds => "list of: family_table | selection_report",
|
||||
SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity",
|
||||
SlotKind::Regimes => {
|
||||
"list of stop regimes { vol: { length, k } }; absent = one default regime"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,6 +977,16 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The one optional slot the probe lists: the structural risk axis is
|
||||
// invisible to a required-slot sweep (#[serde(default)]), which is exactly
|
||||
// how it went undiscovered (#216). Absent, empty, or non-array = open.
|
||||
let risk_bound = v.get("risk").and_then(|r| r.as_array()).is_some_and(|a| !a.is_empty());
|
||||
if !risk_bound {
|
||||
slots.push(open(
|
||||
"risk",
|
||||
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
|
||||
));
|
||||
}
|
||||
match v.get("strategies").and_then(|s| s.as_array()) {
|
||||
None => slots.push(open("strategies", "required, non-empty list of { ref, axes }")),
|
||||
Some(list) if list.is_empty() => {
|
||||
@@ -1595,22 +1614,20 @@ mod tests {
|
||||
"process": { "ref": { "content_id": "4e2d" } },
|
||||
"seed": 1 }"#;
|
||||
let slots = open_slots_campaign(draft).unwrap();
|
||||
let pres = slots
|
||||
.iter()
|
||||
.find(|s| s.path == "presentation")
|
||||
.expect("presentation slot must be open");
|
||||
assert_eq!(
|
||||
slots,
|
||||
vec![open(
|
||||
"presentation",
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().join(" | ")
|
||||
),
|
||||
)]
|
||||
pres.hint,
|
||||
format!(
|
||||
"required section: persist_taps ({}) + emit",
|
||||
tap_vocabulary().join(" | ")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
slots,
|
||||
vec![open(
|
||||
"presentation",
|
||||
"required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit",
|
||||
)]
|
||||
pres.hint,
|
||||
"required section: persist_taps (equity | exposure | r_equity | net_r_equity) + emit"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1681,7 +1698,15 @@ mod tests {
|
||||
#[test]
|
||||
fn open_slots_report_partial_documents_by_path() {
|
||||
assert_eq!(open_slots_process(PROCESS_FIXTURE).unwrap(), Vec::new());
|
||||
assert_eq!(open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(), Vec::new());
|
||||
// #216: a complete risk-less campaign has exactly ONE open slot — the
|
||||
// optional risk axis (the probe's one optional line).
|
||||
assert_eq!(
|
||||
open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(),
|
||||
vec![open(
|
||||
"risk",
|
||||
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
|
||||
)]
|
||||
);
|
||||
|
||||
let partial = r#"{ "format_version": 1, "kind": "process",
|
||||
"pipeline": [ { "block": "std::sweep", "metric": "sqn" } ] }"#;
|
||||
@@ -1739,6 +1764,21 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: the optional risk slot is listed only while unbound — a document
|
||||
/// with a non-empty risk list closes it (a complete bound document has no
|
||||
/// open slots at all; the risk-less complement is pinned in
|
||||
/// `open_slots_report_partial_documents_by_path`).
|
||||
#[test]
|
||||
fn open_slots_omit_a_bound_risk_slot() {
|
||||
let with_risk = CAMPAIGN_FIXTURE.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_risk, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field");
|
||||
assert_eq!(open_slots_campaign(&with_risk).unwrap(), Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sweep_selection_group_is_all_or_nothing() {
|
||||
let free = serde_json::json!({ "block": "std::sweep" });
|
||||
|
||||
Reference in New Issue
Block a user