diff --git a/README.md b/README.md index f12436f..fa55804 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,9 @@ of stopping at reader-only diagnostics. The same save-native live bond-slot surface now also carries per-slot maturity years all the way through runtime summaries and annual bond policy state, which is the next owner seam needed for shellless repayment and bond-burden simulation instead of another round of raw-slot guessing. +That same seam now also derives the current live coupon burden directly from owned bond slots, so +later finance service work can consume a runtime reader instead of recomputing from scattered raw +fields. The same seam now also carries the fixed-world building-density growth setting plus the linked chairman personality byte, which is enough to run the annual stock-repurchase gate as another pure reader over owned save-native state instead of a guessed finance-side approximation. diff --git a/crates/rrt-runtime/src/runtime.rs b/crates/rrt-runtime/src/runtime.rs index 8d4eccf..fdf183f 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -136,6 +136,8 @@ pub struct RuntimeCompanyAnnualFinanceState { pub largest_live_bond_principal: Option, #[serde(default)] pub highest_coupon_live_bond_principal: Option, + #[serde(default)] + pub live_bond_coupon_burden_total: Option, pub assigned_share_pool: u32, pub unassigned_share_pool: u32, #[serde(default)] @@ -331,6 +333,8 @@ pub struct RuntimeCompanyAnnualBondPolicyState { #[serde(default)] pub next_live_bond_maturity_year: Option, #[serde(default)] + pub live_bond_coupon_burden_total: Option, + #[serde(default)] pub current_cash: Option, #[serde(default)] pub cash_after_full_repayment: Option, @@ -3104,6 +3108,24 @@ pub fn runtime_company_average_live_bond_coupon( Some(weighted_coupon_sum / total_principal as f64) } +pub fn runtime_company_live_bond_coupon_burden_total( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + let mut total = 0i64; + for slot in &market_state.live_bond_slots { + let coupon_rate = f32::from_bits(slot.coupon_rate_raw_u32) as f64; + if !coupon_rate.is_finite() { + continue; + } + let coupon_burden = + runtime_round_f64_to_i64((slot.principal as f64) * coupon_rate).unwrap_or(0); + total = total.checked_add(coupon_burden)?; + } + Some(total) +} + pub fn runtime_company_bond_interest_rate_quote_f64( state: &RuntimeState, company_id: u32, @@ -3287,6 +3309,7 @@ pub fn runtime_company_annual_bond_policy_state( matured_live_bond_count, matured_live_bond_principal_total, next_live_bond_maturity_year, + live_bond_coupon_burden_total: annual_finance_state.live_bond_coupon_burden_total, current_cash, cash_after_full_repayment, issue_cash_floor, @@ -4037,6 +4060,9 @@ pub fn runtime_company_annual_finance_state( bond_count: market_state.bond_count, largest_live_bond_principal: market_state.largest_live_bond_principal, highest_coupon_live_bond_principal: market_state.highest_coupon_live_bond_principal, + live_bond_coupon_burden_total: runtime_company_live_bond_coupon_burden_total( + state, company_id, + ), assigned_share_pool, unassigned_share_pool, cached_share_price: rounded_cached_share_price_i64(market_state.cached_share_price_raw_u32), @@ -7421,6 +7447,10 @@ mod tests { let average_live_bond_coupon = runtime_company_average_live_bond_coupon(&state, 7).expect("average coupon"); assert!((average_live_bond_coupon - 0.05).abs() < 1e-6); + assert_eq!( + annual_finance_state.live_bond_coupon_burden_total, + Some(5_000) + ); assert_eq!(runtime_world_prime_rate_baseline(&state), Some(5.0)); assert_eq!( runtime_world_issue_opinion_term_sum_raw( @@ -7801,6 +7831,7 @@ mod tests { bond_count: 3, largest_live_bond_principal: Some(650_000), highest_coupon_live_bond_principal: Some(500_000), + live_bond_coupon_burden_total: Some(0), assigned_share_pool: 15_500, unassigned_share_pool: 4_500, cached_share_price: Some(40), @@ -8308,6 +8339,7 @@ mod tests { assert_eq!(bond_state.matured_live_bond_count, Some(0)); assert_eq!(bond_state.matured_live_bond_principal_total, Some(0)); assert_eq!(bond_state.next_live_bond_maturity_year, None); + assert_eq!(bond_state.live_bond_coupon_burden_total, Some(30_000)); assert_eq!(bond_state.current_cash, Some(-400_000)); assert_eq!(bond_state.cash_after_full_repayment, Some(-750_000)); assert_eq!(bond_state.issue_cash_floor, Some(-30_000)); diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index b9e3510..2a46d05 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -122,6 +122,7 @@ pub struct RuntimeSummary { pub selected_company_annual_bond_matured_live_bond_count: Option, pub selected_company_annual_bond_matured_live_bond_principal_total: Option, pub selected_company_annual_bond_next_live_bond_maturity_year: Option, + pub selected_company_annual_bond_live_bond_coupon_burden_total: Option, pub selected_company_annual_bond_current_cash: Option, pub selected_company_annual_bond_cash_after_full_repayment: Option, pub selected_company_annual_bond_issue_cash_floor: Option, @@ -592,6 +593,10 @@ impl RuntimeSummary { selected_company_annual_bond_state .as_ref() .and_then(|bond_state| bond_state.next_live_bond_maturity_year), + selected_company_annual_bond_live_bond_coupon_burden_total: + selected_company_annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.live_bond_coupon_burden_total), selected_company_annual_bond_current_cash: selected_company_annual_bond_state .as_ref() .and_then(|bond_state| bond_state.current_cash), @@ -3215,6 +3220,10 @@ mod tests { summary.selected_company_annual_bond_next_live_bond_maturity_year, None ); + assert_eq!( + summary.selected_company_annual_bond_live_bond_coupon_burden_total, + Some(30_000) + ); assert_eq!( summary.selected_company_annual_bond_current_cash, Some(-400_000) diff --git a/docs/README.md b/docs/README.md index d83f4a9..b36c4e8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -152,7 +152,8 @@ The highest-value next passes are now: creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches against owned runtime state; the same live bond-slot owner surface now also carries save-native maturity years into annual bond policy summaries as the - next seam for shellless repayment work + next seam for shellless repayment work, and now also derives the current live coupon burden + directly from owned bond slots - the project rule on the remaining closure work is now explicit too: when one runtime-facing field is still ambiguous, prefer rehosting the owning source state or real reader/setter family first instead of guessing another derived leaf field from neighboring raw offsets diff --git a/docs/runtime-rehost-plan.md b/docs/runtime-rehost-plan.md index 8a78620..061d849 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -250,6 +250,9 @@ outstanding-share, issue-calendar, live bond-slot, and company activity state. The same owned live bond-slot surface now also carries maturity years through save import, runtime state, and annual bond summaries, which is the right next base for shellless repayment and bond-service simulation. +That same owner seam now also derives live coupon burden totals directly from saved bond slots, +which gives later finance service work a bounded runtime reader instead of another synthetic +finance leaf. ## Why This Boundary