feat(research,cli,docs): state cost and risk knob units in introspect and the glossary

campaign introspect --unwired now annotates the four silently
misreadable knobs with unit/semantics sub-lines: cost_per_trade is a
price-unit numerator charged as cost/|entry−stop| in R (not a cost in
R); slip_vol_mult multiplies the per-cycle vol estimate; vol.length
smooths the estimator and does not set the stop's timescale; vol.k
scales the stop distance (the risk-unit lever). Rendered additively
via a new OpenSlot.notes field, so the sibling tests pinning the slot
hint lines verbatim stay green byte-identically.

The glossary's cost-model and risk-regime entries mirror the same
semantics inline (two-sentence convention kept), the risk entry
pointing at #262 for the timescale-matched variant.

closes #265
This commit is contained in:
2026-07-13 23:12:36 +02:00
parent e12b4a730b
commit e8b6878d3b
3 changed files with 46 additions and 8 deletions
+38 -6
View File
@@ -978,10 +978,22 @@ pub struct OpenSlot {
pub path: String,
/// Short human hint, e.g. "required, one of: content_id, identity_id".
pub hint: String,
/// Additional unit/semantics notes rendered as indented sub-lines under
/// the slot's main line (#265) — additive, so the `hint` string itself
/// (pinned verbatim by earlier tests) never changes.
pub notes: Vec<String>,
}
fn open(path: impl Into<String>, hint: impl Into<String>) -> OpenSlot {
OpenSlot { path: path.into(), hint: hint.into() }
OpenSlot { path: path.into(), hint: hint.into(), notes: Vec::new() }
}
/// Like `open`, but carries additional unit/semantics notes (#265) rendered
/// as indented sub-lines under the slot — the `hint` string stays exactly as
/// `open` would have produced it, so sibling tests pinning `hint` verbatim
/// are unaffected.
fn open_with_notes(path: impl Into<String>, hint: impl Into<String>, notes: Vec<String>) -> OpenSlot {
OpenSlot { path: path.into(), hint: hint.into(), notes }
}
/// List the open slots of a (possibly partial) process document. Only the
@@ -1064,18 +1076,26 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
// 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(
slots.push(open_with_notes(
"risk",
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
vec![
"vol.length smooths the per-cycle vol estimator; it does not set the stop's timescale.".to_string(),
"vol.k scales the stop distance (the risk-unit lever).".to_string(),
],
));
}
// The optional cost slot, mirroring the risk axis (#216's discoverability
// lesson applied to #234): absent, empty, or non-array = open.
let cost_bound = v.get("cost").and_then(|c| c.as_array()).is_some_and(|a| !a.is_empty());
if !cost_bound {
slots.push(open(
slots.push(open_with_notes(
"cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
vec![
"constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(),
"vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(),
],
));
}
match v.get("strategies").and_then(|s| s.as_array()) {
@@ -1932,13 +1952,21 @@ mod tests {
assert_eq!(
open_slots_campaign(CAMPAIGN_FIXTURE).unwrap(),
vec![
open(
open_with_notes(
"risk",
"optional, list of stop regimes { vol: { length, k } }; absent = one default regime",
vec![
"vol.length smooths the per-cycle vol estimator; it does not set the stop's timescale.".to_string(),
"vol.k scales the stop distance (the risk-unit lever).".to_string(),
],
),
open(
open_with_notes(
"cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
vec![
"constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(),
"vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(),
],
),
]
);
@@ -2012,9 +2040,13 @@ mod tests {
assert_ne!(with_risk, CAMPAIGN_FIXTURE, "replacen must match the fixture's seed field");
assert_eq!(
open_slots_campaign(&with_risk).unwrap(),
vec![open(
vec![open_with_notes(
"cost",
"optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)",
vec![
"constant.cost_per_trade is in price units, not R (charged per close as cost/|entry-stop| in R).".to_string(),
"vol_slippage.slip_vol_mult is a multiplier on the per-cycle vol estimate (charged in R likewise).".to_string(),
],
)]
);
let with_both = CAMPAIGN_FIXTURE.replacen(