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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang
- `prompt_source` identity gate: instruction and prompt boilerplate is not unique latent content and is not erased by a stopword list; `identity_recovery_rate` reports exact kind matches, with a contract test comparing correct recovery with an all-unique collapse on a mixed known-truth fixture (ADR 0004/0012).
- `location_membership` identity gate: geographic and market assignments are time-varying memberships, not permanent entity identity and not language channels; recovered location kinds match known truth at a higher computed rate than collapsing every assignment to entity identity (ADR 0003).
- `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` first-story detection gate: first-story versus follow-up labels stay distinct from promoted instances, false-alarm and miss rates are computed from known truth, and calibrated detection scores recover the binary first-story target with lower RMSE than an always-first detector.
- `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 @@ -38,6 +38,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) |
| First-story detection FAR/miss doctoring | [`docs/research/first-story-detection-calibration.md`](docs/research/first-story-detection-calibration.md) |
| VRAM budget / GPU fallback doctoring | [`docs/research/vram-budget-types.md`](docs/research/vram-budget-types.md) |
| Causal-identification gate doctoring | [`docs/research/causal-identification-gate.md`](docs/research/causal-identification-gate.md) |
| TDT story-segmentation `WindowDiff`/`Pk` doctoring | [`docs/research/tdt-story-segmentation.md`](docs/research/tdt-story-segmentation.md) |
Expand Down
12 changes: 12 additions & 0 deletions crates/event_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub enum EventError {
DetectionIsNotTransition,
/// A CHRONOS prediction was treated as an observed or promoted fact.
PredictionIsNotFact,
/// A first-story detection was treated as an event instance.
FirstStoryIsNotEventInstance,
/// An unknown first-story label name was supplied.
UnknownFirstStoryLabel,
/// A TDT track assignment was treated as an event instance.
EventTrackIsNotEventInstance,
/// A TDT track assignment was treated as a state transition.
Expand Down Expand Up @@ -60,6 +64,8 @@ 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::FirstStoryIsNotEventInstance => "first-story detection is not an event instance",
Self::UnknownFirstStoryLabel => "unknown first-story label",
Self::EventTrackIsNotEventInstance => "event track is not an event instance",
Self::EventTrackIsNotStateTransition => "event track is not a state transition",
Self::UnknownEventTrackLabel => "unknown event track label",
Expand Down Expand Up @@ -121,6 +127,12 @@ mod tests {
"prediction is not an observed fact",
),
(
EventError::FirstStoryIsNotEventInstance,
"first-story detection is not an event instance",
),
(
EventError::UnknownFirstStoryLabel,
"unknown first-story label",
Comment on lines +133 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Merged test tuple drops two error assertions

The UnknownFirstStoryLabel test case is missing its closing paren and the reopening paren for the next case, so it fuses with EventTrackIsNotEventInstance into a single four-element tuple. The two variants are no longer asserted separately, and the array of two-element tuples no longer type-checks.

Suggested change
(
EventError::UnknownFirstStoryLabel,
"unknown first-story label",
(
EventError::UnknownFirstStoryLabel,
"unknown first-story label",
),
(
Open in Devin Review

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

EventError::EventTrackIsNotEventInstance,
"event track is not an event instance",
),
Expand Down
175 changes: 175 additions & 0 deletions crates/event_core/src/first_story.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! First-story detection scores stay distinct from promoted instances.

use crate::{EventConfidence, EventError, EventInstanceId, EventMentionId};

/// TDT first-story versus follow-up label.
///
/// A first-story 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 FirstStoryLabel {
/// The mention is scored as the onset of a new story.
FirstStory,
/// The mention is scored as a continuation of an earlier story.
FollowUp,
}

impl FirstStoryLabel {
/// Return the stable wire label name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::FirstStory => "first_story",
Self::FollowUp => "follow_up",
}
}

/// Parse a stable wire first-story label.
///
/// # Errors
///
/// Returns [`EventError::UnknownFirstStoryLabel`] for unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, EventError> {
match name {
"first_story" => Ok(Self::FirstStory),
"follow_up" => Ok(Self::FollowUp),
_ => Err(EventError::UnknownFirstStoryLabel),
}
}

/// Return whether this label is a first-story detection.
#[must_use]
pub const fn is_first_story(self) -> bool {
matches!(self, Self::FirstStory)
}

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

/// Threshold a first-story probability into a detection label.
///
/// The threshold is inclusive: `probability >= threshold` is a first story.
#[must_use]
pub fn decide_first_story(
probability: EventConfidence,
threshold: EventConfidence,
) -> FirstStoryLabel {
if probability.value() >= threshold.value() {
FirstStoryLabel::FirstStory
} else {
FirstStoryLabel::FollowUp
}
}

/// Explicit refusal to treat a first-story detection as an event instance.
///
/// # Errors
///
/// Always returns [`EventError::FirstStoryIsNotEventInstance`].
pub fn refuse_first_story_as_instance(
_mention_id: EventMentionId,
) -> Result<EventInstanceId, EventError> {
Err(EventError::FirstStoryIsNotEventInstance)
}

/// False-alarm rate: follow-ups labeled first story, over follow-up truth.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when lengths differ, either
/// slice is empty, or the truth stream contains no follow-up.
pub fn first_story_false_alarm_rate(
truth: &[FirstStoryLabel],
decided: &[FirstStoryLabel],
) -> Result<f64, EventError> {
rate_over_class(
truth,
decided,
FirstStoryLabel::FollowUp,
FirstStoryLabel::FirstStory,
)
}

/// Miss rate: first stories labeled follow-up, over first-story truth.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when lengths differ, either
/// slice is empty, or the truth stream contains no first story.
pub fn first_story_miss_rate(
truth: &[FirstStoryLabel],
decided: &[FirstStoryLabel],
) -> Result<f64, EventError> {
rate_over_class(
truth,
decided,
FirstStoryLabel::FirstStory,
FirstStoryLabel::FollowUp,
)
}

fn rate_over_class(
truth: &[FirstStoryLabel],
decided: &[FirstStoryLabel],
class: FirstStoryLabel,
error_label: FirstStoryLabel,
) -> Result<f64, EventError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(EventError::InvalidWirePayload);
}
let mut class_count = 0_u32;
let mut error_count = 0_u32;
for (truth_label, decided_label) in truth.iter().zip(decided) {
if *truth_label == class {
class_count += 1;
if *decided_label == error_label {
error_count += 1;
}
}
}
if class_count == 0 {
return Err(EventError::InvalidWirePayload);
}
Ok(f64::from(error_count) / f64::from(class_count))
}
Comment thread
seonghobae marked this conversation as resolved.
Comment on lines +120 to +143

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: Divergent empty-class behavior across two first-story APIs

The new rate helpers in first_story.rs fail closed with InvalidWirePayload when a class is absent, while the pre-existing first_story_detection_rates returns 0.0 for the same case (crates/event_core/src/intelligence.rs:114-136). Two coexisting APIs for the same concept with opposite edge-case semantics can mislead consumers.

Open in Devin Review

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


#[cfg(test)]
mod tests {
use super::{
FirstStoryLabel, decide_first_story, first_story_false_alarm_rate, first_story_miss_rate,
refuse_first_story_as_instance,
};
use crate::{EventConfidence, EventError, EventMentionId};

#[test]
fn first_story_helpers_cover_local_branches() {
let mention = EventMentionId::new();
assert_eq!(
refuse_first_story_as_instance(mention),
Err(EventError::FirstStoryIsNotEventInstance)
);
let high = EventConfidence::new(0.8).expect("high");
let low = EventConfidence::new(0.2).expect("low");
assert_eq!(decide_first_story(high, low), FirstStoryLabel::FirstStory);
assert_eq!(decide_first_story(low, high), FirstStoryLabel::FollowUp);
let mixed_truth = [FirstStoryLabel::FirstStory, FirstStoryLabel::FollowUp];
let mixed_decided = [FirstStoryLabel::FollowUp, FirstStoryLabel::FirstStory];
assert!(
(first_story_false_alarm_rate(&mixed_truth, &mixed_decided).expect("far") - 1.0).abs()
< f64::EPSILON
);
assert!(
(first_story_miss_rate(&mixed_truth, &mixed_decided).expect("miss") - 1.0).abs()
< f64::EPSILON
);
}
}
14 changes: 14 additions & 0 deletions crates/event_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
//!
//! TEPP separates **fallible event mentions** grounded in evidence from
//! **versioned event instances** used for temporal state, multilevel membership,
//! and scientific estimation. Mentions and first-story detections never
//! silently become instances, and TDT detections and CHRONOS predictions
//! remain measurement or hypothesis artifacts until independently promoted.
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Duplicated, incoherent module docstring

The new summary sentence was added without removing the old overlapping text, so the module doc repeats "and scientific estimation. Mentions..." and the TDT/CHRONOS clause twice, producing a broken, self-contradicting paragraph.

Prompt for agents
The module-level doc comment in crates/event_core/src/lib.rs now contains duplicated text. The newly added lines 7-9 ("and scientific estimation. Mentions and first-story detections never silently become instances, and TDT detections and CHRONOS predictions remain measurement or hypothesis artifacts until independently promoted.") overlap with the pre-existing lines 10-12 ("and scientific estimation. Mentions never silently become instances. TDT detections and CHRONOS predictions remain measurement or hypothesis artifacts until independently promoted. Track assignments, story..."). The result is a single garbled paragraph that repeats the same phrases twice. Consolidate into one coherent paragraph: keep the new first-story mention, then continue directly into the 'Track assignments, story segmentations, ...' sentence, removing the duplicated 'and scientific estimation. Mentions never silently become instances. TDT detections and CHRONOS predictions remain measurement or hypothesis artifacts until independently promoted.' fragment.
Open in Devin Review

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

//! and scientific estimation. Mentions never silently become instances. TDT
//! detections and CHRONOS predictions remain measurement or hypothesis
//! artifacts until independently promoted. Track assignments, story
Expand All @@ -13,6 +16,7 @@

mod confidence;
mod error;
mod first_story;
mod identifier;
mod instance;
mod intelligence;
Expand All @@ -30,6 +34,16 @@ pub use confidence::EventConfidence;
pub use confidence::mention_brier_score;
/// Fail-closed event-ontology errors.
pub use error::EventError;
/// First-story versus follow-up detection label.
pub use first_story::FirstStoryLabel;
/// Threshold a first-story probability into a detection label.
pub use first_story::decide_first_story;
/// False-alarm rate for first-story detections.
pub use first_story::first_story_false_alarm_rate;
/// Miss rate for first-story detections.
pub use first_story::first_story_miss_rate;
/// Explicit refusal to treat a first-story detection as an instance.
pub use first_story::refuse_first_story_as_instance;
/// Opaque event-instance identifier.
pub use identifier::EventInstanceId;
/// Opaque event-mention identifier.
Expand Down
Loading
Loading