diff --git a/README.md b/README.md index 6f9fe13..eaa560b 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,9 @@ dividend, company stat-post, outstanding-share, issue-calendar, and live bond-sl of stopping at reader-only diagnostics. That same service state now also persists the last emitted annual-finance news events as structured runtime records carrying company id, exact selector label, action label, and the grounded debt/share payload totals used by the shell news layer. +Calendar stepping now also starts to use that same seam directly: `StepCount` and `AdvanceTo` +invoke the periodic-boundary service automatically on year rollover, so shellless calendar advance +can drive the annual finance stack instead of requiring a separate manual service command. Those bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy year and halve live bond principals in place instead of treating bankruptcy as a liquidation path. The same save-native live bond-slot surface now also carries per-slot maturity years all the way diff --git a/crates/rrt-runtime/src/step.rs b/crates/rrt-runtime/src/step.rs index f3d900d..b427df4 100644 --- a/crates/rrt-runtime/src/step.rs +++ b/crates/rrt-runtime/src/step.rs @@ -122,10 +122,15 @@ pub fn execute_step_command( let mut boundary_events = Vec::new(); let mut service_events = Vec::new(); let steps_executed = match command { - StepCommand::AdvanceTo { calendar } => { - advance_to_target_calendar_point(state, *calendar, &mut boundary_events)? + StepCommand::AdvanceTo { calendar } => advance_to_target_calendar_point( + state, + *calendar, + &mut boundary_events, + &mut service_events, + )?, + StepCommand::StepCount { steps } => { + step_count(state, *steps, &mut boundary_events, &mut service_events)? } - StepCommand::StepCount { steps } => step_count(state, *steps, &mut boundary_events), StepCommand::ServiceTriggerKind { trigger_kind } => { service_trigger_kind(state, *trigger_kind, &mut service_events)?; 0 @@ -151,6 +156,7 @@ fn advance_to_target_calendar_point( state: &mut RuntimeState, target: crate::CalendarPoint, boundary_events: &mut Vec, + service_events: &mut Vec, ) -> Result { target.validate()?; if target < state.calendar { @@ -162,7 +168,7 @@ fn advance_to_target_calendar_point( let mut steps = 0_u64; while state.calendar < target { - step_once(state, boundary_events); + step_once(state, boundary_events, service_events)?; steps += 1; } Ok(steps) @@ -172,14 +178,19 @@ fn step_count( state: &mut RuntimeState, steps: u32, boundary_events: &mut Vec, -) -> u64 { + service_events: &mut Vec, +) -> Result { for _ in 0..steps { - step_once(state, boundary_events); + step_once(state, boundary_events, service_events)?; } - steps.into() + Ok(steps.into()) } -fn step_once(state: &mut RuntimeState, boundary_events: &mut Vec) { +fn step_once( + state: &mut RuntimeState, + boundary_events: &mut Vec, + service_events: &mut Vec, +) -> Result<(), String> { let boundary = state.calendar.step_forward(); if boundary != BoundaryEventKind::Tick { boundary_events.push(BoundaryEvent { @@ -187,6 +198,10 @@ fn step_once(state: &mut RuntimeState, boundary_events: &mut Vec) calendar: state.calendar, }); } + if boundary == BoundaryEventKind::YearRollover { + service_periodic_boundary(state, service_events)?; + } + Ok(()) } fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { @@ -2909,6 +2924,70 @@ mod tests { assert_eq!(state.calendar.tick_slot, 5); } + #[test] + fn year_rollover_step_runs_periodic_boundary_services() { + let mut state = RuntimeState { + calendar: CalendarPoint { + year: 1830, + month_slot: crate::MONTH_SLOTS_PER_YEAR - 1, + phase_slot: crate::PHASE_SLOTS_PER_MONTH - 1, + tick_slot: crate::TICKS_PER_PHASE - 1, + }, + event_runtime_records: vec![RuntimeEventRecord { + record_id: 77, + trigger_kind: 1, + active: true, + service_count: 0, + marks_collection_dirty: false, + one_shot: false, + has_fired: false, + conditions: Vec::new(), + effects: vec![RuntimeEffect::SetWorldFlag { + key: "world.periodic_rollover_service_fired".to_string(), + value: true, + }], + }], + ..state() + }; + + let result = execute_step_command(&mut state, &StepCommand::StepCount { steps: 1 }) + .expect("year rollover step should run periodic boundary services"); + + assert_eq!(result.steps_executed, 1); + assert_eq!( + result.boundary_events, + vec![BoundaryEvent { + kind: "year_rollover".to_string(), + calendar: CalendarPoint { + year: 1831, + month_slot: 0, + phase_slot: 0, + tick_slot: 0, + }, + }] + ); + assert_eq!(state.service_state.periodic_boundary_calls, 1); + assert_eq!(state.service_state.total_event_record_services, 1); + assert_eq!( + state + .world_flags + .get("world.periodic_rollover_service_fired"), + Some(&true) + ); + assert!( + result + .service_events + .iter() + .any(|event| event.trigger_kind == Some(1)) + ); + assert!( + result + .service_events + .iter() + .any(|event| event.kind == "annual_finance_policy") + ); + } + #[test] fn rejects_backward_target() { let mut state = state(); diff --git a/docs/README.md b/docs/README.md index e29695d..b1f1b8c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -163,6 +163,9 @@ The highest-value next passes are now: onto the exact debt headline selectors `2882..2886` while persisting the last emitted annual-finance news events as structured runtime-owned records carrying company id, selector label, action label, and the grounded debt/share payload totals +- shellless calendar advance now also drives that annual seam directly: `StepCount` and `AdvanceTo` + invoke periodic-boundary service automatically on year rollover instead of requiring a second + manual service pass to make the annual finance stack run - 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 89a1d4c..d10795c 100644 --- a/docs/runtime-rehost-plan.md +++ b/docs/runtime-rehost-plan.md @@ -256,6 +256,10 @@ 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. +Shellless calendar advance now also starts to consume that service seam directly: `StepCount` and +`AdvanceTo` invoke periodic-boundary service automatically on year rollover, so annual finance +state can advance as the runtime clock advances instead of only through an explicit manual service +command. The bankruptcy branches now follow the grounded owner semantics too: they stamp the bankruptcy year and halve live bond principals in place instead of collapsing into a liquidation-only path. The same owned live bond-slot surface now also carries maturity years through save import,