From 19143b737cea4b057a22c0b372de305721f018ce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 12:52:42 +0900 Subject: [PATCH] feat: materialize exact event-time posterior draws --- crates/event_core/src/event_time_posterior.rs | 79 +++++++++++++++++++ crates/event_core/src/lib.rs | 9 +++ .../tests/event_time_posterior_recovery.rs | 65 +++++++++++++++ docs/TRD.md | 4 + ...criterion-and-project-journey-posterior.md | 11 ++- docs/product-technical-gap-baseline.md | 5 ++ docs/research/lineage-pair-project-journey.md | 5 +- 7 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 crates/event_core/src/event_time_posterior.rs create mode 100644 crates/event_core/tests/event_time_posterior_recovery.rs diff --git a/crates/event_core/src/event_time_posterior.rs b/crates/event_core/src/event_time_posterior.rs new file mode 100644 index 000000000..b5c80bbce --- /dev/null +++ b/crates/event_core/src/event_time_posterior.rs @@ -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, +} + +/// 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 { + 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)); + } + Ok(EventTimePosteriorDraws { draws }) +} diff --git a/crates/event_core/src/lib.rs b/crates/event_core/src/lib.rs index 9ce137db7..5e31e37a3 100644 --- a/crates/event_core/src/lib.rs +++ b/crates/event_core/src/lib.rs @@ -17,6 +17,7 @@ mod confidence; mod criterion_posterior; mod error; +mod event_time_posterior; mod first_story; mod identifier; mod instance; @@ -46,6 +47,14 @@ pub use criterion_posterior::IndependentCriterionCounts; pub use criterion_posterior::fit_independent_criterion_posterior; /// Fail-closed event-ontology errors. pub use error::EventError; +/// Exact discrete event-time posterior atom. +pub use event_time_posterior::EventTimePosteriorAtom; +/// Complete canonical event-time posterior draws. +pub use event_time_posterior::EventTimePosteriorDraws; +/// Fail-closed event-time posterior errors. +pub use event_time_posterior::EventTimePosteriorError; +/// Materialize a producer-owned discrete event-time posterior exactly. +pub use event_time_posterior::materialize_event_time_posterior; /// First-story versus follow-up detection label. pub use first_story::FirstStoryLabel; /// Threshold a first-story probability into a detection label. diff --git a/crates/event_core/tests/event_time_posterior_recovery.rs b/crates/event_core/tests/event_time_posterior_recovery.rs new file mode 100644 index 000000000..5b089f024 --- /dev/null +++ b/crates/event_core/tests/event_time_posterior_recovery.rs @@ -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) + ); +} diff --git a/docs/TRD.md b/docs/TRD.md index 088397b5e..3f4430e63 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -89,6 +89,10 @@ Production Rust line and branch coverage are exactly 100%; public API rustdoc is for independently observed binary TDT link criteria. `analysis_engine` preserves exact pair identity and temporal draw arrays while fitting this Jeffreys posterior; it does not infer event dates from record order. +- `event_core::materialize_event_time_posterior` converts identified integer + posterior mass over unique event-clock atoms into canonical complete draws. + It performs no date inference, probability repair, or nearest-date fallback; + atom/mass estimation remains an owning temporal-model responsibility. - Apple Silicon acceleration uses a macOS-native Rust-owned MLX Metal service. Compose calls it through authenticated Unix-socket or host-gateway transport; Colima/Linux never claims Metal execution. Rust owns all arithmetic; diff --git a/docs/adr/0024-lineage-pair-criterion-and-project-journey-posterior.md b/docs/adr/0024-lineage-pair-criterion-and-project-journey-posterior.md index d5ec69dde..76c50155a 100644 --- a/docs/adr/0024-lineage-pair-criterion-and-project-journey-posterior.md +++ b/docs/adr/0024-lineage-pair-criterion-and-project-journey-posterior.md @@ -40,6 +40,10 @@ The Rust CHRONOS relation slice compares common event-time draws directly and publishes the complete `before`/`simultaneous`/`after` draw sequence and its posterior frequencies. It estimates no timestamp, uses no tolerance or nearest date, preserves exact ties, and cannot promote a predicted event into fact. +An owning estimator may hand the Rust core an identified discrete event-time +posterior as unique atoms with integer multiplicities. The core canonicalizes +and materializes that mass exactly; zero mass and duplicate atoms fail closed. +This operation does not estimate atom locations or posterior mass. For an independently observed binary TDT link criterion, the Rust scientific core fits the Bernoulli likelihood with Jeffreys' invariant @@ -58,9 +62,10 @@ invents the temporal model's event-time draws. branches, ties, uncertain relations, backward-edge refusal, anchor ambiguity, and hardware-receipt divergence. -The Rust CPU independent-criterion estimator and qualitative temporal-relation -posterior have deterministic synthetic exact-recovery tests. Event-time draw -generation, artifact assembly with actual MLX execution, and hardware parity +The Rust CPU independent-criterion estimator, exact event-time posterior +materializer, and qualitative temporal-relation posterior have deterministic +synthetic exact-recovery tests. Estimation of event-time atom locations and +mass, artifact assembly with estimator-bound MLX execution, and hardware parity remain unavailable until their owning implementations and receipts pass the same gates. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index cfaeb2aed..472647bb7 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -24,6 +24,11 @@ - `mlx_native_receipt` provides a macOS-only, Rust-owned MLX CPU execution probe. Its receipt proves only the stated matrix objective and cannot be reused as an Event Lineage estimator or Metal receipt. +- `event_core` now materializes producer-identified discrete event-time mass + into canonical complete draws and recovers synthetic mass exactly. Inferring + the event-time atoms/mass from admitted evidence and binding the estimator's + own MLX receipt remain open; record time and nearest-date substitution stay + prohibited. ## 2026-08-25 Event Lineage anchor contract slice diff --git a/docs/research/lineage-pair-project-journey.md b/docs/research/lineage-pair-project-journey.md index 97266a958..1d7c782c6 100644 --- a/docs/research/lineage-pair-project-journey.md +++ b/docs/research/lineage-pair-project-journey.md @@ -3,8 +3,9 @@ ## Scope This note supports ADR 0024's evidence-state and temporal-graph contract. It -also supports the implemented independent binary TDT-link criterion posterior, -but does not establish completed CHRONOS event-time inference, causal +also supports the implemented independent binary TDT-link criterion posterior +and exact materialization of an already identified discrete event-time +posterior, but does not establish completed CHRONOS atom/mass inference, causal identification, or a universal project lifecycle. ## Synthesis