diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db6..3f6b2196b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,10 +61,10 @@ 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 | +| `prediction_contradiction` | Allen promotion gate: `before`/`after` stay contradictory; `meets`/`met_by` stay unsupported | -No crate exposes placeholder production behavior in Task 1. This prevents an -empty façade from becoming a de facto public API before its invariants and tests -exist. +Foundation crates expose only tested contracts. Empty façades are not public +APIs. ## Immutable evidence boundary diff --git a/CHANGELOG.md b/CHANGELOG.md index 93891a271..9873b5f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). - `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). diff --git a/Cargo.lock b/Cargo.lock index 616bfd78e..5d1028231 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,6 +847,13 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prediction_contradiction" +version = "0.1.0" +dependencies = [ + "temporal_core", +] + [[package]] name = "proc-macro2" version = "1.0.107" diff --git a/Cargo.toml b/Cargo.toml index 925659406..671bff1c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/prediction_contradiction", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/prediction_contradiction", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d3..78d461db9 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ implemented in Rust. ## Current implementation state -This branch establishes the Task 1 Rust workspace and quality-gate foundation. -The ten bounded crates compile independently but intentionally expose no -placeholder production APIs. Domain behavior begins in Task 2 with immutable -evidence identifiers and source records. +This branch keeps the Rust workspace quality foundation and the bounded +foundation crates. Domain crates expose only tested contracts: immutable +evidence, six-clock temporal values, event mentions/instances, relations, +membership, persistence, splits, simulation, validation, API DTOs, and the +predicted-versus-observed promotion gate. ```text crates/evidence_core @@ -22,6 +23,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/prediction_contradiction ``` ## Local verification diff --git a/crates/prediction_contradiction/Cargo.toml b/crates/prediction_contradiction/Cargo.toml new file mode 100644 index 000000000..b656fb754 --- /dev/null +++ b/crates/prediction_contradiction/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "prediction_contradiction" +description = "Predicted intervals that contradict observations cannot become fact." +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 + +[dependencies] +temporal_core = { path = "../temporal_core", version = "0.1.0" } + +[lints] +workspace = true diff --git a/crates/prediction_contradiction/src/error.rs b/crates/prediction_contradiction/src/error.rs new file mode 100644 index 000000000..d4bba56b1 --- /dev/null +++ b/crates/prediction_contradiction/src/error.rs @@ -0,0 +1,82 @@ +//! Fail-closed prediction-contradiction errors. + +use std::fmt; + +/// A fail-closed prediction-contradiction error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PredictionContradictionError { + /// Predicted and observed event-time intervals are Allen `before` or `after`. + PredictionContradictsObservation, + /// Predicted and observed intervals meet but do not overlap in their interiors. + PredictionLacksOverlappingSupport, + /// Observed evidence covers only part of the predicted interval. + PredictionLacksFullSupport, + /// Observed evidence became available after the analysis knowledge cutoff. + EvidenceAfterCutoff, + /// An interval is not a closed proper Allen input. + InvalidIntervalPayload, + /// An agreement-rate comparison used empty or length-mismatched slices. + AgreementSliceMismatch, +} + +impl fmt::Display for PredictionContradictionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::PredictionContradictsObservation => { + "predicted interval contradicts observed evidence" + } + Self::PredictionLacksOverlappingSupport => { + "predicted interval is adjacent to observation without overlapping support" + } + Self::PredictionLacksFullSupport => { + "predicted interval is not fully supported by observed evidence" + } + Self::EvidenceAfterCutoff => { + "observed evidence is available after the knowledge cutoff" + } + Self::InvalidIntervalPayload => "invalid prediction-contradiction payload", + Self::AgreementSliceMismatch => "agreement slices are empty or length-mismatched", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for PredictionContradictionError {} + +#[cfg(test)] +mod tests { + use super::PredictionContradictionError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + PredictionContradictionError::PredictionContradictsObservation, + "predicted interval contradicts observed evidence", + ), + ( + PredictionContradictionError::PredictionLacksOverlappingSupport, + "predicted interval is adjacent to observation without overlapping support", + ), + ( + PredictionContradictionError::PredictionLacksFullSupport, + "predicted interval is not fully supported by observed evidence", + ), + ( + PredictionContradictionError::EvidenceAfterCutoff, + "observed evidence is available after the knowledge cutoff", + ), + ( + PredictionContradictionError::InvalidIntervalPayload, + "invalid prediction-contradiction payload", + ), + ( + PredictionContradictionError::AgreementSliceMismatch, + "agreement slices are empty or length-mismatched", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/prediction_contradiction/src/interval.rs b/crates/prediction_contradiction/src/interval.rs new file mode 100644 index 000000000..d2e87437f --- /dev/null +++ b/crates/prediction_contradiction/src/interval.rs @@ -0,0 +1,223 @@ +//! Predicted-versus-observed promotion using `temporal_core` Allen classification. + +use crate::PredictionContradictionError; +use temporal_core::{ + AllenRelation, AvailableTime, EventTime, KnowledgeCutoff, TemporalError, TemporalInterval, + classify_interval_relation, +}; + +fn map_temporal(error: TemporalError) -> PredictionContradictionError { + let _ = error; + PredictionContradictionError::InvalidIntervalPayload +} + +/// Return whether two closed proper intervals are Allen `before` or `after`. +/// +/// Adjacent `meets` / `met_by` pairs are not contradictions. They share an +/// endpoint and remain consistent under Allen (1983); they still lack interior +/// overlap and therefore cannot support promotion. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when either +/// interval is not a closed proper Allen input. +pub fn intervals_contradict( + predicted: &TemporalInterval, + observed: &TemporalInterval, +) -> Result { + match classify_interval_relation(predicted, observed).map_err(map_temporal)? { + AllenRelation::Before | AllenRelation::After => Ok(true), + AllenRelation::Meets + | AllenRelation::MetBy + | AllenRelation::Overlaps + | AllenRelation::OverlappedBy + | AllenRelation::Starts + | AllenRelation::StartedBy + | AllenRelation::During + | AllenRelation::Contains + | AllenRelation::Finishes + | AllenRelation::FinishedBy + | AllenRelation::Equals => Ok(false), + } +} + +/// Refuse promotion when evidence is ineligible, disjoint, or only adjacent. +/// +/// This function classifies intervals with +/// [`temporal_core::classify_interval_relation`]. It does not run the +/// path-consistency reasoner. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::EvidenceAfterCutoff`] when +/// `observed_available` is later than `cutoff`. Returns +/// [`PredictionContradictionError::PredictionContradictsObservation`] for +/// Allen `before` / `after`. Returns +/// [`PredictionContradictionError::PredictionLacksOverlappingSupport`] for +/// `meets` / `met_by`, and +/// [`PredictionContradictionError::PredictionLacksFullSupport`] when observed +/// evidence covers only part of the prediction. Returns +/// [`PredictionContradictionError::InvalidIntervalPayload`] when either +/// interval is not a closed proper Allen input. +pub fn refuse_promotion( + predicted: &TemporalInterval, + observed: &TemporalInterval, + observed_available: AvailableTime, + cutoff: KnowledgeCutoff, +) -> Result<(), PredictionContradictionError> { + if observed_available.instant() > cutoff.instant() { + return Err(PredictionContradictionError::EvidenceAfterCutoff); + } + match classify_interval_relation(predicted, observed).map_err(map_temporal)? { + AllenRelation::Before | AllenRelation::After => { + Err(PredictionContradictionError::PredictionContradictsObservation) + } + AllenRelation::Meets | AllenRelation::MetBy => { + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + } + AllenRelation::Starts + | AllenRelation::During + | AllenRelation::Finishes + | AllenRelation::Equals => Ok(()), + AllenRelation::Overlaps + | AllenRelation::OverlappedBy + | AllenRelation::StartedBy + | AllenRelation::Contains + | AllenRelation::FinishedBy => { + Err(PredictionContradictionError::PredictionLacksFullSupport) + } + } +} + +/// Fraction of contradiction flags that match independently supplied labels. +/// +/// This is a label-agreement helper for the promotion gate. It is not RMSE, +/// bias, or interval-coverage recovery against a generative truth process. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::AgreementSliceMismatch`] when +/// either slice is empty or the lengths differ. +pub fn contradiction_agreement_rate( + truth: &[bool], + decided: &[bool], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(PredictionContradictionError::AgreementSliceMismatch); + } + let mut matches = 0_u32; + for (truth_flag, decided_flag) in truth.iter().zip(decided) { + if truth_flag == decided_flag { + matches += 1; + } + } + #[allow(clippy::cast_precision_loss)] + let rate = f64::from(matches) / truth.len() as f64; + Ok(rate) +} + +#[cfg(test)] +mod tests { + use super::{contradiction_agreement_rate, intervals_contradict, refuse_promotion}; + use crate::PredictionContradictionError; + use temporal_core::{ + AvailableTime, EventTime, KnowledgeCutoff, TemporalBoundary, TemporalInterval, + TemporalPrecision, + }; + + fn event_at(second: u8) -> EventTime { + EventTime::parse_rfc3339(&format!("2026-01-01T00:00:{second:02}Z")).expect("event time") + } + + fn closed(start: u8, end: u8) -> TemporalInterval { + TemporalInterval::bounded( + TemporalBoundary::Included(event_at(start)), + TemporalBoundary::Included(event_at(end)), + TemporalPrecision::Second, + ) + .expect("closed interval") + } + + fn clocks() -> (AvailableTime, KnowledgeCutoff) { + ( + AvailableTime::parse_rfc3339("2026-01-02T00:00:00Z").expect("available"), + KnowledgeCutoff::parse_rfc3339("2026-01-03T00:00:00Z").expect("cutoff"), + ) + } + + #[test] + fn local_branches_cover_relations_cutoff_and_agreement() { + let (available, cutoff) = clocks(); + let predicted = closed(0, 10); + assert!(intervals_contradict(&predicted, &closed(20, 30)).expect("before")); + assert!(intervals_contradict(&closed(40, 50), &predicted).expect("after")); + assert!(!intervals_contradict(&predicted, &closed(10, 20)).expect("meets")); + assert!(!intervals_contradict(&closed(10, 20), &predicted).expect("met_by")); + assert!(!intervals_contradict(&predicted, &closed(5, 15)).expect("overlaps")); + assert!(!intervals_contradict(&closed(5, 15), &predicted).expect("overlapped_by")); + assert!(!intervals_contradict(&predicted, &closed(0, 8)).expect("started_by")); + assert!(!intervals_contradict(&closed(0, 8), &predicted).expect("starts")); + assert!(!intervals_contradict(&predicted, &closed(2, 8)).expect("contains")); + assert!(!intervals_contradict(&closed(2, 8), &predicted).expect("during")); + assert!(!intervals_contradict(&predicted, &closed(2, 10)).expect("finished_by")); + assert!(!intervals_contradict(&closed(2, 10), &predicted).expect("finishes")); + assert!(!intervals_contradict(&predicted, &closed(0, 10)).expect("equals")); + assert_eq!( + refuse_promotion(&predicted, &closed(5, 15), available, cutoff), + Err(PredictionContradictionError::PredictionLacksFullSupport) + ); + assert_eq!( + refuse_promotion(&predicted, &closed(0, 8), available, cutoff), + Err(PredictionContradictionError::PredictionLacksFullSupport) + ); + refuse_promotion(&closed(0, 8), &predicted, available, cutoff).expect("starts"); + assert_eq!( + refuse_promotion(&predicted, &closed(2, 8), available, cutoff), + Err(PredictionContradictionError::PredictionLacksFullSupport) + ); + refuse_promotion(&closed(2, 8), &predicted, available, cutoff).expect("during"); + assert_eq!( + refuse_promotion(&predicted, &closed(2, 10), available, cutoff), + Err(PredictionContradictionError::PredictionLacksFullSupport) + ); + refuse_promotion(&closed(2, 10), &predicted, available, cutoff).expect("finishes"); + refuse_promotion(&predicted, &closed(0, 10), available, cutoff).expect("equals"); + assert_eq!( + refuse_promotion(&predicted, &closed(20, 30), available, cutoff), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + assert_eq!( + refuse_promotion(&predicted, &closed(10, 20), available, cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); + let late = AvailableTime::parse_rfc3339("2026-01-04T00:00:00Z").expect("late"); + assert_eq!( + refuse_promotion(&predicted, &closed(5, 15), late, cutoff), + Err(PredictionContradictionError::EvidenceAfterCutoff) + ); + let half_open = TemporalInterval::bounded( + TemporalBoundary::Included(event_at(0)), + TemporalBoundary::Excluded(event_at(10)), + TemporalPrecision::Second, + ) + .expect("half-open"); + assert_eq!( + intervals_contradict(&half_open, &closed(20, 30)), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + refuse_promotion(&half_open, &closed(20, 30), available, cutoff), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + let matched = contradiction_agreement_rate(&[true], &[true]).expect("rate"); + assert!((matched - 1.0).abs() < f64::EPSILON); + assert_eq!( + contradiction_agreement_rate(&[], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) + ); + assert_eq!( + contradiction_agreement_rate(&[true], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) + ); + } +} diff --git a/crates/prediction_contradiction/src/lib.rs b/crates/prediction_contradiction/src/lib.rs new file mode 100644 index 000000000..6c296d057 --- /dev/null +++ b/crates/prediction_contradiction/src/lib.rs @@ -0,0 +1,22 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +//! Predicted intervals that contradict observations stay hypothetical. +//! +//! A forecast cannot be promoted to an observed event when +//! [`temporal_core::classify_interval_relation`] returns Allen `before` or +//! `after`, when the pair only `meets` / is `met_by`, or when observed evidence +//! covers only part of the prediction. Evidence whose availability time exceeds +//! the analysis knowledge cutoff is ineligible (ADR 0002, ADR 0016). This crate +//! does not run the path-consistency reasoner. + +mod error; +mod interval; + +/// Fail-closed prediction-contradiction errors. +pub use error::PredictionContradictionError; +/// Fraction of contradiction flags that match independently supplied labels. +pub use interval::contradiction_agreement_rate; +/// Return whether two closed proper intervals are Allen `before` or `after`. +pub use interval::intervals_contradict; +/// Refuse promotion when evidence is ineligible, disjoint, or only adjacent. +pub use interval::refuse_promotion; diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs new file mode 100644 index 000000000..82031cfa8 --- /dev/null +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -0,0 +1,202 @@ +//! Predicted intervals stay hypothetical unless later-available evidence overlaps. + +use prediction_contradiction::{ + PredictionContradictionError, contradiction_agreement_rate, intervals_contradict, + refuse_promotion, +}; +use temporal_core::{ + AvailableTime, EventTime, KnowledgeCutoff, TemporalBoundary, TemporalInterval, + TemporalPrecision, +}; + +fn event_at(second: u8) -> EventTime { + EventTime::parse_rfc3339(&format!("2026-01-01T00:00:{second:02}Z")).expect("event time") +} + +fn closed_event_interval(start: u8, end: u8) -> TemporalInterval { + TemporalInterval::bounded( + TemporalBoundary::Included(event_at(start)), + TemporalBoundary::Included(event_at(end)), + TemporalPrecision::Second, + ) + .expect("closed proper interval") +} + +fn available(stamp: &str) -> AvailableTime { + AvailableTime::parse_rfc3339(stamp).expect("available time") +} + +fn cutoff(stamp: &str) -> KnowledgeCutoff { + KnowledgeCutoff::parse_rfc3339(stamp).expect("knowledge cutoff") +} + +fn eligible_clocks() -> (AvailableTime, KnowledgeCutoff) { + ( + available("2026-01-02T00:00:00Z"), + cutoff("2026-01-03T00:00:00Z"), + ) +} + +#[test] +fn before_and_after_cannot_become_observed_fact() { + let predicted = closed_event_interval(0, 10); + let later_observed = closed_event_interval(20, 30); + let earlier_observed = closed_event_interval(40, 50); + let predicted_later = closed_event_interval(0, 10); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + + assert!(intervals_contradict(&predicted, &later_observed).expect("before")); + assert_eq!( + refuse_promotion( + &predicted, + &later_observed, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + + assert!(intervals_contradict(&earlier_observed, &predicted_later).expect("after")); + assert_eq!( + refuse_promotion( + &earlier_observed, + &predicted_later, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); +} + +#[test] +fn meeting_intervals_are_adjacent_not_allen_contradiction() { + let predicted = closed_event_interval(0, 10); + let meeting = closed_event_interval(10, 20); + let met_by = closed_event_interval(10, 20); + let earlier = closed_event_interval(0, 10); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + + assert!(!intervals_contradict(&predicted, &meeting).expect("meets")); + assert_eq!( + refuse_promotion(&predicted, &meeting, observed_available, knowledge_cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); + assert!(!intervals_contradict(&met_by, &earlier).expect("met_by")); + assert_eq!( + refuse_promotion(&met_by, &earlier, observed_available, knowledge_cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); +} + +#[test] +fn partially_overlapping_observation_lacks_full_support() { + let predicted = closed_event_interval(0, 10); + let overlapping = closed_event_interval(5, 15); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + assert!(!intervals_contradict(&predicted, &overlapping).expect("overlaps")); + assert_eq!( + refuse_promotion( + &predicted, + &overlapping, + observed_available, + knowledge_cutoff, + ), + Err(PredictionContradictionError::PredictionLacksFullSupport) + ); +} + +#[test] +fn full_interval_coverage_may_support_promotion() { + let (observed_available, knowledge_cutoff) = eligible_clocks(); + let predictions = [ + (closed_event_interval(0, 5), closed_event_interval(0, 10)), + (closed_event_interval(2, 8), closed_event_interval(0, 10)), + (closed_event_interval(5, 10), closed_event_interval(0, 10)), + (closed_event_interval(0, 10), closed_event_interval(0, 10)), + ]; + for (predicted, observed) in predictions { + refuse_promotion(&predicted, &observed, observed_available, knowledge_cutoff) + .expect("observed interval fully covers prediction"); + } +} + +#[test] +fn evidence_available_after_cutoff_is_ineligible() { + let predicted = closed_event_interval(0, 10); + let overlapping = closed_event_interval(5, 15); + assert_eq!( + refuse_promotion( + &predicted, + &overlapping, + available("2026-01-04T00:00:00Z"), + cutoff("2026-01-03T00:00:00Z"), + ), + Err(PredictionContradictionError::EvidenceAfterCutoff) + ); +} + +#[test] +fn half_open_intervals_are_not_allen_inputs() { + let predicted = TemporalInterval::bounded( + TemporalBoundary::Included(event_at(0)), + TemporalBoundary::Excluded(event_at(10)), + TemporalPrecision::Second, + ) + .expect("half-open interval is representable"); + let observed = closed_event_interval(20, 30); + assert_eq!( + intervals_contradict(&predicted, &observed), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); +} + +#[test] +fn agreement_rate_matches_known_allen_labels_not_promote_all() { + let pairs = [ + (closed_event_interval(0, 10), closed_event_interval(20, 30)), + (closed_event_interval(0, 10), closed_event_interval(5, 15)), + (closed_event_interval(0, 10), closed_event_interval(10, 20)), + (closed_event_interval(40, 50), closed_event_interval(0, 10)), + ]; + let truth = [true, false, false, true]; + let decided = [ + intervals_contradict(&pairs[0].0, &pairs[0].1).expect("before"), + intervals_contradict(&pairs[1].0, &pairs[1].1).expect("overlaps"), + intervals_contradict(&pairs[2].0, &pairs[2].1).expect("meets"), + intervals_contradict(&pairs[3].0, &pairs[3].1).expect("after"), + ]; + let collapsed = [false, false, false, false]; + let agreed = contradiction_agreement_rate(&truth, &decided).expect("agreement"); + let collapsed_rate = contradiction_agreement_rate(&truth, &collapsed).expect("collapsed"); + assert!((agreed - 1.0).abs() < f64::EPSILON); + assert!(agreed > collapsed_rate); +} + +#[test] +fn empty_or_mismatched_agreement_slices_fail_closed() { + assert_eq!( + contradiction_agreement_rate(&[], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) + ); + assert_eq!( + contradiction_agreement_rate(&[true], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) + ); + assert_eq!( + contradiction_agreement_rate(&[true, false], &[true]), + Err(PredictionContradictionError::AgreementSliceMismatch) + ); +} + +#[test] +fn adjacency_failure_explains_missing_interior_overlap() { + let message = PredictionContradictionError::PredictionLacksOverlappingSupport.to_string(); + assert_eq!( + message, + "predicted interval is adjacent to observation without overlapping support" + ); + assert_ne!( + message, + PredictionContradictionError::PredictionContradictsObservation.to_string() + ); +} diff --git a/crates/prediction_contradiction/tests/crate_contract.rs b/crates/prediction_contradiction/tests/crate_contract.rs new file mode 100644 index 000000000..2d414c63c --- /dev/null +++ b/crates/prediction_contradiction/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `prediction_contradiction` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "prediction_contradiction"); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index f67396413..68830dcc6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,10 +30,10 @@ The full APA 7th standards/literature register remains `docs/research/standards- | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | | posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | future `psychometric_core` | accepted-target | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | -| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | +| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `prediction_contradiction` bounded Allen promotion gate on the active PR (`before`/`after` contradiction, `meets`/`met_by` unsupported, cutoff eligibility); remaining TDT/CHRONOS tasks stay accepted-target | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | `tepp_api` router plus future `interpretation_gateway` | partial | -| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | `tepp_api::route_orchestration` + ablation record on the active PR; live contextual-orchestrator execution remaining | partial | -| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; persistence retention/deletion remaining | partial | +| adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | `tepp_api::route_orchestration` + ablation record implemented-main; live contextual-orchestrator execution remaining | partial | +| purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | `tepp_api` export authorization plus provider-payload minimization / elevated re-identification implemented-main; persistence retention/deletion/legal-hold (`0007`) implemented-main | partial | | tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | `tepp_api` time-bounded `PurposeGrant` + cross-tenant denial implemented-main; persistent `access_grant` storage remaining | partial | | standalone + modular CWL MSA / no cross-service DB coupling | ADR 0011; `docs/API_CONTRACT.md` | current standalone crates; future service ports | partial | | naruon modular artifact consumer boundary | ADR 0011/0012; API contract | `docs/connectors/naruon-artifact-consumer.md` + PR #22 versioned consumer contract on protected main; `tepp_api` HTTP interchange (active PR); live HTTP service remaining | partial | diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index b85ee0b4a..f6ed60e21 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -1,7 +1,8 @@ # ADR 0016 — TDT, CHRONOS, and Event Ontology intelligence boundary **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** active-PR — bounded predicted-vs-observed Allen promotion gate only; TDT detection/tracking, CHRONOS schema extraction, prediction calibration, and path-consistency laws remain accepted-target + **Date:** 2026-08-12 **Supersedes:** None; complements ADR 0002 temporal semantics and ADR 0003 event ontology/membership. diff --git a/docs/adr/README.md b/docs/adr/README.md index 258eb7f31..2a5fca51c 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,7 +21,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold on the active PR; remaining physical ERD/backup accepted-target. | | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | | [0015](0015-autonomous-development-review-and-merge-authority.md) | Autonomous development, review, and merge authority separation | Accepted | active-PR | Separates model proposal, deterministic verification, publication, independent review, and merge/release authority. | -| [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | accepted-target | Separates observed evidence, detection/tracking, prediction/schema inference, temporal consistency, and promoted transition authority. | +| [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | active-PR | Bounded predicted-vs-observed Allen promotion gate in `prediction_contradiction` on the active PR; remaining TDT/CHRONOS tasks stay accepted-target. | ## Decision ownership summary diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 652acfe21..7576735cf 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -20,6 +20,18 @@ run may print the task contract without either credential. When a PR exists, normal review → repair → exact-head Checks → merge governance owns the hour. The scheduler does not create a competing branch. +Current executable queue while drafts remain open: + +1. Repair and merge the predicted-versus-observed Allen promotion gate + (`prediction_contradiction` / PR #93) using `temporal_core` classification, + not a second interval algebra. +2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #87), + `text_segment` SQL contracts on existing migration `0006`, retention and + legal-hold migration `0007` (PR #45), foundation known-truth recovery + study, then CHRONOS forecast Brier calibration (PR #85). +3. Do not open a competing hourly proposal until the open-PR inventory is + empty. Prefer reviewing, repairing, and merging the existing drafts. + ## Required repository configuration Configure these repository or organization values: diff --git a/docs/research/prediction-contradiction-gate.md b/docs/research/prediction-contradiction-gate.md new file mode 100644 index 000000000..6984efc65 --- /dev/null +++ b/docs/research/prediction-contradiction-gate.md @@ -0,0 +1,39 @@ +# Predicted-versus-observed temporal contradiction + +## Scope + +`prediction_contradiction` is a promotion policy over +`temporal_core::classify_interval_relation`. A predicted closed proper +event-time interval cannot become observed fact when the Allen relation is +`before` or `after` (contradiction) or `meets` / `met_by` (adjacent, no +interior overlap). Observed evidence whose availability time exceeds the +analysis knowledge cutoff is ineligible. + +Label agreement on those contradiction flags is a helper for the gate. It is +not RMSE, bias, or interval-coverage recovery against a generative truth +process. + +This slice does not run the `temporal_core` path-consistency reasoner, fit +CHRONOS schemas, extract TDT tracks, or claim that the full ADR 0016 +intelligence stack is implemented. + +## Authority + +### Normative TEPP contract + +- `docs/adr/0016-tdt-chronos-event-intelligence-boundary.md` — predictions + remain hypothetical until supported by later evidence; temporal + contradiction can reject a proposed promotion. +- `docs/adr/0002-six-clock-temporal-semantics.md` — event/valid time is + the clock for occurrence intervals; availability may not exceed cutoff. + +### Supporting literature + +Allen (1983) defines thirteen interval relations. `before` and `after` are +strictly disjoint with a gap. `meets` and `met_by` share an endpoint and are +not network contradictions. This crate uses that distinction for promotion +and does not implement the composition table or path consistency. + +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. +*Communications of the ACM, 26*(11), 832–843. +https://doi.org/10.1145/182.358434 diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index bfda7a795..8e9015301 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -66,6 +66,8 @@ Allan, J. (Ed.). (2002). *Topic detection and tracking: Event-based information Anagnostopoulos, E., Batsakis, S., & Petrakis, E. G. M. (2013). CHRONOS: A reasoning engine for qualitative temporal information in OWL. *Procedia Computer Science, 22*, 70–77. https://doi.org/10.1016/j.procs.2013.09.082 +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. *Communications of the ACM, 26*(11), 832–843. https://doi.org/10.1145/182.358434. `temporal_core` owns the thirteen elementary relations and composition; `prediction_contradiction` uses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency. + TEPP uses interval and partial-order reasoning, bitemporal availability, leakage-safe cutoffs, TDT segmentation/link/detection/first-story/tracking tasks, and separate neural/symbolic event-schema and temporal-consistency layers. ## Unicode, language tags, and multilingual structure diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index aae1a06e7..caf576f42 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -24,7 +24,8 @@ This report tracks exact-head scientific and engineering evidence required befor | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | -| Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | +| Adaptive orchestration router | `tepp_api` | implemented-main | mode selection | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` | +| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | Allen `before`/`after` contradiction, `meets`/`met_by` unsupported, cutoff eligibility; label agreement is not RMSE recovery | ADR 0016 | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf58..2e7a024a9 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "prediction_contradiction", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py index 2c11f7a53..56d553d27 100644 --- a/tests/quality/test_check_docstrings.py +++ b/tests/quality/test_check_docstrings.py @@ -11,6 +11,7 @@ from unittest import mock from scripts import check_docstrings as docstrings +from scripts import check_workspace_contract as contract REPOSITORY_ROOT = Path(__file__).resolve().parents[2] @@ -24,7 +25,7 @@ def test_live_repository_is_documented(self) -> None: sources = docstrings.rust_sources(REPOSITORY_ROOT) crate_roots = sorted(REPOSITORY_ROOT.glob("crates/*/src/lib.rs")) - self.assertEqual(len(crate_roots), 10) + self.assertEqual(len(crate_roots), len(contract.EXPECTED_CRATES)) self.assertTrue(set(crate_roots).issubset(sources)) self.assertGreaterEqual(len(sources), len(crate_roots)) self.assertEqual(docstrings.validate_repository(REPOSITORY_ROOT), [])