fix(aura-strategy): charge-basis-specific cost-node one-liners

The three cost nodes (ConstantCost, CarryCost, VolSlippageCost) shared a
byte-identical generic doc line, so `aura graph introspect --vocabulary`
could not tell them apart — C29's per-entry meaning was formally met but
not entry-specific (binary-only M1 fieldtest finding).

cost_node_builder now takes the factor-specific one-line doc as a
parameter (the shared string is gone from the single schema home); each
builder states its own charge basis. RED-first:
cost_node_descriptions_are_pairwise_distinct failed against the shared
string before the fix.

closes #330
This commit is contained in:
2026-07-24 16:02:45 +02:00
parent 7cc3ce0d9e
commit 7943b123ae
4 changed files with 25 additions and 12 deletions
+1
View File
@@ -29,6 +29,7 @@ impl CarryCost {
"CarryCost", "CarryCost",
Vec::new(), Vec::new(),
vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }], vec![ParamSpec { name: "carry_per_cycle".into(), kind: ScalarKind::F64 }],
"cost-model node: cost accrued per held cycle (param carry_per_cycle)",
|p| Box::new(CarryCost::new(p[0].f64())), |p| Box::new(CarryCost::new(p[0].f64())),
) )
} }
@@ -28,6 +28,7 @@ impl ConstantCost {
"ConstantCost", "ConstantCost",
Vec::new(), Vec::new(),
vec![ParamSpec { name: "cost_per_trade".into(), kind: ScalarKind::F64 }], vec![ParamSpec { name: "cost_per_trade".into(), kind: ScalarKind::F64 }],
"cost-model node: fixed cost per trade (param cost_per_trade), charged at close",
|p| Box::new(ConstantCost::new(p[0].f64())), |p| Box::new(ConstantCost::new(p[0].f64())),
) )
} }
+22 -12
View File
@@ -214,26 +214,20 @@ impl<F: CostNode> Node for CostRunner<F> {
} }
/// Assemble a cost-node `PrimitiveBuilder`: the 4 geometry inputs ++ the factor's /// Assemble a cost-node `PrimitiveBuilder`: the 4 geometry inputs ++ the factor's
/// extra inputs, the standard 3-field cost output, the given params, and a build /// extra inputs, the standard 3-field cost output, the given params, a
/// closure. The single home for the cost-node schema shape. /// factor-specific one-line `doc` (C29 — each cost node names its own charge
/// basis rather than sharing one generic sentence, #330), and a build closure.
/// The single home for the cost-node schema shape.
pub fn cost_node_builder( pub fn cost_node_builder(
name: &'static str, name: &'static str,
extra_inputs: Vec<PortSpec>, extra_inputs: Vec<PortSpec>,
params: Vec<ParamSpec>, params: Vec<ParamSpec>,
doc: &'static str,
build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static, build: impl Fn(&[Cell]) -> Box<dyn Node> + 'static,
) -> PrimitiveBuilder { ) -> PrimitiveBuilder {
let mut inputs = geometry_input_ports(); let mut inputs = geometry_input_ports();
inputs.extend(extra_inputs); inputs.extend(extra_inputs);
PrimitiveBuilder::new( PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params, doc }, build)
name,
NodeSchema {
inputs,
output: cost_output_fields(),
params,
doc: "cost-model node: charges its cost in R from position geometry, at close or accrued per held cycle",
},
build,
)
} }
#[cfg(test)] #[cfg(test)]
@@ -501,6 +495,7 @@ mod tests {
"Probe", "Probe",
extra.clone(), extra.clone(),
params.clone(), params.clone(),
"cost-model node: probe factor for the builder-assembly test",
|_p| -> Box<dyn Node> { Box::new(CostRunner::new(StubCost(0.0))) }, |_p| -> Box<dyn Node> { Box::new(CostRunner::new(StubCost(0.0))) },
); );
let schema = builder.schema(); let schema = builder.schema();
@@ -537,6 +532,21 @@ mod tests {
assert_eq!(a, "volatility"); assert_eq!(a, "volatility");
} }
#[test]
fn cost_node_descriptions_are_pairwise_distinct() {
// C29: `aura graph introspect --vocabulary` must be able to tell the
// three cost nodes apart by their charge basis (flat-per-trade vs
// per-held-cycle-accrual vs volatility-scaled), not read one
// identical generic sentence three times (#330).
use crate::{CarryCost, ConstantCost, VolSlippageCost};
let constant_doc = ConstantCost::builder().schema().doc;
let carry_doc = CarryCost::builder().schema().doc;
let vol_slippage_doc = VolSlippageCost::builder().schema().doc;
assert_ne!(constant_doc, carry_doc, "ConstantCost vs CarryCost doc must differ");
assert_ne!(constant_doc, vol_slippage_doc, "ConstantCost vs VolSlippageCost doc must differ");
assert_ne!(carry_doc, vol_slippage_doc, "CarryCost vs VolSlippageCost doc must differ");
}
#[test] #[test]
fn producer_and_aggregator_share_the_triple() { fn producer_and_aggregator_share_the_triple() {
// The structural lockstep: producer output and aggregator output/inputs all // The structural lockstep: producer output and aggregator output/inputs all
@@ -43,6 +43,7 @@ impl VolSlippageCost {
"VolSlippageCost", "VolSlippageCost",
volatility_input_ports(), volatility_input_ports(),
vec![ParamSpec { name: "slip_vol_mult".into(), kind: ScalarKind::F64 }], vec![ParamSpec { name: "slip_vol_mult".into(), kind: ScalarKind::F64 }],
"cost-model node: slippage proportional to volatility (slip_vol_mult × volatility input), charged at close",
|p| Box::new(VolSlippageCost::new(p[0].f64())), |p| Box::new(VolSlippageCost::new(p[0].f64())),
) )
} }