From 3d31b0b65e3091219d1c1bf8f89b27c52b9ec4b9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 18 Apr 2026 01:43:35 -0700 Subject: [PATCH] Carry bond maturity through runtime state --- README.md | 3 ++ crates/rrt-runtime/src/runtime.rs | 75 +++++++++++++++++++++++++++++++ crates/rrt-runtime/src/smp.rs | 3 ++ crates/rrt-runtime/src/step.rs | 15 +++++++ crates/rrt-runtime/src/summary.rs | 31 +++++++++++++ docs/README.md | 4 +- docs/runtime-rehost-plan.md | 3 ++ 7 files changed, 133 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c605fc..f12436f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,9 @@ the shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-a stock-repurchase, stock-issue, and bond-issue branches by mutating owned company activity, dividend, company stat-post, outstanding-share, issue-calendar, and live bond-slot state instead 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. 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 52c03d0..8d4eccf 100644 --- a/crates/rrt-runtime/src/runtime.rs +++ b/crates/rrt-runtime/src/runtime.rs @@ -122,6 +122,8 @@ pub struct RuntimeCompanyMarketState { pub struct RuntimeCompanyBondSlot { pub slot_index: u32, pub principal: u32, + #[serde(default)] + pub maturity_year: u32, pub coupon_rate_raw_u32: u32, } @@ -323,6 +325,12 @@ pub struct RuntimeCompanyAnnualBondPolicyState { #[serde(default)] pub live_bond_principal_total: Option, #[serde(default)] + pub matured_live_bond_count: Option, + #[serde(default)] + pub matured_live_bond_principal_total: Option, + #[serde(default)] + pub next_live_bond_maturity_year: Option, + #[serde(default)] pub current_cash: Option, #[serde(default)] pub cash_after_full_repayment: Option, @@ -2353,6 +2361,49 @@ fn runtime_company_total_live_bond_principal(state: &RuntimeState, company_id: u ) } +fn runtime_company_matured_live_bond_count( + state: &RuntimeState, + company_id: u32, + current_year_word: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some( + market_state + .live_bond_slots + .iter() + .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) + .count() as u32, + ) +} + +fn runtime_company_matured_live_bond_principal_total( + state: &RuntimeState, + company_id: u32, + current_year_word: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + Some( + market_state + .live_bond_slots + .iter() + .filter(|slot| slot.maturity_year != 0 && slot.maturity_year <= current_year_word) + .map(|slot| slot.principal) + .sum(), + ) +} + +fn runtime_company_next_live_bond_maturity_year( + state: &RuntimeState, + company_id: u32, +) -> Option { + let market_state = state.service_state.company_market_state.get(&company_id)?; + market_state + .live_bond_slots + .iter() + .filter_map(|slot| (slot.maturity_year != 0).then_some(slot.maturity_year)) + .min() +} + pub(crate) fn runtime_company_support_adjusted_share_price_scalar_with_pressure_f64( state: &RuntimeState, company_id: u32, @@ -3184,6 +3235,11 @@ pub fn runtime_company_annual_bond_policy_state( const ISSUE_YEARS_TO_MATURITY: u32 = 30; let annual_finance_state = runtime_company_annual_finance_state(state, company_id)?; + let current_year_word = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .unwrap_or(state.calendar.year); let current_cash = runtime_company_control_transfer_stat_value_f64( state, company_id, @@ -3191,6 +3247,12 @@ pub fn runtime_company_annual_bond_policy_state( ) .and_then(runtime_round_f64_to_i64); let live_bond_principal_total = runtime_company_total_live_bond_principal(state, company_id); + let matured_live_bond_count = + runtime_company_matured_live_bond_count(state, company_id, current_year_word); + let matured_live_bond_principal_total = + runtime_company_matured_live_bond_principal_total(state, company_id, current_year_word); + let next_live_bond_maturity_year = + runtime_company_next_live_bond_maturity_year(state, company_id); let cash_after_full_repayment = current_cash .zip(live_bond_principal_total) .map(|(cash, principal)| cash - i64::from(principal)); @@ -3222,6 +3284,9 @@ pub fn runtime_company_annual_bond_policy_state( linked_transit_latch: annual_finance_state.linked_transit_latch, live_bond_count: Some(annual_finance_state.bond_count), live_bond_principal_total, + matured_live_bond_count, + matured_live_bond_principal_total, + next_live_bond_maturity_year, current_cash, cash_after_full_repayment, issue_cash_floor, @@ -7315,6 +7380,7 @@ mod tests { live_bond_slots: vec![RuntimeCompanyBondSlot { slot_index: 0, principal: 100_000, + maturity_year: 0, coupon_rate_raw_u32: 0.05f32.to_bits(), }], ..RuntimeCompanyMarketState::default() @@ -7459,11 +7525,13 @@ mod tests { RuntimeCompanyBondSlot { slot_index: 0, principal: 100_000, + maturity_year: 0, coupon_rate_raw_u32: 0.04f32.to_bits(), }, RuntimeCompanyBondSlot { slot_index: 1, principal: 300_000, + maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }, ], @@ -8215,11 +8283,13 @@ mod tests { RuntimeCompanyBondSlot { slot_index: 0, principal: 200_000, + maturity_year: 0, coupon_rate_raw_u32: 0.09f32.to_bits(), }, RuntimeCompanyBondSlot { slot_index: 1, principal: 150_000, + maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }, ], @@ -8235,6 +8305,9 @@ mod tests { runtime_company_annual_bond_policy_state(&state, 11).expect("bond policy state"); assert_eq!(bond_state.live_bond_count, Some(2)); assert_eq!(bond_state.live_bond_principal_total, Some(350_000)); + 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.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)); @@ -8356,11 +8429,13 @@ mod tests { RuntimeCompanyBondSlot { slot_index: 0, principal: 300_000, + maturity_year: 0, coupon_rate_raw_u32: 0.11f32.to_bits(), }, RuntimeCompanyBondSlot { slot_index: 1, principal: 200_000, + maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }, ], diff --git a/crates/rrt-runtime/src/smp.rs b/crates/rrt-runtime/src/smp.rs index 4b8ef6d..e2e6aa5 100644 --- a/crates/rrt-runtime/src/smp.rs +++ b/crates/rrt-runtime/src/smp.rs @@ -4180,6 +4180,7 @@ fn parse_save_company_live_bond_slots( if principal <= 0 { continue; } + let maturity_year = read_u32_at(bytes, slot_offset + 4)?; let coupon_rate_raw_u32 = read_u32_at(bytes, slot_offset + 8)?; let coupon_rate = f32::from_bits(coupon_rate_raw_u32); if !coupon_rate.is_finite() { @@ -4188,6 +4189,7 @@ fn parse_save_company_live_bond_slots( slots.push(crate::RuntimeCompanyBondSlot { slot_index: slot_index as u32, principal: principal as u32, + maturity_year, coupon_rate_raw_u32, }); } @@ -16663,6 +16665,7 @@ mod tests { assert_eq!(market_state.outstanding_shares, 20_000); assert_eq!(market_state.live_bond_slots.len(), 2); assert_eq!(market_state.live_bond_slots[0].principal, 900_000); + assert_eq!(market_state.live_bond_slots[0].maturity_year, 1894); assert_eq!( market_state.live_bond_slots[1].coupon_rate_raw_u32, 0.12f32.to_bits() diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index 3a33b56..3d251b7 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -499,6 +499,14 @@ fn service_company_annual_finance_policy( let Some(years_to_maturity) = bond_state.proposed_issue_years_to_maturity else { continue; }; + let Some(maturity_year) = state + .world_restore + .packed_year_word_raw_u16 + .map(u32::from) + .and_then(|year| year.checked_add(years_to_maturity)) + else { + continue; + }; let Some(quote_rate) = runtime_company_bond_interest_rate_quote_f64( state, company_id, @@ -544,6 +552,7 @@ fn service_company_annual_finance_policy( market_state.live_bond_slots.push(crate::RuntimeCompanyBondSlot { slot_index, principal, + maturity_year, coupon_rate_raw_u32: (quote_rate as f32).to_bits(), }); market_state.bond_count = market_state.bond_count.saturating_add(1); @@ -2671,11 +2680,13 @@ mod tests { crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 300_000, + maturity_year: 0, coupon_rate_raw_u32: 0.11f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 200_000, + maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }, ], @@ -2985,6 +2996,7 @@ mod tests { vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 500_000, + maturity_year: 1875, coupon_rate_raw_u32: 0.09f32.to_bits(), }] ); @@ -3114,6 +3126,7 @@ mod tests { live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 500_000, + maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() @@ -3268,6 +3281,7 @@ mod tests { live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 250_000, + maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() @@ -5808,6 +5822,7 @@ mod tests { live_bond_slots: vec![crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 100_000, + maturity_year: 0, coupon_rate_raw_u32: 0.05f32.to_bits(), }], ..crate::RuntimeCompanyMarketState::default() diff --git a/crates/rrt-runtime/src/summary.rs b/crates/rrt-runtime/src/summary.rs index e29fa65..b9e3510 100644 --- a/crates/rrt-runtime/src/summary.rs +++ b/crates/rrt-runtime/src/summary.rs @@ -119,6 +119,9 @@ pub struct RuntimeSummary { pub selected_company_annual_bond_linked_transit_latch: Option, pub selected_company_annual_bond_live_bond_count: Option, pub selected_company_annual_bond_live_bond_principal_total: Option, + 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_current_cash: Option, pub selected_company_annual_bond_cash_after_full_repayment: Option, pub selected_company_annual_bond_issue_cash_floor: Option, @@ -577,6 +580,18 @@ impl RuntimeSummary { selected_company_annual_bond_state .as_ref() .and_then(|bond_state| bond_state.live_bond_principal_total), + selected_company_annual_bond_matured_live_bond_count: + selected_company_annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.matured_live_bond_count), + selected_company_annual_bond_matured_live_bond_principal_total: + selected_company_annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.matured_live_bond_principal_total), + selected_company_annual_bond_next_live_bond_maturity_year: + selected_company_annual_bond_state + .as_ref() + .and_then(|bond_state| bond_state.next_live_bond_maturity_year), selected_company_annual_bond_current_cash: selected_company_annual_bond_state .as_ref() .and_then(|bond_state| bond_state.current_cash), @@ -3157,11 +3172,13 @@ mod tests { crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 200_000, + maturity_year: 0, coupon_rate_raw_u32: 0.09f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 150_000, + maturity_year: 0, coupon_rate_raw_u32: 0.08f32.to_bits(), }, ], @@ -3186,6 +3203,18 @@ mod tests { summary.selected_company_annual_bond_live_bond_principal_total, Some(350_000) ); + assert_eq!( + summary.selected_company_annual_bond_matured_live_bond_count, + Some(0) + ); + assert_eq!( + summary.selected_company_annual_bond_matured_live_bond_principal_total, + Some(0) + ); + assert_eq!( + summary.selected_company_annual_bond_next_live_bond_maturity_year, + None + ); assert_eq!( summary.selected_company_annual_bond_current_cash, Some(-400_000) @@ -3329,11 +3358,13 @@ mod tests { crate::RuntimeCompanyBondSlot { slot_index: 0, principal: 300_000, + maturity_year: 0, coupon_rate_raw_u32: 0.11f32.to_bits(), }, crate::RuntimeCompanyBondSlot { slot_index: 1, principal: 200_000, + maturity_year: 0, coupon_rate_raw_u32: 0.07f32.to_bits(), }, ], diff --git a/docs/README.md b/docs/README.md index a061e96..d83f4a9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -150,7 +150,9 @@ The highest-value next passes are now: stock-repurchase gate headlessly as another pure reader; periodic boundary service now also chooses one annual-finance action per active company and already commits the shellless creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, - stock-issue, and bond-issue branches against owned runtime state + 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 - 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 d5d6cc9..8a78620 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -247,6 +247,9 @@ the runtime selects one annual-finance action per active company and already com creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase, stock-issue, and bond-issue branches directly into owned dividend, company stat-post, 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. ## Why This Boundary