Run periodic boundary service on year rollover

This commit is contained in:
Jan Petykiewicz 2026-04-18 06:07:44 -07:00
commit ef2c317b6b
4 changed files with 97 additions and 8 deletions

View file

@ -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 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, 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. 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 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. 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 The same save-native live bond-slot surface now also carries per-slot maturity years all the way

View file

@ -122,10 +122,15 @@ pub fn execute_step_command(
let mut boundary_events = Vec::new(); let mut boundary_events = Vec::new();
let mut service_events = Vec::new(); let mut service_events = Vec::new();
let steps_executed = match command { let steps_executed = match command {
StepCommand::AdvanceTo { calendar } => { StepCommand::AdvanceTo { calendar } => advance_to_target_calendar_point(
advance_to_target_calendar_point(state, *calendar, &mut boundary_events)? 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 } => { StepCommand::ServiceTriggerKind { trigger_kind } => {
service_trigger_kind(state, *trigger_kind, &mut service_events)?; service_trigger_kind(state, *trigger_kind, &mut service_events)?;
0 0
@ -151,6 +156,7 @@ fn advance_to_target_calendar_point(
state: &mut RuntimeState, state: &mut RuntimeState,
target: crate::CalendarPoint, target: crate::CalendarPoint,
boundary_events: &mut Vec<BoundaryEvent>, boundary_events: &mut Vec<BoundaryEvent>,
service_events: &mut Vec<ServiceEvent>,
) -> Result<u64, String> { ) -> Result<u64, String> {
target.validate()?; target.validate()?;
if target < state.calendar { if target < state.calendar {
@ -162,7 +168,7 @@ fn advance_to_target_calendar_point(
let mut steps = 0_u64; let mut steps = 0_u64;
while state.calendar < target { while state.calendar < target {
step_once(state, boundary_events); step_once(state, boundary_events, service_events)?;
steps += 1; steps += 1;
} }
Ok(steps) Ok(steps)
@ -172,14 +178,19 @@ fn step_count(
state: &mut RuntimeState, state: &mut RuntimeState,
steps: u32, steps: u32,
boundary_events: &mut Vec<BoundaryEvent>, boundary_events: &mut Vec<BoundaryEvent>,
) -> u64 { service_events: &mut Vec<ServiceEvent>,
) -> Result<u64, String> {
for _ in 0..steps { 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<BoundaryEvent>) { fn step_once(
state: &mut RuntimeState,
boundary_events: &mut Vec<BoundaryEvent>,
service_events: &mut Vec<ServiceEvent>,
) -> Result<(), String> {
let boundary = state.calendar.step_forward(); let boundary = state.calendar.step_forward();
if boundary != BoundaryEventKind::Tick { if boundary != BoundaryEventKind::Tick {
boundary_events.push(BoundaryEvent { boundary_events.push(BoundaryEvent {
@ -187,6 +198,10 @@ fn step_once(state: &mut RuntimeState, boundary_events: &mut Vec<BoundaryEvent>)
calendar: state.calendar, calendar: state.calendar,
}); });
} }
if boundary == BoundaryEventKind::YearRollover {
service_periodic_boundary(state, service_events)?;
}
Ok(())
} }
fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str { fn boundary_kind_label(boundary: BoundaryEventKind) -> &'static str {
@ -2909,6 +2924,70 @@ mod tests {
assert_eq!(state.calendar.tick_slot, 5); 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] #[test]
fn rejects_backward_target() { fn rejects_backward_target() {
let mut state = state(); let mut state = state();

View file

@ -163,6 +163,9 @@ The highest-value next passes are now:
onto the exact debt headline selectors `2882..2886` onto the exact debt headline selectors `2882..2886`
while persisting the last emitted annual-finance news events as structured runtime-owned records 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 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 - 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 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 instead of guessing another derived leaf field from neighboring raw offsets

View file

@ -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, creditor-pressure-bankruptcy, deep-distress-bankruptcy, dividend-adjustment, stock-repurchase,
stock-issue, and bond-issue branches directly into owned dividend, company stat-post, stock-issue, and bond-issue branches directly into owned dividend, company stat-post,
outstanding-share, issue-calendar, live bond-slot, and company activity state. 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 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. 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, The same owned live bond-slot surface now also carries maturity years through save import,