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 ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ boundaries above remain the target modular MSA architecture.
| `tepp_simulation` | known-truth temporal/event data generation |
| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics |
| `tepp_api` | versioned DTO, schema, and export contracts |
| `outcome_order` | input-process-outcome edges cannot move backward in event time |
| `retrospective_edge` | retrospective reporting cannot become a transition or a translation |
| `payload_bound` | untrusted documents, records, checkpoints, and LLM outputs fail closed without identity, provenance, size, and depth |
| `inferred_status` | inferred relations cannot be promoted to observed evidence or transitions |
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `outcome_order` identity gate: `input_to` and `process_to` cannot move backward or stay contemporaneous in event-time rank; `outcome_of` may point at an earlier producer and cannot become a state transition; recovered kinds match known truth at a higher computed rate than collapsing every kind to `input_to` (ADR 0002/0003).
- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose.
- `retrospective_edge` identity gate: retrospective reporting may point to earlier event time but cannot become a state transition or a translation; recovered reporting kinds match known truth at a higher computed rate than collapsing every report to a contemporaneous forward report (ADR 0002/0003).
- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose.
- `payload_bound` identity gate: documents, serialized records, model checkpoints, and LLM outputs stay untrusted until identity, provenance, size, and depth validate; recovered accept/reject flags match known truth at a higher computed rate than accepting every payload (ADR 0008/0013).
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/outcome_order",
"crates/retrospective_edge",
"crates/payload_bound",
"crates/inferred_status",
Expand Down Expand Up @@ -49,6 +50,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/outcome_order",
"crates/retrospective_edge",
"crates/payload_bound",
"crates/inferred_status",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/outcome_order
crates/retrospective_edge
crates/payload_bound
crates/inferred_status
Expand Down
17 changes: 17 additions & 0 deletions crates/outcome_order/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "outcome_order"
description = "Input-process-outcome edges never move backward in event time."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false

[lints]
workspace = true
64 changes: 64 additions & 0 deletions crates/outcome_order/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Fail-closed input-process-outcome order errors.

use std::fmt;

/// A fail-closed input-process-outcome order error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum OutcomeOrderError {
/// An `input_to` or `process_to` edge moved backward in event time.
ReverseIpoOrder,
/// A transition IPO edge used equal event-time ranks.
UncertainIpoOrder,
/// An `outcome_of` provenance edge was treated as a state transition.
OutcomeOfIsNotTransition,
/// A recovery slice was empty or length-mismatched.
InvalidEdgePayload,
}

impl fmt::Display for OutcomeOrderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::ReverseIpoOrder => {
"input-process-outcome transitions cannot move backward in event time"
}
Self::UncertainIpoOrder => {
"input-process-outcome transitions require a strict event-time order"
}
Self::OutcomeOfIsNotTransition => "outcome_of is not a state transition",
Self::InvalidEdgePayload => "invalid outcome-order payload",
};
formatter.write_str(message)
}
}

impl std::error::Error for OutcomeOrderError {}

#[cfg(test)]
mod tests {
use super::OutcomeOrderError;

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
OutcomeOrderError::ReverseIpoOrder,
"input-process-outcome transitions cannot move backward in event time",
),
(
OutcomeOrderError::UncertainIpoOrder,
"input-process-outcome transitions require a strict event-time order",
),
(
OutcomeOrderError::OutcomeOfIsNotTransition,
"outcome_of is not a state transition",
),
(
OutcomeOrderError::InvalidEdgePayload,
"invalid outcome-order payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
176 changes: 176 additions & 0 deletions crates/outcome_order/src/kind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//! Input, process, and outcome-of kinds with event-time order gates.

use crate::OutcomeOrderError;
use std::cmp::Ordering;

/// Closed vocabulary of input-process-outcome edges.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OutcomeKind {
/// Input feeding a later process (forward transition).
InputTo,
/// Process feeding a later process or outcome (forward transition).
ProcessTo,
/// Outcome pointing back at its producer (provenance; may look backward).
OutcomeOf,
}

impl OutcomeKind {
/// Return the stable wire kind name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::InputTo => "input_to",
Self::ProcessTo => "process_to",
Self::OutcomeOf => "outcome_of",
}
}

/// Parse a stable wire kind name.
///
/// # Errors
///
/// Returns [`OutcomeOrderError::InvalidEdgePayload`] for unrecognized names.
pub fn from_wire_name(name: &str) -> Result<Self, OutcomeOrderError> {
match name {
"input_to" => Ok(Self::InputTo),
"process_to" => Ok(Self::ProcessTo),
"outcome_of" => Ok(Self::OutcomeOf),
_ => Err(OutcomeOrderError::InvalidEdgePayload),
}
}

/// Return whether this kind is a forward state-transition edge.
///
/// `outcome_of` is provenance (the inverse of `produces`).
#[must_use]
pub const fn is_transition_edge(self) -> bool {
match self {
Self::InputTo | Self::ProcessTo => true,
Self::OutcomeOf => false,
}
}
}

/// Refuse reverse event-time order on input and process transitions.
///
/// `source_rank` and `target_rank` are opaque event-time ordinals, not clock
/// identities. Transition kinds require `source_rank < target_rank`.
/// [`OutcomeKind::OutcomeOf`] may point at an earlier producer.
///
/// # Errors
///
/// Returns [`OutcomeOrderError::ReverseIpoOrder`] when a transition moves
/// backward and [`OutcomeOrderError::UncertainIpoOrder`] when a transition
/// uses equal ranks.
pub fn refuse_reverse_ipo_order(
kind: OutcomeKind,
source_rank: u64,
target_rank: u64,
) -> Result<(), OutcomeOrderError> {
match kind {
OutcomeKind::InputTo | OutcomeKind::ProcessTo => match source_rank.cmp(&target_rank) {
Ordering::Less => Ok(()),
Ordering::Greater => Err(OutcomeOrderError::ReverseIpoOrder),
Ordering::Equal => Err(OutcomeOrderError::UncertainIpoOrder),
},
OutcomeKind::OutcomeOf => Ok(()),
}
}

/// Refuse to treat `outcome_of` as a forward state transition.
///
/// # Errors
///
/// Returns [`OutcomeOrderError::OutcomeOfIsNotTransition`] when `kind` is
/// [`OutcomeKind::OutcomeOf`].
pub fn refuse_outcome_of_as_transition(kind: OutcomeKind) -> Result<(), OutcomeOrderError> {
match kind {
OutcomeKind::OutcomeOf => Err(OutcomeOrderError::OutcomeOfIsNotTransition),
OutcomeKind::InputTo | OutcomeKind::ProcessTo => Ok(()),
}
}

/// Fraction of recovered IPO kinds that match known truth.
///
/// # Errors
///
/// Returns [`OutcomeOrderError::InvalidEdgePayload`] when either slice is
/// empty or the lengths differ.
pub fn kind_recovery_rate(
truth: &[OutcomeKind],
decided: &[OutcomeKind],
) -> Result<f64, OutcomeOrderError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(OutcomeOrderError::InvalidEdgePayload);
}
let mut matches = 0_u32;
for (truth_kind, decided_kind) in truth.iter().zip(decided) {
if truth_kind == decided_kind {
matches += 1;
}
}
Ok(f64::from(matches) / truth.len() as f64)
}

#[cfg(test)]
mod tests {
use super::{
OutcomeKind, kind_recovery_rate, refuse_outcome_of_as_transition, refuse_reverse_ipo_order,
};
use crate::OutcomeOrderError;

#[test]
fn local_branches_cover_kinds_order_and_payloads() {
for kind in [
OutcomeKind::InputTo,
OutcomeKind::ProcessTo,
OutcomeKind::OutcomeOf,
] {
assert_eq!(
OutcomeKind::from_wire_name(kind.wire_name()).expect("round-trip"),
kind
);
}
assert!(OutcomeKind::InputTo.is_transition_edge());
assert!(OutcomeKind::ProcessTo.is_transition_edge());
assert!(!OutcomeKind::OutcomeOf.is_transition_edge());
assert_eq!(
OutcomeKind::from_wire_name("causes"),
Err(OutcomeOrderError::InvalidEdgePayload)
);
refuse_reverse_ipo_order(OutcomeKind::InputTo, 1, 2).expect("forward");
refuse_reverse_ipo_order(OutcomeKind::ProcessTo, 2, 3).expect("forward");
refuse_reverse_ipo_order(OutcomeKind::OutcomeOf, 9, 1).expect("look-back");
assert_eq!(
refuse_reverse_ipo_order(OutcomeKind::InputTo, 4, 1),
Err(OutcomeOrderError::ReverseIpoOrder)
);
assert_eq!(
refuse_reverse_ipo_order(OutcomeKind::ProcessTo, 8, 8),
Err(OutcomeOrderError::UncertainIpoOrder)
);
assert_eq!(
refuse_outcome_of_as_transition(OutcomeKind::OutcomeOf),
Err(OutcomeOrderError::OutcomeOfIsNotTransition)
);
refuse_outcome_of_as_transition(OutcomeKind::InputTo).expect("transition");
refuse_outcome_of_as_transition(OutcomeKind::ProcessTo).expect("transition");
let matched =
kind_recovery_rate(&[OutcomeKind::InputTo], &[OutcomeKind::InputTo]).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
let partial = kind_recovery_rate(
&[OutcomeKind::InputTo, OutcomeKind::OutcomeOf],
&[OutcomeKind::InputTo, OutcomeKind::InputTo],
)
.expect("partial");
assert!((partial - 0.5).abs() < f64::EPSILON);
assert_eq!(
kind_recovery_rate(&[], &[]),
Err(OutcomeOrderError::InvalidEdgePayload)
);
assert_eq!(
kind_recovery_rate(&[OutcomeKind::InputTo], &[]),
Err(OutcomeOrderError::InvalidEdgePayload)
);
}
}
22 changes: 22 additions & 0 deletions crates/outcome_order/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Input-process-outcome edges never move backward in event time.
//!
//! `input_to` and `process_to` are forward transitions. `outcome_of` is
//! provenance (the inverse of `produces`) and may point at an earlier
//! producer without becoming a reverse state transition (ADR 0002/0003).

mod error;
mod kind;

/// Fail-closed input-process-outcome order errors.
pub use error::OutcomeOrderError;
/// Closed vocabulary of input, process, and outcome-of edges.
pub use kind::OutcomeKind;
/// Fraction of recovered IPO kinds that match known truth.
pub use kind::kind_recovery_rate;
/// Refuse to treat `outcome_of` as a forward state transition.
pub use kind::refuse_outcome_of_as_transition;
/// Refuse reverse event-time order on input and process transitions.
pub use kind::refuse_reverse_ipo_order;
7 changes: 7 additions & 0 deletions crates/outcome_order/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `outcome_order` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "outcome_order");
}
Loading
Loading