Repository navigation
feat: materialize exact CHRONOS event-time posterior draws #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| //! Exact event-time draws from a producer-owned discrete posterior measure. | ||
| //! | ||
| //! This module materializes, but does not invent, a CHRONOS event-time | ||
| //! posterior. The owning temporal estimator supplies integer posterior mass | ||
| //! over identified event-time atoms. Integer mass avoids consumer-selected | ||
| //! probability tolerances and makes every emitted draw auditable. | ||
|
|
||
| use temporal_core::EventTime; | ||
|
|
||
| /// One identified event-time atom and its exact posterior multiplicity. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub struct EventTimePosteriorAtom { | ||
| /// Event-clock instant, distinct from document and availability clocks. | ||
| pub event_time: EventTime, | ||
| /// Exact number of posterior draws assigned to this atom. | ||
| pub multiplicity: u32, | ||
| } | ||
|
|
||
| /// A complete discrete event-time posterior draw set. | ||
| #[derive(Clone, Debug, Eq, PartialEq)] | ||
| pub struct EventTimePosteriorDraws { | ||
| /// Draws in canonical event-time order, preserving exact ties. | ||
| pub draws: Vec<EventTime>, | ||
| } | ||
|
|
||
| /// Fail-closed event-time posterior materialization errors. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub enum EventTimePosteriorError { | ||
| /// No posterior atom was supplied. | ||
| EmptyPosterior, | ||
| /// An atom carried zero posterior mass. | ||
| ZeroMassAtom, | ||
| /// Two atoms referred to the same event-clock instant. | ||
| DuplicateEventTime, | ||
| /// Total posterior multiplicity overflowed the supported address space. | ||
| DrawCountOverflow, | ||
| } | ||
|
|
||
| /// Materialize exact event-time draws from identified integer posterior mass. | ||
| /// | ||
| /// Atoms are sorted by event time so producer input order cannot change the | ||
| /// canonical draw digest. Equal instants remain repeated draws, representing | ||
| /// posterior mass and exact simultaneity rather than being collapsed. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Fails closed for empty, zero-mass, duplicate, or overflowing input. | ||
| pub fn materialize_event_time_posterior( | ||
| atoms: &[EventTimePosteriorAtom], | ||
| ) -> Result<EventTimePosteriorDraws, EventTimePosteriorError> { | ||
| if atoms.is_empty() { | ||
| return Err(EventTimePosteriorError::EmptyPosterior); | ||
| } | ||
| if atoms.iter().any(|atom| atom.multiplicity == 0) { | ||
| return Err(EventTimePosteriorError::ZeroMassAtom); | ||
| } | ||
| let mut ordered = atoms.to_vec(); | ||
| ordered.sort_unstable_by_key(|atom| atom.event_time); | ||
| if ordered | ||
| .windows(2) | ||
| .any(|pair| pair[0].event_time == pair[1].event_time) | ||
| { | ||
| return Err(EventTimePosteriorError::DuplicateEventTime); | ||
| } | ||
| let draw_count = ordered.iter().try_fold(0_usize, |total, atom| { | ||
| let multiplicity = usize::try_from(atom.multiplicity) | ||
| .map_err(|_| EventTimePosteriorError::DrawCountOverflow)?; | ||
| total | ||
| .checked_add(multiplicity) | ||
| .ok_or(EventTimePosteriorError::DrawCountOverflow) | ||
| })?; | ||
| let mut draws = Vec::with_capacity(draw_count); | ||
| for atom in ordered { | ||
| let multiplicity = usize::try_from(atom.multiplicity) | ||
| .map_err(|_| EventTimePosteriorError::DrawCountOverflow)?; | ||
| draws.extend(std::iter::repeat_n(atom.event_time, multiplicity)); | ||
|
Comment on lines
+65
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Overflow error branches are unreachable and untested The Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
| Ok(EventTimePosteriorDraws { draws }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| //! Synthetic recovery tests for exact CHRONOS event-time posterior draws. | ||
|
|
||
| use event_core::{ | ||
| EventTimePosteriorAtom, EventTimePosteriorError, materialize_event_time_posterior, | ||
| }; | ||
| use temporal_core::EventTime; | ||
|
|
||
| fn event_time(value: &str) -> EventTime { | ||
| EventTime::parse_rfc3339(value).expect("synthetic event time must parse") | ||
| } | ||
|
|
||
| #[test] | ||
| fn recovers_exact_posterior_mass_and_canonical_order() { | ||
| let early = event_time("2026-01-01T00:00:00Z"); | ||
| let late = event_time("2026-01-03T00:00:00Z"); | ||
| let posterior = materialize_event_time_posterior(&[ | ||
| EventTimePosteriorAtom { | ||
| event_time: late, | ||
| multiplicity: 1, | ||
| }, | ||
| EventTimePosteriorAtom { | ||
| event_time: early, | ||
| multiplicity: 3, | ||
| }, | ||
| ]) | ||
| .expect("identified posterior must materialize"); | ||
| assert_eq!(posterior.draws, vec![early, early, early, late]); | ||
| assert_eq!( | ||
| posterior | ||
| .draws | ||
| .iter() | ||
| .filter(|draw| **draw == early) | ||
| .count(), | ||
| 3 | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn refuses_unidentified_or_noncanonical_mass() { | ||
| let instant = event_time("2026-01-01T00:00:00Z"); | ||
| assert_eq!( | ||
| materialize_event_time_posterior(&[]), | ||
| Err(EventTimePosteriorError::EmptyPosterior) | ||
| ); | ||
| assert_eq!( | ||
| materialize_event_time_posterior(&[EventTimePosteriorAtom { | ||
| event_time: instant, | ||
| multiplicity: 0, | ||
| }]), | ||
| Err(EventTimePosteriorError::ZeroMassAtom) | ||
| ); | ||
| assert_eq!( | ||
| materialize_event_time_posterior(&[ | ||
| EventTimePosteriorAtom { | ||
| event_time: instant, | ||
| multiplicity: 1, | ||
| }, | ||
| EventTimePosteriorAtom { | ||
| event_time: instant, | ||
| multiplicity: 1, | ||
| }, | ||
| ]), | ||
| Err(EventTimePosteriorError::DuplicateEventTime) | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Unstable sort is safe given duplicate rejection
sort_unstable_by_keyis non-stable, but duplicateevent_timevalues are rejected immediately after (event_time_posterior.rs:59-64), so no equal keys survive and the canonical order stays deterministic.Was this helpful? React with 👍 or 👎 to provide feedback.