feat(research,cli): the CostSpec campaign vocabulary (#234 task 2)
CampaignDoc gains an additive cost: Vec<CostSpec> block — a closed, externally tagged vocabulary over the three shipped cost nodes (constant/vol_slippage/carry, field names = the builders' ParamSpec names), mirroring the risk block: serde default + skip-if-empty (cost- less documents keep their content ids, pinned), DocFault::BadCost validation (finite/bounds per component), std::cost construct- vocabulary block + SlotKind::Costs + open-slot mirror, prose arm, and all CampaignDoc struct literals across crates carry cost: vec![] (compile-driven). Verified: aura-research suite green, campaign validate e2e green, full workspace suite green, clippy -D warnings clean; in-loop spec review compliant and quality review approved (real diff fingerprint — the worktree-anchored review prompt works). refs #234
This commit is contained in:
@@ -597,7 +597,7 @@ fn campaign_introspect_vocabulary_lists_sections() {
|
||||
// #216: std::risk joins the roster the CLI's own --vocabulary listing
|
||||
// enumerates — the regime axis is discoverable from the public surface,
|
||||
// not only from the design ledger or a generated document.
|
||||
for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation", "std::risk"] {
|
||||
for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation", "std::risk", "std::cost"] {
|
||||
assert!(out.contains(id), "vocabulary misses {id}: {out}");
|
||||
}
|
||||
}
|
||||
@@ -625,6 +625,23 @@ fn campaign_introspect_block_risk_describes_the_regime_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #234: `--block std::cost` names the component shape and its
|
||||
/// absent-means-zero-costs semantics — the cost model is discoverable from
|
||||
/// the public introspection surface (the `std::risk` #216 precedent).
|
||||
#[test]
|
||||
fn campaign_introspect_block_cost_describes_the_component_shape() {
|
||||
let (out, code) = run_code(&["campaign", "introspect", "--block", "std::cost"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)"),
|
||||
"the cost slot must render as optional with the component shape: {out}"
|
||||
);
|
||||
assert!(
|
||||
out.contains("std::cost — campaign section: the cost model"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: a complete-but-risk-less campaign document (the exact shape a
|
||||
/// verb-generated document has before a `--stop-*` flag is added) has its one
|
||||
/// remaining open slot — the optional risk axis — enumerated by the public
|
||||
@@ -660,22 +677,23 @@ fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #216: a bound (non-empty) risk list closes the slot — a complete document
|
||||
/// with risk reports no open slots at all.
|
||||
/// #216/#234: bound (non-empty) risk AND cost lists close both optional
|
||||
/// slots — a complete document with both reports no open slots at all.
|
||||
#[test]
|
||||
fn campaign_introspect_unwired_omits_a_bound_risk_slot() {
|
||||
fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() {
|
||||
let dir = temp_cwd("campaign-introspect-unwired-risk-bound");
|
||||
let with_risk = CAMPAIGN_DOC.replacen(
|
||||
let with_both = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],",
|
||||
"\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 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);
|
||||
assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bound.campaign.json", &with_both);
|
||||
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("open slot: cost"), "bound cost must not list: {out}");
|
||||
assert!(out.contains("no open slots"), "stdout/stderr: {out}");
|
||||
}
|
||||
|
||||
@@ -965,6 +983,33 @@ fn campaign_register_refuses_invalid_risk_section_and_writes_nothing() {
|
||||
);
|
||||
}
|
||||
|
||||
/// #234: the register validate-gate extends to the cost model — a document
|
||||
/// whose only fault is a negative cost knob is refused with prose (exit 1)
|
||||
/// and no file is ever written under `runs/campaigns/` (the risk-section
|
||||
/// precedent).
|
||||
#[test]
|
||||
fn campaign_register_refuses_invalid_cost_section_and_writes_nothing() {
|
||||
let dir = temp_cwd("campaign-register-invalid-cost");
|
||||
let bad = CAMPAIGN_DOC.replacen(
|
||||
"\"seed\": 1,",
|
||||
"\"seed\": 1,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": -2.0 } } ],",
|
||||
1,
|
||||
);
|
||||
assert_ne!(bad, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "bad-cost.campaign.json", &bad);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "register", "bad-cost.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("refusing to register:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("cost[0]: the component's price-unit knob must be finite and >= 0"),
|
||||
"stdout/stderr: {out}"
|
||||
);
|
||||
assert!(
|
||||
!dir.join("runs").join("campaigns").exists(),
|
||||
"register must not create a store entry for an invalid cost 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