From 7943b123ae2b69d5595aeb53402541e2121d4680 Mon Sep 17 00:00:00 2001 From: claude Date: Fri, 24 Jul 2026 16:02:45 +0200 Subject: [PATCH] fix(aura-strategy): charge-basis-specific cost-node one-liners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-strategy/src/carry_cost.rs | 1 + crates/aura-strategy/src/constant_cost.rs | 1 + crates/aura-strategy/src/cost.rs | 34 ++++++++++++------- crates/aura-strategy/src/vol_slippage_cost.rs | 1 + 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/aura-strategy/src/carry_cost.rs b/crates/aura-strategy/src/carry_cost.rs index 47bd237..ecf1d9e 100644 --- a/crates/aura-strategy/src/carry_cost.rs +++ b/crates/aura-strategy/src/carry_cost.rs @@ -29,6 +29,7 @@ impl CarryCost { "CarryCost", Vec::new(), 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())), ) } diff --git a/crates/aura-strategy/src/constant_cost.rs b/crates/aura-strategy/src/constant_cost.rs index dbd887b..dcd5d16 100644 --- a/crates/aura-strategy/src/constant_cost.rs +++ b/crates/aura-strategy/src/constant_cost.rs @@ -28,6 +28,7 @@ impl ConstantCost { "ConstantCost", Vec::new(), 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())), ) } diff --git a/crates/aura-strategy/src/cost.rs b/crates/aura-strategy/src/cost.rs index 28c1bf7..afabe62 100644 --- a/crates/aura-strategy/src/cost.rs +++ b/crates/aura-strategy/src/cost.rs @@ -214,26 +214,20 @@ impl Node for CostRunner { } /// 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 -/// closure. The single home for the cost-node schema shape. +/// extra inputs, the standard 3-field cost output, the given params, a +/// 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( name: &'static str, extra_inputs: Vec, params: Vec, + doc: &'static str, build: impl Fn(&[Cell]) -> Box + 'static, ) -> PrimitiveBuilder { let mut inputs = geometry_input_ports(); inputs.extend(extra_inputs); - PrimitiveBuilder::new( - 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, - ) + PrimitiveBuilder::new(name, NodeSchema { inputs, output: cost_output_fields(), params, doc }, build) } #[cfg(test)] @@ -501,6 +495,7 @@ mod tests { "Probe", extra.clone(), params.clone(), + "cost-model node: probe factor for the builder-assembly test", |_p| -> Box { Box::new(CostRunner::new(StubCost(0.0))) }, ); let schema = builder.schema(); @@ -537,6 +532,21 @@ mod tests { 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] fn producer_and_aggregator_share_the_triple() { // The structural lockstep: producer output and aggregator output/inputs all diff --git a/crates/aura-strategy/src/vol_slippage_cost.rs b/crates/aura-strategy/src/vol_slippage_cost.rs index 695a074..5cad3fa 100644 --- a/crates/aura-strategy/src/vol_slippage_cost.rs +++ b/crates/aura-strategy/src/vol_slippage_cost.rs @@ -43,6 +43,7 @@ impl VolSlippageCost { "VolSlippageCost", volatility_input_ports(), 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())), ) }