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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `event_core` CHRONOS schema-slot gate: predicted role fillers stay distinct from promoted instances and transitions, slot precision/recall are computed from known-truth fills, and production label/confidence APIs produce calibrated occupancy RMSE ≈ 0.1411 versus always-fill ≈ 0.7071 in the contract fixture.
- `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
25 changes: 25 additions & 0 deletions crates/event_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ pub enum EventError {
UnsupportedWireVersion,
/// An unknown event-role name was supplied.
UnknownEventRole,
/// A CHRONOS schema prediction was treated as an event instance.
SchemaPredictionIsNotEventInstance,
/// A CHRONOS schema prediction was treated as a state transition.
SchemaPredictionIsNotStateTransition,
/// An unknown schema-slot occupancy label was supplied.
UnknownSchemaSlotLabel,
}

impl fmt::Display for EventError {
Expand All @@ -32,6 +38,13 @@ impl fmt::Display for EventError {
Self::InvalidWirePayload => "invalid event wire payload",
Self::UnsupportedWireVersion => "unsupported event wire version",
Self::UnknownEventRole => "unknown event role",
Self::SchemaPredictionIsNotEventInstance => {
"schema prediction is not an event instance"
}
Self::SchemaPredictionIsNotStateTransition => {
"schema prediction is not a state transition"
}
Self::UnknownSchemaSlotLabel => "unknown schema slot label",
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -65,6 +78,18 @@ mod tests {
"unsupported event wire version",
),
(EventError::UnknownEventRole, "unknown event role"),
(
EventError::SchemaPredictionIsNotEventInstance,
"schema prediction is not an event instance",
),
(
EventError::SchemaPredictionIsNotStateTransition,
"schema prediction is not a state transition",
),
(
EventError::UnknownSchemaSlotLabel,
"unknown schema slot label",
),
] {
assert_eq!(error.to_string(), message);
}
Expand Down
20 changes: 19 additions & 1 deletion crates/event_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 and CHRONOS schema-slot predictions
//! never silently become instances.

mod confidence;
mod error;
Expand All @@ -13,6 +14,7 @@ mod instance;
mod mention;
mod registry;
mod role;
mod schema;

/// Finite confidence on the closed unit interval.
pub use confidence::EventConfidence;
Expand All @@ -34,3 +36,19 @@ pub use mention::EventMention;
pub use registry::EventRegistry;
/// Typed event role kind.
pub use role::EventRoleKind;
/// Opaque CHRONOS schema-prediction identity.
pub use schema::SchemaPredictionId;
/// Predicted or observed filler for one schema slot.
pub use schema::SchemaSlotAssignment;
/// Filled-versus-empty occupancy label.
pub use schema::SchemaSlotLabel;
/// Threshold a slot-occupancy probability into a fill label.
pub use schema::decide_schema_slot;
/// Explicit refusal to treat a schema prediction as an instance.
pub use schema::refuse_schema_prediction_as_instance;
/// Explicit refusal to treat a schema prediction as a state transition.
pub use schema::refuse_schema_prediction_as_transition;
/// Precision of recovered filled slots against known truth.
pub use schema::schema_slot_precision;
/// Recall of recovered filled slots against known truth.
pub use schema::schema_slot_recall;
258 changes: 258 additions & 0 deletions crates/event_core/src/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//! CHRONOS schema-slot predictions stay distinct from instances and transitions.

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

/// Opaque CHRONOS schema-prediction identity.
///
/// A schema prediction is a hypothesized slot-fill. 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 SchemaPredictionId(u32);

impl SchemaPredictionId {
/// Reconstruct a prediction identity from a raw fixture or estimator label.
#[must_use]
pub const fn from_raw(raw: u32) -> Self {
Self(raw)
}

/// Return the raw prediction label.
#[must_use]
pub const fn raw(self) -> u32 {
self.0
}
}

/// CHRONOS filled-versus-empty occupancy for one schema slot.
///
/// A fill decision is prediction 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 SchemaSlotLabel {
/// The slot is scored as occupied by a filler.
Filled,
/// The slot is scored as unoccupied.
Empty,
}

impl SchemaSlotLabel {
/// Return the stable wire label name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Filled => "filled",
Self::Empty => "empty",
}
}

/// Parse a stable wire schema-slot occupancy label.
///
/// # Errors
///
/// Returns [`EventError::UnknownSchemaSlotLabel`] for unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, EventError> {
match name {
"filled" => Ok(Self::Filled),
"empty" => Ok(Self::Empty),
_ => Err(EventError::UnknownSchemaSlotLabel),
}
}

/// Return whether this label marks a filled slot.
#[must_use]
pub const fn is_filled(self) -> bool {
matches!(self, Self::Filled)
}

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

/// Predicted or observed filler for one CHRONOS schema slot.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SchemaSlotAssignment {
role: EventRoleKind,
argument: String,
}

impl SchemaSlotAssignment {
/// Bind a role to a hypothesized filler argument.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when the argument is empty
/// or whitespace-only.
pub fn new(role: EventRoleKind, argument: impl Into<String>) -> Result<Self, EventError> {
let argument = argument.into();
let argument = argument.trim();
if argument.is_empty() {
return Err(EventError::InvalidWirePayload);
}
Ok(Self {
role,
argument: argument.to_string(),
})
}

/// Return the typed role for this slot.
#[must_use]
pub const fn role(&self) -> EventRoleKind {
self.role
}

/// Return the hypothesized filler argument.
#[must_use]
pub fn argument(&self) -> &str {
&self.argument
}
}

/// Threshold a slot-occupancy probability into a filled/empty label.
///
/// The threshold is inclusive: `probability >= threshold` fills the slot.
#[must_use]
pub fn decide_schema_slot(
probability: EventConfidence,
threshold: EventConfidence,
) -> SchemaSlotLabel {
if probability.value() >= threshold.value() {
SchemaSlotLabel::Filled
} else {
SchemaSlotLabel::Empty
}
}

/// Explicit refusal to treat a CHRONOS schema prediction as an event instance.
///
/// # Errors
///
/// Always returns [`EventError::SchemaPredictionIsNotEventInstance`].
pub fn refuse_schema_prediction_as_instance(
_prediction: SchemaPredictionId,
) -> Result<EventInstanceId, EventError> {
Err(EventError::SchemaPredictionIsNotEventInstance)
}

/// Explicit refusal to treat a CHRONOS schema prediction as a state transition.
///
/// # Errors
///
/// Always returns [`EventError::SchemaPredictionIsNotStateTransition`].
pub fn refuse_schema_prediction_as_transition(
_prediction: SchemaPredictionId,
) -> Result<(), EventError> {
Err(EventError::SchemaPredictionIsNotStateTransition)
}

/// Precision of recovered filled slots against known truth fills.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when either fill set is empty
/// or a `(role, argument)` pair is duplicated.
pub fn schema_slot_precision(
truth: &[SchemaSlotAssignment],
recovered: &[SchemaSlotAssignment],
) -> Result<f64, EventError> {
let truth_slots = unique_slot_set(truth)?;
let recovered_slots = unique_slot_set(recovered)?;
counted_rate(
recovered_slots.intersection(&truth_slots).count(),
recovered_slots.len(),
)
}

/// Recall of recovered filled slots against known truth fills.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] when either fill set is empty
/// or a `(role, argument)` pair is duplicated.
pub fn schema_slot_recall(
truth: &[SchemaSlotAssignment],
recovered: &[SchemaSlotAssignment],
) -> Result<f64, EventError> {
let truth_slots = unique_slot_set(truth)?;
let recovered_slots = unique_slot_set(recovered)?;
counted_rate(
recovered_slots.intersection(&truth_slots).count(),
truth_slots.len(),
)
}

fn unique_slot_set(
assignments: &[SchemaSlotAssignment],
) -> Result<BTreeSet<(EventRoleKind, String)>, EventError> {
if assignments.is_empty() {
return Err(EventError::InvalidWirePayload);
}
let mut slots = BTreeSet::new();
for assignment in assignments {
if !slots.insert((assignment.role(), assignment.argument().to_string())) {
return Err(EventError::InvalidWirePayload);
}
}
Ok(slots)
}

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))
}

#[cfg(test)]
mod tests {
use super::{
SchemaPredictionId, SchemaSlotAssignment, SchemaSlotLabel, counted_rate,
decide_schema_slot, refuse_schema_prediction_as_instance,
refuse_schema_prediction_as_transition, schema_slot_precision, schema_slot_recall,
};
use crate::{EventConfidence, EventError, EventRoleKind};

fn filled(role: EventRoleKind, argument: &str) -> SchemaSlotAssignment {
SchemaSlotAssignment::new(role, argument).expect("slot")
}

#[test]
fn schema_helpers_cover_local_branches() {
let prediction = SchemaPredictionId::from_raw(3);
assert_eq!(
refuse_schema_prediction_as_instance(prediction),
Err(EventError::SchemaPredictionIsNotEventInstance)
);
assert_eq!(
refuse_schema_prediction_as_transition(prediction),
Err(EventError::SchemaPredictionIsNotStateTransition)
);
let high = EventConfidence::new(0.8).expect("high");
let low = EventConfidence::new(0.2).expect("low");
assert_eq!(decide_schema_slot(high, low), SchemaSlotLabel::Filled);
assert_eq!(decide_schema_slot(low, high), SchemaSlotLabel::Empty);
let truth = [filled(EventRoleKind::Agent, "procurement office")];
assert!((schema_slot_precision(&truth, &truth).expect("p") - 1.0).abs() < f64::EPSILON);
assert!((schema_slot_recall(&truth, &truth).expect("r") - 1.0).abs() < f64::EPSILON);
assert_eq!(counted_rate(0, 0), Err(EventError::InvalidWirePayload));
assert_eq!(
counted_rate(usize::MAX, 1),
Err(EventError::InvalidWirePayload)
);
assert_eq!(
counted_rate(1, usize::MAX),
Err(EventError::InvalidWirePayload)
);
assert!((counted_rate(1, 2).expect("half") - 0.5).abs() < f64::EPSILON);
}
}
Loading
Loading