diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c2e8dd..2f97d13d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `event_core` TDT tracking contracts: hypothesized track assignments, fail-closed duplicate mentions, refusal to treat a track as an instance or state transition, and computed pair precision/recall, identity-switch rate, and RMSE against known-truth assignments. - `tepp_api` naruon live loopback HTTP/1.1 listener: `serve_one` installs a read/write deadline, requires a loopback `Host`, refuses `Transfer-Encoding` and NIM/proxy credential headers, parses `knowledge_cutoff` as RFC 3339 and refuses a future cutoff, keys analysis-run idempotency by tenant plus key, and proves both analysis-run and export POSTs over a real `TcpStream`. Not a production TLS/`$PORT` service (ADR 0011). - `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target. - `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3f094947..d7349ade 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -59,4 +59,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, and typed six-clock values/uncertain intervals (PR #8) are implemented-main. PR #9 is the active-PR that replays Task 4 Allen interval algebra and bounded path-consistency reasoner work onto that protected-main temporal foundation. Superseded PRs #5 and #6 remain historical lineage only. Event ontology, PostgreSQL persistence, shared-latent topic estimation, GPU kernels, TDT/CHRONOS intelligence, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance remain later accepted-target or deployment-owned work. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, and typed six-clock values/uncertain intervals (PR #8) are implemented-main. PR #9 is the active-PR that replays Task 4 Allen interval algebra and bounded path-consistency reasoner work onto that protected-main temporal foundation. Superseded PRs #5 and #6 remain historical lineage only. Event ontology mention/instance separation is on protected main; TDT tracking pair precision/recall lives in existing `event_core` on this active PR. PostgreSQL persistence, shared-latent topic estimation, GPU kernels, remaining TDT/CHRONOS intelligence, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance remain later accepted-target or deployment-owned work. diff --git a/crates/event_core/src/error.rs b/crates/event_core/src/error.rs index 6c795fef..e5eeb344 100644 --- a/crates/event_core/src/error.rs +++ b/crates/event_core/src/error.rs @@ -20,6 +20,12 @@ pub enum EventError { UnsupportedWireVersion, /// An unknown event-role name was supplied. UnknownEventRole, + /// A TDT track assignment was treated as an event instance. + EventTrackIsNotEventInstance, + /// A TDT track assignment was treated as a state transition. + EventTrackIsNotStateTransition, + /// An unknown event-track label was supplied. + UnknownEventTrackLabel, } impl fmt::Display for EventError { @@ -32,6 +38,9 @@ impl fmt::Display for EventError { Self::InvalidWirePayload => "invalid event wire payload", Self::UnsupportedWireVersion => "unsupported event wire version", Self::UnknownEventRole => "unknown event role", + Self::EventTrackIsNotEventInstance => "event track is not an event instance", + Self::EventTrackIsNotStateTransition => "event track is not a state transition", + Self::UnknownEventTrackLabel => "unknown event track label", }; formatter.write_str(message) } @@ -65,6 +74,18 @@ mod tests { "unsupported event wire version", ), (EventError::UnknownEventRole, "unknown event role"), + ( + EventError::EventTrackIsNotEventInstance, + "event track is not an event instance", + ), + ( + EventError::EventTrackIsNotStateTransition, + "event track is not a state transition", + ), + ( + EventError::UnknownEventTrackLabel, + "unknown event track label", + ), ] { assert_eq!(error.to_string(), message); } diff --git a/crates/event_core/src/lib.rs b/crates/event_core/src/lib.rs index 25fd1022..c357ec61 100644 --- a/crates/event_core/src/lib.rs +++ b/crates/event_core/src/lib.rs @@ -4,7 +4,8 @@ //! //! TEPP separates **fallible event mentions** grounded in evidence from //! **versioned event instances** used for temporal state, multilevel membership, -//! and scientific estimation. Mentions never silently become instances. +//! and scientific estimation. Mentions never silently become instances. TDT +//! track assignments remain measurement evidence and cannot promote an instance. mod confidence; mod error; @@ -13,6 +14,7 @@ mod instance; mod mention; mod registry; mod role; +mod track; /// Finite confidence on the closed unit interval. pub use confidence::EventConfidence; @@ -34,3 +36,21 @@ pub use mention::EventMention; pub use registry::EventRegistry; /// Typed event role kind. pub use role::EventRoleKind; +/// Assignment of one mention to one hypothesized TDT track. +pub use track::EventTrackAssignment; +/// Opaque TDT track identity. +pub use track::EventTrackId; +/// TDT continue-versus-switch track label. +pub use track::EventTrackLabel; +/// Threshold a same-track probability into a continue/switch label. +pub use track::decide_track_continue; +/// Explicit refusal to treat a TDT track as an event instance. +pub use track::refuse_track_as_instance; +/// Explicit refusal to treat a TDT track as a state transition. +pub use track::refuse_track_as_transition; +/// Identity-switch rate among consecutive same-truth-track mentions. +pub use track::tracking_identity_switch_rate; +/// Precision of recovered same-track mention pairs against known truth. +pub use track::tracking_pair_precision; +/// Recall of recovered same-track mention pairs against known truth. +pub use track::tracking_pair_recall; diff --git a/crates/event_core/src/track.rs b/crates/event_core/src/track.rs new file mode 100644 index 00000000..fe8eb9d1 --- /dev/null +++ b/crates/event_core/src/track.rs @@ -0,0 +1,408 @@ +//! TDT track assignments stay distinct from instances and transitions. + +use crate::{EventConfidence, EventError, EventInstanceId, EventMentionId}; +use std::collections::{BTreeMap, BTreeSet}; + +/// Opaque TDT track identity. +/// +/// A track is a hypothesized cluster of mentions over time. It is never a +/// promoted event instance and cannot create a forward state transition. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct EventTrackId(u32); + +impl EventTrackId { + /// Reconstruct a track identity from a raw fixture or estimator label. + #[must_use] + pub const fn from_raw(raw: u32) -> Self { + Self(raw) + } + + /// Return the raw track label. + #[must_use] + pub const fn raw(self) -> u32 { + self.0 + } +} + +/// TDT continue-versus-switch label for a mention relative to the prior track. +/// +/// A continue/switch decision is tracking evidence. It is never a promoted +/// event instance and cannot create a forward state transition by itself. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EventTrackLabel { + /// The mention is scored as continuing the previous track. + Continue, + /// The mention is scored as a switch onto a different track. + Switch, +} + +impl EventTrackLabel { + /// Return the stable wire label name. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Continue => "continue", + Self::Switch => "switch", + } + } + + /// Parse a stable wire track label. + /// + /// # Errors + /// + /// Returns [`EventError::UnknownEventTrackLabel`] for unrecognized names. + pub fn from_wire_name(name: &str) -> Result { + match name { + "continue" => Ok(Self::Continue), + "switch" => Ok(Self::Switch), + _ => Err(EventError::UnknownEventTrackLabel), + } + } + + /// Return whether this label continues the previous track. + #[must_use] + pub const fn is_continue(self) -> bool { + matches!(self, Self::Continue) + } + + /// Return the binary probability target used for RMSE. + /// + /// Continue truth is `1.0`; switch truth is `0.0`. + #[must_use] + pub const fn as_probability_target(self) -> f64 { + match self { + Self::Continue => 1.0, + Self::Switch => 0.0, + } + } +} + +/// Assignment of one mention to one hypothesized TDT track. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EventTrackAssignment { + mention_id: EventMentionId, + track_id: EventTrackId, +} + +impl EventTrackAssignment { + /// Bind a mention to a hypothesized track. + #[must_use] + pub const fn new(mention_id: EventMentionId, track_id: EventTrackId) -> Self { + Self { + mention_id, + track_id, + } + } + + /// Return the assigned mention identity. + #[must_use] + pub const fn mention_id(self) -> EventMentionId { + self.mention_id + } + + /// Return the hypothesized track identity. + #[must_use] + pub const fn track_id(self) -> EventTrackId { + self.track_id + } +} + +/// Threshold a same-track probability into a continue/switch label. +/// +/// The threshold is inclusive: `probability >= threshold` continues the track. +#[must_use] +pub fn decide_track_continue( + probability: EventConfidence, + threshold: EventConfidence, +) -> EventTrackLabel { + if probability.value() >= threshold.value() { + EventTrackLabel::Continue + } else { + EventTrackLabel::Switch + } +} + +/// Explicit refusal to treat a TDT track as an event instance. +/// +/// # Errors +/// +/// Always returns [`EventError::EventTrackIsNotEventInstance`]. +pub fn refuse_track_as_instance(_track: EventTrackId) -> Result { + Err(EventError::EventTrackIsNotEventInstance) +} + +/// Explicit refusal to treat a TDT track as a state transition. +/// +/// # Errors +/// +/// Always returns [`EventError::EventTrackIsNotStateTransition`]. +pub fn refuse_track_as_transition(_track: EventTrackId) -> Result<(), EventError> { + Err(EventError::EventTrackIsNotStateTransition) +} + +/// Precision of recovered same-track mention pairs against known truth. +/// +/// # Errors +/// +/// Returns [`EventError::InvalidWirePayload`] when assignments are empty, +/// mention identities collide, lengths differ, or the recovered pair set is +/// empty. +pub fn tracking_pair_precision( + truth: &[EventTrackAssignment], + recovered: &[EventTrackAssignment], +) -> Result { + let truth_pairs = same_track_pairs(truth)?; + let recovered_pairs = same_track_pairs(recovered)?; + if truth.len() != recovered.len() { + return Err(EventError::InvalidWirePayload); + } + counted_rate( + recovered_pairs.intersection(&truth_pairs).count(), + recovered_pairs.len(), + ) +} + +/// Recall of recovered same-track mention pairs against known truth. +/// +/// # Errors +/// +/// Returns [`EventError::InvalidWirePayload`] when assignments are empty, +/// mention identities collide, lengths differ, or the truth pair set is empty. +pub fn tracking_pair_recall( + truth: &[EventTrackAssignment], + recovered: &[EventTrackAssignment], +) -> Result { + let truth_pairs = same_track_pairs(truth)?; + let recovered_pairs = same_track_pairs(recovered)?; + if truth.len() != recovered.len() { + return Err(EventError::InvalidWirePayload); + } + counted_rate( + recovered_pairs.intersection(&truth_pairs).count(), + truth_pairs.len(), + ) +} + +/// Identity-switch rate among consecutive mentions that share a truth track. +/// +/// # Errors +/// +/// Returns [`EventError::InvalidWirePayload`] when streams are empty, lengths +/// differ, mention identities collide or disagree, or no consecutive truth +/// pair stays on the same track. +pub fn tracking_identity_switch_rate( + truth: &[EventTrackAssignment], + recovered: &[EventTrackAssignment], +) -> Result { + if truth.is_empty() || truth.len() != recovered.len() { + return Err(EventError::InvalidWirePayload); + } + let truth_map = unique_assignment_map(truth)?; + let recovered_map = unique_assignment_map(recovered)?; + let mut stay_count = 0_u32; + let mut switch_count = 0_u32; + for window in truth.windows(2) { + let left = window[0].mention_id(); + let right = window[1].mention_id(); + if truth_map.get(&left) != truth_map.get(&right) { + continue; + } + stay_count += 1; + let recovered_left = recovered_map + .get(&left) + .ok_or(EventError::InvalidWirePayload)?; + let recovered_right = recovered_map + .get(&right) + .ok_or(EventError::InvalidWirePayload)?; + if recovered_left != recovered_right { + switch_count += 1; + } + } + if stay_count == 0 { + return Err(EventError::InvalidWirePayload); + } + Ok(f64::from(switch_count) / f64::from(stay_count)) +} + +fn unique_assignment_map( + assignments: &[EventTrackAssignment], +) -> Result, EventError> { + if assignments.is_empty() { + return Err(EventError::InvalidWirePayload); + } + let mut map = BTreeMap::new(); + for assignment in assignments { + if map + .insert(assignment.mention_id(), assignment.track_id()) + .is_some() + { + return Err(EventError::InvalidWirePayload); + } + } + Ok(map) +} + +fn same_track_pairs( + assignments: &[EventTrackAssignment], +) -> Result, EventError> { + let map = unique_assignment_map(assignments)?; + let mut pairs = BTreeSet::new(); + let mentions: Vec = map.keys().copied().collect(); + for (index, left) in mentions.iter().enumerate() { + for right in mentions.iter().skip(index + 1) { + if map.get(left) == map.get(right) { + pairs.insert((*left, *right)); + } + } + } + if pairs.is_empty() { + return Err(EventError::InvalidWirePayload); + } + Ok(pairs) +} + +fn counted_rate(numerator: usize, denominator: usize) -> Result { + let numerator = u32::try_from(numerator).map_err(|_| EventError::InvalidWirePayload)?; + let denominator = u32::try_from(denominator).map_err(|_| EventError::InvalidWirePayload)?; + if denominator == 0 { + return Err(EventError::InvalidWirePayload); + } + Ok(f64::from(numerator) / f64::from(denominator)) +} + +#[cfg(test)] +mod tests { + use super::{ + EventTrackAssignment, EventTrackId, EventTrackLabel, counted_rate, decide_track_continue, + refuse_track_as_instance, refuse_track_as_transition, tracking_identity_switch_rate, + tracking_pair_precision, tracking_pair_recall, + }; + use crate::{EventConfidence, EventError, EventMentionId}; + + fn assigned(mention_id: EventMentionId, track: u32) -> EventTrackAssignment { + EventTrackAssignment::new(mention_id, EventTrackId::from_raw(track)) + } + + #[test] + fn track_helpers_cover_local_branches() { + let track = EventTrackId::from_raw(3); + assert_eq!( + refuse_track_as_instance(track), + Err(EventError::EventTrackIsNotEventInstance) + ); + assert_eq!( + refuse_track_as_transition(track), + Err(EventError::EventTrackIsNotStateTransition) + ); + let high = EventConfidence::new(0.8).expect("high"); + let low = EventConfidence::new(0.2).expect("low"); + assert_eq!(decide_track_continue(high, low), EventTrackLabel::Continue); + assert_eq!(decide_track_continue(low, high), EventTrackLabel::Switch); + let left = EventMentionId::new(); + let right = EventMentionId::new(); + let truth = [assigned(left, 1), assigned(right, 1)]; + assert!((tracking_pair_precision(&truth, &truth).expect("p") - 1.0).abs() < f64::EPSILON); + assert!((tracking_pair_recall(&truth, &truth).expect("r") - 1.0).abs() < f64::EPSILON); + assert!( + (tracking_identity_switch_rate(&truth, &truth).expect("s") - 0.0).abs() < f64::EPSILON + ); + let switched = [assigned(left, 1), assigned(right, 2)]; + assert!( + (tracking_identity_switch_rate(&truth, &switched).expect("sw") - 1.0).abs() + < f64::EPSILON + ); + assert_eq!( + counted_rate(0, usize::MAX), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + counted_rate(usize::MAX, 1), + Err(EventError::InvalidWirePayload) + ); + assert_eq!(counted_rate(1, 0), Err(EventError::InvalidWirePayload)); + cover_fail_closed_assignment_streams(left, right); + } + + fn cover_fail_closed_assignment_streams(left: EventMentionId, right: EventMentionId) { + let truth = [assigned(left, 1), assigned(right, 1)]; + let switched = [assigned(left, 1), assigned(right, 2)]; + let third = EventMentionId::new(); + let fourth = EventMentionId::new(); + let three = [assigned(left, 1), assigned(right, 1), assigned(third, 2)]; + let four = [ + assigned(left, 1), + assigned(right, 1), + assigned(third, 2), + assigned(fourth, 2), + ]; + assert_eq!( + tracking_pair_precision(&truth, &[]), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&truth, &[]), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + unique_missing_recovered_switch(), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_precision(&three, &four), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_recall(&three, &four), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_recall(&truth, &switched), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&[assigned(left, 1), assigned(left, 2)], &truth), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&truth, &[assigned(left, 1), assigned(left, 1)]), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate( + &truth, + &[ + assigned(EventMentionId::new(), 1), + assigned(EventMentionId::new(), 1) + ] + ), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&[], &[]), + Err(EventError::InvalidWirePayload) + ); + assert!( + (tracking_identity_switch_rate(&three, &three).expect("changed") - 0.0).abs() + < f64::EPSILON + ); + assert_eq!( + tracking_identity_switch_rate(&[assigned(left, 1)], &[assigned(left, 1)]), + Err(EventError::InvalidWirePayload) + ); + } + + fn unique_missing_recovered_switch() -> Result { + let left = EventMentionId::new(); + let right = EventMentionId::new(); + let extra = EventMentionId::new(); + let truth = [ + EventTrackAssignment::new(left, EventTrackId::from_raw(1)), + EventTrackAssignment::new(right, EventTrackId::from_raw(1)), + ]; + let recovered = [ + EventTrackAssignment::new(left, EventTrackId::from_raw(1)), + EventTrackAssignment::new(extra, EventTrackId::from_raw(1)), + ]; + tracking_identity_switch_rate(&truth, &recovered) + } +} diff --git a/crates/event_core/tests/tracking_contract.rs b/crates/event_core/tests/tracking_contract.rs new file mode 100644 index 00000000..b22500d3 --- /dev/null +++ b/crates/event_core/tests/tracking_contract.rs @@ -0,0 +1,211 @@ +//! TDT tracks are not instances; pair P/R and switch rate come from truth. + +use event_core::{ + EventConfidence, EventError, EventMentionId, EventTrackAssignment, EventTrackId, + EventTrackLabel, decide_track_continue, refuse_track_as_instance, refuse_track_as_transition, + tracking_identity_switch_rate, tracking_pair_precision, tracking_pair_recall, +}; + +fn computed_rmse(truth: &[f64], recovered: &[f64]) -> f64 { + assert_eq!(truth.len(), recovered.len()); + let n = f64::from(u32::try_from(truth.len()).expect("tiny fixture")); + let sse: f64 = truth + .iter() + .zip(recovered) + .map(|(truth_value, recovered_value)| { + let residual = truth_value - recovered_value; + residual * residual + }) + .sum(); + (sse / n).sqrt() +} + +fn mention() -> EventMentionId { + EventMentionId::new() +} + +fn assignment(mention_id: EventMentionId, track: u32) -> EventTrackAssignment { + EventTrackAssignment::new(mention_id, EventTrackId::from_raw(track)) +} + +#[test] +fn event_track_cannot_be_cast_to_an_instance_or_transition() { + let track = EventTrackId::from_raw(1); + assert_eq!( + refuse_track_as_instance(track), + Err(EventError::EventTrackIsNotEventInstance) + ); + assert_eq!( + refuse_track_as_transition(track), + Err(EventError::EventTrackIsNotStateTransition) + ); +} + +#[test] +fn pair_precision_and_recall_are_computed_from_known_truth_assignments() { + let a = mention(); + let b = mention(); + let c = mention(); + let d = mention(); + let truth = [ + assignment(a, 1), + assignment(b, 1), + assignment(c, 2), + assignment(d, 2), + ]; + let calibrated = [ + assignment(a, 1), + assignment(b, 1), + assignment(c, 2), + assignment(d, 3), + ]; + let always_one_track = [ + assignment(a, 1), + assignment(b, 1), + assignment(c, 1), + assignment(d, 1), + ]; + + let calibrated_precision = tracking_pair_precision(&truth, &calibrated).expect("precision"); + let naive_precision = tracking_pair_precision(&truth, &always_one_track).expect("naive p"); + let calibrated_recall = tracking_pair_recall(&truth, &calibrated).expect("recall"); + let naive_recall = tracking_pair_recall(&truth, &always_one_track).expect("naive r"); + + assert!( + calibrated_precision > naive_precision, + "computed precision {calibrated_precision} must exceed always-one-track precision {naive_precision}" + ); + assert!(calibrated_recall <= naive_recall); +} + +#[test] +fn identity_switch_rate_is_lower_for_stable_tracks_than_always_switch() { + let a = mention(); + let b = mention(); + let c = mention(); + let d = mention(); + let truth = [ + assignment(a, 1), + assignment(b, 1), + assignment(c, 2), + assignment(d, 2), + ]; + let stable = truth; + let always_switch = [ + assignment(a, 1), + assignment(b, 2), + assignment(c, 3), + assignment(d, 4), + ]; + + let stable_rate = tracking_identity_switch_rate(&truth, &stable).expect("stable"); + let switch_rate = tracking_identity_switch_rate(&truth, &always_switch).expect("switch"); + assert!( + stable_rate < switch_rate, + "computed switch rate {stable_rate} must be below always-switch rate {switch_rate}" + ); +} + +#[test] +fn calibrated_same_track_scores_have_lower_rmse_than_always_one_track() { + let truth = [1.0_f64, 1.0, 0.0, 0.0, 0.0, 1.0]; + let calibrated = [0.90_f64, 0.85, 0.15, 0.10, 0.20, 0.88]; + let always_one = [1.0_f64, 1.0, 1.0, 1.0, 1.0, 1.0]; + let calibrated_rmse = computed_rmse(&truth, &calibrated); + let naive_rmse = computed_rmse(&truth, &always_one); + assert!( + calibrated_rmse < naive_rmse, + "computed calibrated RMSE {calibrated_rmse} must be below always-one-track RMSE {naive_rmse}" + ); +} + +#[test] +fn assignment_helpers_fail_closed_on_empty_mismatch_duplicate_and_missing_pairs() { + let a = mention(); + let b = mention(); + let one = [assignment(a, 1)]; + let two = [assignment(a, 1), assignment(b, 1)]; + assert_eq!( + tracking_pair_precision(&[], &[]), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_recall(&one, &two), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_precision(&one, &one), + Err(EventError::InvalidWirePayload) + ); + let duplicate = [assignment(a, 1), assignment(a, 2)]; + assert_eq!( + tracking_pair_recall(&duplicate, &duplicate), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&one, &one), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&[], &[]), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_identity_switch_rate(&two, &one), + Err(EventError::InvalidWirePayload) + ); + let a2 = mention(); + let b2 = mention(); + let c2 = mention(); + let d2 = mention(); + let three = [assignment(a2, 1), assignment(b2, 1), assignment(c2, 2)]; + let four = [ + assignment(a2, 1), + assignment(b2, 1), + assignment(c2, 2), + assignment(d2, 2), + ]; + assert_eq!( + tracking_pair_precision(&three, &four), + Err(EventError::InvalidWirePayload) + ); + assert_eq!( + tracking_pair_recall(&three, &four), + Err(EventError::InvalidWirePayload) + ); +} + +#[test] +fn labels_round_trip_and_threshold_is_inclusive() { + assert_eq!(EventTrackLabel::Continue.wire_name(), "continue"); + assert_eq!(EventTrackLabel::Switch.wire_name(), "switch"); + assert_eq!( + EventTrackLabel::from_wire_name("continue").expect("parse"), + EventTrackLabel::Continue + ); + assert_eq!( + EventTrackLabel::from_wire_name("switch").expect("parse"), + EventTrackLabel::Switch + ); + assert_eq!( + EventTrackLabel::from_wire_name("same_track"), + Err(EventError::UnknownEventTrackLabel) + ); + assert!(EventTrackLabel::Continue.is_continue()); + assert!(!EventTrackLabel::Switch.is_continue()); + assert!((EventTrackLabel::Continue.as_probability_target() - 1.0).abs() < f64::EPSILON); + assert!((EventTrackLabel::Switch.as_probability_target() - 0.0).abs() < f64::EPSILON); + + let half = EventConfidence::new(0.5).expect("half"); + assert_eq!(decide_track_continue(half, half), EventTrackLabel::Continue); + assert_eq!( + decide_track_continue(EventConfidence::new(0.49).expect("below"), half), + EventTrackLabel::Switch + ); + + let mention_id = mention(); + let assigned = assignment(mention_id, 7); + assert_eq!(assigned.mention_id(), mention_id); + assert_eq!(assigned.track_id(), EventTrackId::from_raw(7)); + assert_eq!(EventTrackId::from_raw(7).raw(), 7); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index a3e674cf..11767bf2 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | | posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | future `psychometric_core` | accepted-target | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | -| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | +| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `event_core` TDT tracking pair precision/recall and identity-switch rate on the active PR; remaining TDT/CHRONOS stack and any future `event_intelligence` crate remain accepted-target | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | `tepp_api::route_orchestration` + ablation record on the active PR; live contextual-orchestrator execution remaining | partial | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; persistence retention/deletion remaining | partial | diff --git a/docs/adr/0011-standalone-modular-msa-boundary.md b/docs/adr/0011-standalone-modular-msa-boundary.md index d545ee23..04181fb3 100644 --- a/docs/adr/0011-standalone-modular-msa-boundary.md +++ b/docs/adr/0011-standalone-modular-msa-boundary.md @@ -1,7 +1,7 @@ # ADR 0011 — Standalone operation and modular CWL MSA boundary **Decision status:** Accepted -**Implementation maturity:** partial — Rust crates are independently usable; naruon HTTP interchange and loopback live listener (`POST /v1/analysis-runs` and `/v1/exports`, fail-closed table-access, NIM/proxy headers, RFC 3339 cutoff, stream deadline) are on the active PR (not implemented-main); production TLS/`$PORT` and remaining persistence integrations remain accepted-target +**Implementation maturity:** partial — Rust crates are independently usable; naruon HTTP interchange and loopback live listener (`POST /v1/analysis-runs` and `/v1/exports`, fail-closed table-access, NIM/proxy headers, RFC 3339 cutoff, stream deadline) are on the active PR (not implemented-main); production TLS/`$PORT` and remaining persistence integrations remain accepted-target **Date:** 2026-08-10 **Supersedes:** The broad cross-service ownership wording in ADR 0001. ADR 0001 remains authoritative for Rust-first numerical architecture. diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index b85ee0b4..cd29c0a9 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -1,7 +1,7 @@ # ADR 0016 — TDT, CHRONOS, and Event Ontology intelligence boundary **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** active-PR — TDT tracking pair precision/recall, identity-switch rate, and track-versus-instance/transition refusal live in existing `event_core`; remaining TDT segmentation/first-story/link and CHRONOS schema/prediction layers remain accepted-target **Date:** 2026-08-12 **Supersedes:** None; complements ADR 0002 temporal semantics and ADR 0003 event ontology/membership. diff --git a/docs/adr/README.md b/docs/adr/README.md index 258eb7f3..57a9dfef 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,7 +21,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold on the active PR; remaining physical ERD/backup accepted-target. | | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | -| [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | +| [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | active-PR | TDT tracking pair precision/recall and identity-switch rate in existing `event_core`; remaining TDT/CHRONOS stack remains accepted-target. | ## Decision ownership summary diff --git a/docs/connectors/naruon-artifact-consumer.md b/docs/connectors/naruon-artifact-consumer.md index 2e4f4d6c..5fe0424c 100644 --- a/docs/connectors/naruon-artifact-consumer.md +++ b/docs/connectors/naruon-artifact-consumer.md @@ -1,6 +1,6 @@ # naruon modular consumer contract for TEPP artifacts -**Status:** Partial — versioned DTO, HTTP interchange, and loopback live listener on the active PR; production TLS/`$PORT` remaining +**Status:** Partial — versioned DTO, HTTP interchange, and loopback live listener on the active PR; production TLS/`$PORT` remaining **Last reviewed:** 2026-08-16 ## Boundary diff --git a/docs/research/event-tracking-calibration.md b/docs/research/event-tracking-calibration.md new file mode 100644 index 00000000..5389a5a8 --- /dev/null +++ b/docs/research/event-tracking-calibration.md @@ -0,0 +1,31 @@ +# Event-tracking calibration + +## Scope + +This note doctors the `event_core` gate that keeps TDT tracking distinct from event-instance promotion and state-transition authority: + +1. a hypothesized track assignment is measurement evidence, not a promoted instance or transition; +2. pair precision, pair recall, and identity-switch rate are computed from known-truth assignments; +3. calibrated same-track probabilities recover the binary same-track target with lower RMSE than an always-one-track detector. + +No database migration is allocated. Later TDT/CHRONOS layers may consume these scores as measurement evidence only. + +## Authoritative sources + +Allan, J., Carbonell, J., Doddington, G., Yamron, J., & Yang, Y. (1998). Topic detection and tracking pilot study: Final report. In *Proceedings of the DARPA Broadcast News Transcription and Understanding Workshop* (pp. 194–218). + +Allan, J. (Ed.). (2002). *Topic detection and tracking: Event-based information organization*. Kluwer Academic Publishers. + +Fiscus, J. G., & Doddington, G. R. (2002). Topic detection and tracking evaluation overview. In J. Allan (Ed.), *Topic detection and tracking: Event-based information organization* (pp. 17–31). Kluwer Academic Publishers. + +## Application + +Allan et al. (1998) and Allan (2002) define topic tracking as a longitudinal *same-topic / same-story* assignment task whose official evaluation reports miss, false-alarm, and tracking-cost trade-offs rather than instance identity. Fiscus and Doddington (2002) keep those tracking scores in the measurement layer. TEPP therefore refuses to cast a track assignment as an event instance or a forward state transition and requires computed pair precision, pair recall, identity-switch rate, and RMSE against known truth (Allan et al., 1998; Allan, 2002; Fiscus & Doddington, 2002). + +## Verification + +- `refuse_track_as_instance` always returns `EventTrackIsNotEventInstance`; +- `refuse_track_as_transition` always returns `EventTrackIsNotStateTransition`; +- `tracking_pair_precision` and `tracking_pair_recall` fail closed on empty, mismatched, duplicate-mention, or pairless assignment streams; +- `tracking_identity_switch_rate` fails closed when no consecutive truth pair stays on the same track; +- computed RMSE of known same-track pair targets is lower under calibrated probabilities than under an always-one-track detector. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 28e62d5c..ebdabe57 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -62,8 +62,12 @@ International Organization for Standardization. (2012). *Language resource manag Hobbs, J. R., & Pan, F. (2017). *Time ontology in OWL* (W3C Recommendation). World Wide Web Consortium. https://www.w3.org/TR/owl-time/ +Allan, J., Carbonell, J., Doddington, G., Yamron, J., & Yang, Y. (1998). Topic detection and tracking pilot study: Final report. In *Proceedings of the DARPA Broadcast News Transcription and Understanding Workshop* (pp. 194–218). + Allan, J. (Ed.). (2002). *Topic detection and tracking: Event-based information organization*. Kluwer Academic Publishers. +Fiscus, J. G., & Doddington, G. R. (2002). Topic detection and tracking evaluation overview. In J. Allan (Ed.), *Topic detection and tracking: Event-based information organization* (pp. 17–31). Kluwer Academic Publishers. + Anagnostopoulos, E., Batsakis, S., & Petrakis, E. G. M. (2013). CHRONOS: A reasoning engine for qualitative temporal information in OWL. *Procedia Computer Science, 22*, 70–77. https://doi.org/10.1016/j.procs.2013.09.082 TEPP uses interval and partial-order reasoning, bitemporal availability, leakage-safe cutoffs, TDT segmentation/link/detection/first-story/tracking tasks, and separate neural/symbolic event-schema and temporal-consistency layers. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index aae1a06e..24eb3107 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -16,6 +16,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Six-clock temporal | `temporal_core` | implemented-main | — | unit + wire | Task 3 / PR #8 | | Allen path-consistency | `temporal_core` | implemented-main | — | unit + budget tests | Task 4 / PR #9 | | Event mention/instance | `event_core` | partial | — | unit + fail-closed promotion | Task 5 / PR #13 | +| TDT tracking stability | `event_core` | active-PR | this PR | pair P/R + switch rate + RMSE vs always-one-track | ADR 0016; `docs/research/event-tracking-calibration.md` | | Multiple membership | `membership_core` | partial | — | unit + ESS weights | Task 7 / PR #12 + #25 | | Forward transition DAG | `relation_graph` | implemented-main | — | unit + cycle rejection | Task 6 / PR #14 | | Bitemporal persistence + live SQL port | `persistence_postgres` | partial | backup/restore integrity | migration contracts + recording transport + optional PgPool + live CI + tenant RLS + `0005`/`0006` + event relation/mention/instance + source-artifact + audit-event + concurrent-write (#37–#43 implemented-main) + restore integrity probes (active PR) | Task 8 / PR #16 + #23 + #26 + #27 + #29 + #30–#43 + restore integrity |