Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions crates/event_core/src/event_time_posterior.rs
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);
}
Comment on lines +57 to +64

Copy link
Copy Markdown

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_key is non-stable, but duplicate event_time values are rejected immediately after (event_time_posterior.rs:59-64), so no equal keys survive and the canonical order stays deterministic.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Overflow error branches are unreachable and untested

The DrawCountOverflow paths fire only if usize::try_from(u32) fails or checked_add overflows usize, neither of which can happen on a 64-bit target. No test exercises them, and the second try_from at line 74 merely repeats the check already done in the try_fold. The mandated 100% line and branch coverage gate cannot pass with these dead branches.

Prompt for agents
The DrawCountOverflow error variant in materialize_event_time_posterior is effectively unreachable on 64-bit targets: usize::try_from(u32) always succeeds and checked_add cannot realistically overflow usize, so the map_err/ok_or error arms at lines 66-70 and 74-75 are never taken. The repo requires exactly 100% line and branch coverage (AGENTS.md rule 8, CONTRIBUTING.md), which these dead branches prevent. Additionally the second usize::try_from at line 74 duplicates the conversion already performed in the try_fold. Consider either: (a) removing the redundant second conversion by summing multiplicities as u32 with checked_add so overflow is reachable and testable on all targets, or (b) restructuring so the overflow condition can be triggered by a synthetic test, or (c) removing the unreachable variant entirely if it truly cannot occur. Ensure the final design keeps the coverage gate satisfiable.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
Ok(EventTimePosteriorDraws { draws })
}
9 changes: 9 additions & 0 deletions crates/event_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
mod confidence;
mod criterion_posterior;
mod error;
mod event_time_posterior;
mod first_story;
mod identifier;
mod instance;
Expand Down Expand Up @@ -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.
Expand Down
65 changes: 65 additions & 0 deletions crates/event_core/tests/event_time_posterior_recovery.rs
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)
);
}
4 changes: 4 additions & 0 deletions docs/TRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions docs/research/lineage-pair-project-journey.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading