Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang
- `event_core` now requires and retains `EventEvidenceLayer::PromotedTransition` when constructing an `EventInstance`; every other layer is rejected at the promotion boundary, and TDT story classification uses a caller-owned hash set for expected constant-time membership checks.
- `event_core` ADR 0016 evidence-status gates: TDT detections and CHRONOS predictions cannot admit a forward state transition; first-story detection scores miss/false-alarm rates against a known story stream (Allan 2002 task).
- `membership_target` identity gate: language, episode, template, department, and opportunity-pool memberships cannot collapse into the entity/project pair stored by migration `0006`; comparison-contract tests record recovered target kinds against an entity-collapse baseline (ADR 0003).

- `event_core` TDT link-detection contracts: undirected mention-pair hypotheses, fail-closed self-links, refusal to treat a detected link as an instance or state transition, and computed precision/recall plus RMSE against known-truth pairs.
- `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.
Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) |
| Mention-confidence Brier doctoring | [`docs/research/mention-confidence-brier.md`](docs/research/mention-confidence-brier.md) |
| Event-intelligence status-gate doctoring | [`docs/research/event-intelligence-status-gates.md`](docs/research/event-intelligence-status-gates.md) |
| TDT link-detection precision/recall doctoring | [`docs/research/event-link-detection-calibration.md`](docs/research/event-link-detection-calibration.md) |
| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) |
| Stopword-deletion doctoring | [`docs/research/stopword-deletion.md`](docs/research/stopword-deletion.md) |
| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) |
Expand Down
21 changes: 21 additions & 0 deletions crates/event_core/src/error.rs

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Pre-existing fused error-message tuple

The UnknownFirstStoryLabel entry is already fused with EventTrackIsNotEventInstance into a four-element tuple in the merge-base, the same defect as the newly introduced one. The test array was already structurally broken; fixing it in the same pass would restore both message assertions.

(Refers to this code)

Open in Devin Review

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

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ pub enum EventError {
DetectionIsNotTransition,
/// A CHRONOS prediction was treated as an observed or promoted fact.
PredictionIsNotFact,
/// A TDT link detection was treated as an event instance.
EventLinkIsNotEventInstance,
/// A TDT link detection was treated as a state transition.
EventLinkIsNotStateTransition,
/// An unknown event-link label was supplied.
UnknownEventLinkLabel,
}

impl fmt::Display for EventError {
Expand All @@ -38,6 +44,9 @@ impl fmt::Display for EventError {
Self::UnknownEventRole => "unknown event role",
Self::DetectionIsNotTransition => "detection is not a state transition",
Self::PredictionIsNotFact => "prediction is not an observed fact",
Self::EventLinkIsNotEventInstance => "event link is not an event instance",
Self::EventLinkIsNotStateTransition => "event link is not a state transition",
Self::UnknownEventLinkLabel => "unknown event link label",
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -79,6 +88,18 @@ mod tests {
EventError::PredictionIsNotFact,
"prediction is not an observed fact",
),
(
EventError::EventLinkIsNotEventInstance,
"event link is not an event instance",
),
(
EventError::EventLinkIsNotStateTransition,
"event link is not a state transition",
),
(
EventError::UnknownEventLinkLabel,
"unknown event link label",
Comment on lines +146 to +148

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Link-label error test fused with first-story case

The added UnknownEventLinkLabel test entry never closes its tuple, so it absorbs the following FirstStoryIsNotEventInstance entry into one four-element tuple. The array now mixes tuple shapes and the message checks for both error variants no longer run.

Suggested change
(
EventError::UnknownEventLinkLabel,
"unknown event link label",
(
EventError::UnknownEventLinkLabel,
"unknown event link label",
),
(
Open in Devin Review

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

),
] {
assert_eq!(error.to_string(), message);
}
Expand Down
15 changes: 15 additions & 0 deletions crates/event_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod error;
mod identifier;
mod instance;
mod intelligence;
mod link;
mod mention;
mod registry;
mod role;
Expand Down Expand Up @@ -45,6 +46,20 @@ pub use intelligence::admit_state_transition;
pub use intelligence::classify_tdt_story;
/// Score first-story detections against a known stream.
pub use intelligence::first_story_detection_rates;
/// TDT same-event versus distinct-event link label.
pub use link::EventLinkLabel;
/// Undirected TDT link hypothesis between two mentions.
pub use link::EventLinkPair;
/// Threshold a link probability into a detection label.
pub use link::decide_event_link;
/// Precision of recovered TDT links against known-truth pairs.
pub use link::event_link_precision;
/// Recall of recovered TDT links against known-truth pairs.
pub use link::event_link_recall;
/// Explicit refusal to treat a TDT link as an event instance.
pub use link::refuse_event_link_as_instance;
/// Explicit refusal to treat a TDT link as a state transition.
pub use link::refuse_event_link_as_transition;
/// Fallible textual event mention.
pub use mention::EventMention;
/// In-memory registry separating mentions from instances.
Expand Down
223 changes: 223 additions & 0 deletions crates/event_core/src/link.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! TDT link-detection scores stay distinct from instances and transitions.

use crate::{EventConfidence, EventError, EventInstanceId, EventMentionId};
use std::collections::BTreeSet;

/// TDT same-event versus distinct-event link label.
///
/// A link decision is detection 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 EventLinkLabel {
/// The mention pair is scored as the same event or story.
Linked,
/// The mention pair is scored as distinct events.
Unlinked,
}

impl EventLinkLabel {
/// Return the stable wire label name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Linked => "linked",
Self::Unlinked => "unlinked",
}
}

/// Parse a stable wire link label.
///
/// # Errors
///
/// Returns [`EventError::UnknownEventLinkLabel`] for unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, EventError> {
match name {
"linked" => Ok(Self::Linked),
"unlinked" => Ok(Self::Unlinked),
_ => Err(EventError::UnknownEventLinkLabel),
}
}

/// Return whether this label is a positive link detection.
#[must_use]
pub const fn is_linked(self) -> bool {
matches!(self, Self::Linked)
}

/// Return the binary probability target used for RMSE.
///
/// Linked truth is `1.0`; unlinked truth is `0.0`.
#[must_use]
pub const fn as_probability_target(self) -> f64 {
match self {
Self::Linked => 1.0,
Self::Unlinked => 0.0,
}
}
}

/// An undirected TDT link hypothesis between two mentions.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EventLinkPair {
left: EventMentionId,
right: EventMentionId,
}

impl EventLinkPair {
/// Construct a normalized undirected mention pair.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when both mentions are the
/// same identity. A mention cannot link to itself.
pub fn new(left: EventMentionId, right: EventMentionId) -> Result<Self, EventError> {
if left == right {
return Err(EventError::InvalidWirePayload);
}
if left <= right {
Ok(Self { left, right })
} else {
Ok(Self {
left: right,
right: left,
})
}
}

/// Return the lexicographically smaller mention identifier.
#[must_use]
pub const fn left(self) -> EventMentionId {
self.left
}

/// Return the lexicographically larger mention identifier.
#[must_use]
pub const fn right(self) -> EventMentionId {
self.right
}
}

/// Threshold a link probability into a detection label.
///
/// The threshold is inclusive: `probability >= threshold` is linked.
#[must_use]
pub fn decide_event_link(
probability: EventConfidence,
threshold: EventConfidence,
) -> EventLinkLabel {
if probability.value() >= threshold.value() {
EventLinkLabel::Linked
} else {
EventLinkLabel::Unlinked
}
}

/// Explicit refusal to treat a TDT link as an event instance.
///
/// # Errors
///
/// Always returns [`EventError::EventLinkIsNotEventInstance`].
pub fn refuse_event_link_as_instance(_link: EventLinkPair) -> Result<EventInstanceId, EventError> {
Err(EventError::EventLinkIsNotEventInstance)
}

/// Explicit refusal to treat a TDT link as a state transition.
///
/// # Errors
///
/// Always returns [`EventError::EventLinkIsNotStateTransition`].
pub fn refuse_event_link_as_transition(_link: EventLinkPair) -> Result<(), EventError> {
Err(EventError::EventLinkIsNotStateTransition)
}

/// Precision of recovered TDT links against the known-truth pair set.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when the recovered set is empty.
pub fn event_link_precision(
truth: &[EventLinkPair],
recovered: &[EventLinkPair],
) -> Result<f64, EventError> {
let truth_set: BTreeSet<_> = truth.iter().copied().collect();
let recovered_set: BTreeSet<_> = recovered.iter().copied().collect();
counted_rate(
recovered_set.intersection(&truth_set).count(),
recovered_set.len(),
)
}
Comment thread
seonghobae marked this conversation as resolved.

/// Recall of recovered TDT links against the known-truth pair set.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when the truth set is empty.
pub fn event_link_recall(
truth: &[EventLinkPair],
recovered: &[EventLinkPair],
) -> Result<f64, EventError> {
let truth_set: BTreeSet<_> = truth.iter().copied().collect();
let recovered_set: BTreeSet<_> = recovered.iter().copied().collect();
counted_rate(
recovered_set.intersection(&truth_set).count(),
truth_set.len(),
)
Comment thread
seonghobae marked this conversation as resolved.
}

fn counted_rate(numerator: usize, denominator: usize) -> Result<f64, EventError> {
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))
}
Comment thread
seonghobae marked this conversation as resolved.
Comment on lines +167 to +174

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: Empty-set precision/recall fails closed by design

event_link_precision errors on empty recovered set and event_link_recall errors on empty truth set, via the denominator==0 guard in counted_rate. This is a deliberate fail-closed choice matching the doctoring note and tests, not a defect.

Open in Devin Review

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


#[cfg(test)]
mod tests {
use super::{
EventLinkLabel, EventLinkPair, counted_rate, decide_event_link, event_link_precision,
event_link_recall, refuse_event_link_as_instance, refuse_event_link_as_transition,
};
use crate::{EventConfidence, EventError, EventMentionId};

#[test]
fn link_helpers_cover_local_branches() {
let left = EventMentionId::new();
let right = EventMentionId::new();
let link = EventLinkPair::new(left, right).expect("pair");
assert_eq!(
refuse_event_link_as_instance(link),
Err(EventError::EventLinkIsNotEventInstance)
);
assert_eq!(
refuse_event_link_as_transition(link),
Err(EventError::EventLinkIsNotStateTransition)
);
let high = EventConfidence::new(0.8).expect("high");
let low = EventConfidence::new(0.2).expect("low");
assert_eq!(decide_event_link(high, low), EventLinkLabel::Linked);
assert_eq!(decide_event_link(low, high), EventLinkLabel::Unlinked);
let truth = [link];
let recovered = [link];
assert!((event_link_precision(&truth, &recovered).expect("p") - 1.0).abs() < f64::EPSILON);
assert!((event_link_recall(&truth, &recovered).expect("r") - 1.0).abs() < f64::EPSILON);
assert_eq!(
EventLinkPair::new(left, left),
Err(EventError::InvalidWirePayload)
);
assert_eq!(
event_link_precision(&truth, &[]),
Err(EventError::InvalidWirePayload)
);
assert_eq!(
event_link_recall(&[], &recovered),
Err(EventError::InvalidWirePayload)
);
assert_eq!(
counted_rate(0, usize::MAX),
Err(EventError::InvalidWirePayload)
);
assert_eq!(counted_rate(1, 0), Err(EventError::InvalidWirePayload));
}
}
Loading
Loading