feat(research): RiskRegime vocabulary + CampaignDoc.risk field + validation (#210 T1-2)
The first slice of the risk-regime structural axis (spec/plan abab1f1):
- aura-research: a serializable `RiskRegime` closed vocabulary (externally
tagged, sole variant `Vol { length, k }` — a content-addressable mirror of
the shipped two-variant StopRule, so a future Fixed regime is additive), and
an optional top-level `CampaignDoc.risk: Vec<RiskRegime>` that absent-
serializes (the `description` precedent) — content-id parity for every
risk-less document.
- validate_campaign refuses a non-positive regime (length < 1 or k <= 0, NaN
included) via a new `DocFault::BadRegime`; the CLI renders it as prose at the
research_docs seam (register/validate paths).
- The three CLI-seam E2E tests pin that a well-formed risk section passes
`campaign validate` and a bad one is refused with prose (exit 1, no Debug
leak) — the field is reachable at the seam a data author drives, not only in
library units.
Adding the field is compile-breaking at every `CampaignDoc { .. }` literal
(serde default does not cover Rust construction), so this slice also threads
`risk: vec![]` into the sites across aura-cli and aura-campaign — the
field-addition's compile-gate completion the plan should have scoped here. The
BadRegime prose reads `risk[i]: ...` (the implementer's terser wording over the
plan's illustrative string; the spec pinned no exact text). Full workspace
suite green.
refs #210
This commit is contained in:
@@ -500,6 +500,53 @@ fn campaign_validate_refuses_empty_axis_prose_exit_1() {
|
||||
assert!(!out.contains("EmptyAxis"), "Debug leak: {out}");
|
||||
}
|
||||
|
||||
/// #210 risk-regime axis: a campaign document carrying a `risk` section
|
||||
/// round-trips through the CLI's actual `campaign validate` binary path
|
||||
/// (parse -> `validate_campaign`) without a fault. Before this iteration the
|
||||
/// `RiskRegime`/`CampaignDoc.risk` wiring was proven only inside
|
||||
/// `aura-research`'s own unit tests; this pins that the new field is actually
|
||||
/// reachable — and accepted when well-formed — at the CLI seam a data author
|
||||
/// drives, not merely inside the library.
|
||||
#[test]
|
||||
fn campaign_validate_accepts_a_valid_risk_section_end_to_end() {
|
||||
let dir = temp_cwd("campaign-validate-risk-ok");
|
||||
let with_risk = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 1.5 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "risk-ok.campaign.json", &with_risk);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-ok.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert!(out.contains("campaign document valid (intrinsic):"), "stdout/stderr: {out}");
|
||||
assert!(!out.contains("BadRegime"), "Debug leak: {out}");
|
||||
}
|
||||
|
||||
/// #210: a non-positive stop regime (`k <= 0`, here `k: 0.0`) is refused by
|
||||
/// `validate_campaign`'s `DocFault::BadRegime` and — the property under test —
|
||||
/// that fault reaches the CLI's `doc_fault_prose` mapping end to end: exit 1,
|
||||
/// prose naming the offending index and BOTH malformed fields ("stop length
|
||||
/// must be >= 1 and k must be > 0"), never the bare `BadRegime` Debug variant.
|
||||
#[test]
|
||||
fn campaign_validate_refuses_bad_risk_regime_prose_exit_1() {
|
||||
let dir = temp_cwd("campaign-validate-risk-bad");
|
||||
let with_bad_risk = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 0.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(with_bad_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "risk-bad.campaign.json", &with_bad_risk);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-bad.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("risk[0]: stop length must be >= 1 and k must be > 0"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
assert!(!out.contains("BadRegime"), "Debug leak: {out}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_introspect_vocabulary_lists_sections() {
|
||||
let (out, code) = run_code(&["campaign", "introspect", "--vocabulary"]);
|
||||
@@ -719,6 +766,35 @@ fn campaign_register_refuses_invalid_document_and_writes_nothing() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #210: the register validate-gate extends to the new risk axis — a
|
||||
/// document whose only fault is a non-positive stop regime is refused with
|
||||
/// prose (exit 1) and, the property that matters for a content-addressed
|
||||
/// store, no file is ever written under `runs/campaigns/`. A store that
|
||||
/// accepted a document because register only checked the OLD fault kinds
|
||||
/// would silently persist an unenforceable risk regime.
|
||||
#[test]
|
||||
fn campaign_register_refuses_invalid_risk_section_and_writes_nothing() {
|
||||
let dir = temp_cwd("campaign-register-invalid-risk");
|
||||
let bad = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 0, \"k\": 2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(bad, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bad-risk.campaign.json", &bad);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "register", "bad-risk.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("refusing to register:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("risk[0]: stop length must be >= 1 and k must be > 0"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
assert!(
|
||||
!dir.join("runs").join("campaigns").exists(),
|
||||
"register must not create a store entry for an invalid risk section"
|
||||
);
|
||||
}
|
||||
|
||||
/// Seed one open-param blueprint into the built demo project's store via a
|
||||
/// real sweep and return its content id (the referential test's recipe).
|
||||
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||||
|
||||
Reference in New Issue
Block a user