Skip to content
Merged
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 calibrated occupancy scores recover fill targets with lower RMSE than an always-fill predictor.
- `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.
- `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013).
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