From 9902a9023bc0adcbe12a9ee837f8837f74be7233 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 23:17:46 +0900 Subject: [PATCH 01/13] feat(temporal): refuse disjoint predictions as observed fact Half-open event-time forecasts that do not overlap later-observed evidence stay hypothetical (ADR 0016). --- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + Cargo.lock | 4 + Cargo.toml | 2 + README.md | 3 +- crates/prediction_contradiction/Cargo.toml | 17 +++ crates/prediction_contradiction/src/error.rs | 48 +++++++ .../prediction_contradiction/src/interval.rs | 135 ++++++++++++++++++ crates/prediction_contradiction/src/lib.rs | 21 +++ .../tests/contradiction_contract.rs | 77 ++++++++++ .../tests/crate_contract.rs | 7 + docs/TRACEABILITY.md | 2 +- ...tdt-chronos-event-intelligence-boundary.md | 2 +- docs/adr/README.md | 2 +- .../research/prediction-contradiction-gate.md | 31 ++++ docs/research/standards-and-literature.md | 2 + docs/validation/temporal-event-foundation.md | 1 + scripts/check_workspace_contract.py | 1 + 18 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 crates/prediction_contradiction/Cargo.toml create mode 100644 crates/prediction_contradiction/src/error.rs create mode 100644 crates/prediction_contradiction/src/interval.rs create mode 100644 crates/prediction_contradiction/src/lib.rs create mode 100644 crates/prediction_contradiction/tests/contradiction_contract.rs create mode 100644 crates/prediction_contradiction/tests/crate_contract.rs create mode 100644 docs/research/prediction-contradiction-gate.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db6..4cd1a25b0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 | +| `prediction_contradiction` | predicted event-time intervals that contradict observations stay hypothetical | 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c1cc6e879..6a8482625 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: half-open event-time intervals that are disjoint from later-observed evidence cannot be promoted to fact; recovered contradiction flags match known truth at a higher computed rate than promoting every forecast (ADR 0016). - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/Cargo.lock b/Cargo.lock index 372a55f43..985f058de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,6 +847,10 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prediction_contradiction" +version = "0.1.0" + [[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..37af2c0ba 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ 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 +The eleven bounded crates compile independently but intentionally expose no placeholder production APIs. Domain behavior begins in Task 2 with immutable evidence identifiers and source records. @@ -22,6 +22,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..4c393a739 --- /dev/null +++ b/crates/prediction_contradiction/Cargo.toml @@ -0,0 +1,17 @@ +[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 + +[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..25cbe28ee --- /dev/null +++ b/crates/prediction_contradiction/src/error.rs @@ -0,0 +1,48 @@ +//! 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 { + /// A predicted interval was disjoint from later-observed evidence. + PredictionContradictsObservation, + /// An interval or recovery slice was empty, inverted, or length-mismatched. + InvalidIntervalPayload, +} + +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::InvalidIntervalPayload => "invalid prediction-contradiction payload", + }; + 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::InvalidIntervalPayload, + "invalid prediction-contradiction payload", + ), + ] { + 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..8abf9ef6f --- /dev/null +++ b/crates/prediction_contradiction/src/interval.rs @@ -0,0 +1,135 @@ +//! Half-open event-time intervals and contradiction checks. + +use crate::PredictionContradictionError; + +/// One half-open event-time interval `[start, end)` in seconds. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ClosedEventInterval { + start_seconds: i64, + end_seconds: i64, +} + +impl ClosedEventInterval { + /// Construct a half-open interval with a strictly positive length. + /// + /// # Errors + /// + /// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when + /// `end_seconds` is not greater than `start_seconds`. + pub const fn new( + start_seconds: i64, + end_seconds: i64, + ) -> Result { + if end_seconds <= start_seconds { + return Err(PredictionContradictionError::InvalidIntervalPayload); + } + Ok(Self { + start_seconds, + end_seconds, + }) + } + + /// Inclusive start bound in seconds. + #[must_use] + pub const fn start_seconds(self) -> i64 { + self.start_seconds + } + + /// Exclusive end bound in seconds. + #[must_use] + pub const fn end_seconds(self) -> i64 { + self.end_seconds + } +} + +/// Return whether two half-open intervals are disjoint. +/// +/// # Errors +/// +/// This function is infallible for validated intervals and exists to keep the +/// public comparison surface explicit. +#[allow(clippy::unnecessary_wraps)] +pub fn intervals_contradict( + predicted: ClosedEventInterval, + observed: ClosedEventInterval, +) -> Result { + Ok(predicted.end_seconds <= observed.start_seconds + || observed.end_seconds <= predicted.start_seconds) +} + +/// Refuse to promote a contradicting prediction to observed fact. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::PredictionContradictsObservation`] +/// when the intervals are disjoint. +pub fn refuse_promotion_when_contradict( + predicted: ClosedEventInterval, + observed: ClosedEventInterval, +) -> Result<(), PredictionContradictionError> { + if intervals_contradict(predicted, observed)? { + return Err(PredictionContradictionError::PredictionContradictsObservation); + } + Ok(()) +} + +/// Fraction of recovered contradiction flags that match known truth. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when +/// either slice is empty or the lengths differ. +pub fn contradiction_recovery_rate( + truth: &[bool], + decided: &[bool], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(PredictionContradictionError::InvalidIntervalPayload); + } + let mut matches = 0_u32; + for (truth_flag, decided_flag) in truth.iter().zip(decided) { + if truth_flag == decided_flag { + matches += 1; + } + } + Ok(f64::from(matches) / truth.len() as f64) +} + +#[cfg(test)] +mod tests { + use super::{ + ClosedEventInterval, contradiction_recovery_rate, intervals_contradict, + refuse_promotion_when_contradict, + }; + use crate::PredictionContradictionError; + + #[test] + fn local_branches_cover_overlap_disjoint_and_payloads() { + let predicted = ClosedEventInterval::new(0, 10).expect("predicted"); + let observed = ClosedEventInterval::new(20, 30).expect("observed"); + assert_eq!(predicted.start_seconds(), 0); + assert_eq!(predicted.end_seconds(), 10); + assert!(intervals_contradict(predicted, observed).expect("disjoint")); + assert_eq!( + refuse_promotion_when_contradict(predicted, observed), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + let overlap = ClosedEventInterval::new(5, 15).expect("overlap"); + refuse_promotion_when_contradict(predicted, overlap).expect("consistent"); + assert!(!intervals_contradict(predicted, overlap).expect("overlap")); + assert_eq!( + ClosedEventInterval::new(4, 4), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + let matched = contradiction_recovery_rate(&[true], &[true]).expect("rate"); + assert!((matched - 1.0).abs() < f64::EPSILON); + assert_eq!( + contradiction_recovery_rate(&[], &[]), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + contradiction_recovery_rate(&[true], &[]), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + } +} diff --git a/crates/prediction_contradiction/src/lib.rs b/crates/prediction_contradiction/src/lib.rs new file mode 100644 index 000000000..2cc4eaba3 --- /dev/null +++ b/crates/prediction_contradiction/src/lib.rs @@ -0,0 +1,21 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Predicted intervals that contradict observations stay hypothetical. +//! +//! A CHRONOS-style forecast cannot be promoted to an observed event when its +//! event-time interval is disjoint from later-observed evidence (ADR 0016). + +mod error; +mod interval; + +/// Fail-closed prediction-contradiction errors. +pub use error::PredictionContradictionError; +/// One half-open event-time interval. +pub use interval::ClosedEventInterval; +/// Fraction of recovered contradiction flags that match known truth. +pub use interval::contradiction_recovery_rate; +/// Return whether two half-open intervals are disjoint. +pub use interval::intervals_contradict; +/// Refuse to promote a contradicting prediction to observed fact. +pub use interval::refuse_promotion_when_contradict; diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs new file mode 100644 index 000000000..5b2ad3a2a --- /dev/null +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -0,0 +1,77 @@ +//! Contradicting predictions cannot be promoted to observed fact. + +use prediction_contradiction::{ + ClosedEventInterval, PredictionContradictionError, contradiction_recovery_rate, + intervals_contradict, refuse_promotion_when_contradict, +}; + +fn interval(start: i64, end: i64) -> ClosedEventInterval { + ClosedEventInterval::new(start, end).expect("interval") +} + +#[test] +fn disjoint_prediction_cannot_become_observed_fact() { + let predicted = interval(0, 10); + let observed = interval(20, 30); + assert!(intervals_contradict(predicted, observed).expect("compare")); + assert_eq!( + refuse_promotion_when_contradict(predicted, observed), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + let overlapping = interval(5, 15); + refuse_promotion_when_contradict(predicted, overlapping).expect("consistent"); + assert!(!intervals_contradict(predicted, overlapping).expect("overlap")); +} + +#[test] +fn recovered_contradictions_match_known_truth_better_than_promoting_all() { + let pairs = [ + (interval(0, 10), interval(20, 30)), + (interval(0, 10), interval(5, 15)), + (interval(40, 50), interval(0, 10)), + ]; + let truth = [true, false, true]; + let recovered = [ + intervals_contradict(pairs[0].0, pairs[0].1).expect("p0"), + intervals_contradict(pairs[1].0, pairs[1].1).expect("p1"), + intervals_contradict(pairs[2].0, pairs[2].1).expect("p2"), + ]; + let collapsed = [false, false, false]; + let recovered_rate = contradiction_recovery_rate(&truth, &recovered).expect("recovered"); + let collapsed_rate = contradiction_recovery_rate(&truth, &collapsed).expect("collapsed"); + let expected = { + let mut matches = 0_u32; + for (truth_flag, decided_flag) in truth.iter().zip(recovered.iter()) { + if truth_flag == decided_flag { + matches += 1; + } + } + f64::from(matches) / f64::from(u32::try_from(truth.len()).expect("len")) + }; + assert!((recovered_rate - expected).abs() < f64::EPSILON); + assert!(recovered_rate > collapsed_rate); +} + +#[test] +fn empty_or_invalid_interval_payloads_fail_closed() { + assert_eq!( + ClosedEventInterval::new(10, 10), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + ClosedEventInterval::new(10, 9), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + contradiction_recovery_rate(&[], &[]), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + contradiction_recovery_rate(&[true], &[]), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + contradiction_recovery_rate(&[true, false], &[true]), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); +} 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 c29d97433..d96ee0e32 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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` predicted-vs-observed disjoint gate on the active PR; remaining TDT/CHRONOS tasks stay accepted-target | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index b85ee0b4a..1ba375c89 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -1,7 +1,7 @@ # ADR 0016 — TDT, CHRONOS, and Event Ontology intelligence boundary **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** active-PR **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 1a9a7b315..1d54ffea2 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, and tenant RLS implemented; full physical ERD remaining. | | [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 | Predicted-vs-observed contradiction gate in `prediction_contradiction` on the active PR; remaining TDT/CHRONOS tasks stay accepted-target. | ## Decision ownership summary diff --git a/docs/research/prediction-contradiction-gate.md b/docs/research/prediction-contradiction-gate.md new file mode 100644 index 000000000..0237d78f5 --- /dev/null +++ b/docs/research/prediction-contradiction-gate.md @@ -0,0 +1,31 @@ +# Predicted-versus-observed temporal contradiction (doctoring) + +## Scope + +`prediction_contradiction` compares half-open event-time intervals. A +predicted interval that is disjoint from later-observed evidence cannot be +promoted to fact. Recovery is the computed share of contradiction flags that +match known truth. + +This slice does not run the `temporal_core` path-consistency reasoner, fit +CHRONOS schemas, or claim a unique interval algebra. + +## 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. + +### Supporting literature + +Allen (1983) defines thirteen interval relations, including disjoint +`before`/`after`. Disjointness is sufficient to refuse promotion; this +crate does not implement the full composition table. + +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 b4b144684..95acf9df9 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -96,6 +96,8 @@ Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. World TEPP separates stable record identity, content equality, exact text location, wire representation, authorization, and provenance. JSON wire records are explicit versioned DTOs with unknown-field rejection and reconstruct through domain validation. `SHA-256` detects content substitution but is not treated as proof of origin, authority, or chain of custody. +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. *Communications of the ACM, 26*(11), 832–843. https://doi.org/10.1145/182.358434. Disjoint `before`/`after` relations inform `prediction_contradiction`; the crate does not implement Allen composition. + ## AI risk, management systems, and assurance readiness International Organization for Standardization. (2023a). *Information technology—Artificial intelligence—Guidance on risk management* (ISO/IEC Standard No. 23894:2023). https://www.iso.org/standard/77304.html diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0f..f7634d698 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | 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 | +| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | contradiction-flag recovery vs promote-all | 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, ...] = ( From 5daed33eab4558f4e578c2bdd4e503c14fb7db64 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 14:42:43 +0000 Subject: [PATCH 02/13] fix(quality): bind crate-root count to the workspace contract prediction_contradiction is an eleventh crate. The docstring discovery test now compares against EXPECTED_CRATES instead of a hardcoded 10. Co-authored-by: Seongho Bae --- tests/quality/test_check_docstrings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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), []) From 3b309351c4f12e163ce021a5abfa2883e65cef27 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 14:57:04 +0000 Subject: [PATCH 03/13] fix(temporal): classify promotion with temporal_core Allen relations ClosedEventInterval was half-open and treated meets as contradiction. Use temporal_core closed intervals, refuse before/after as contradiction and meets/met_by as unsupported adjacency, and fail closed when availability exceeds cutoff. Relabel agreement so it is not RMSE recovery. Co-authored-by: Seongho Bae --- ARCHITECTURE.md | 7 +- CHANGELOG.md | 2 +- Cargo.lock | 3 + README.md | 9 +- crates/prediction_contradiction/Cargo.toml | 3 + crates/prediction_contradiction/src/error.rs | 29 ++- .../prediction_contradiction/src/interval.rs | 234 ++++++++++++------ crates/prediction_contradiction/src/lib.rs | 21 +- .../tests/contradiction_contract.rs | 193 +++++++++++---- docs/TRACEABILITY.md | 2 +- ...tdt-chronos-event-intelligence-boundary.md | 3 +- docs/adr/README.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 12 + .../research/prediction-contradiction-gate.md | 28 ++- docs/research/standards-and-literature.md | 4 +- docs/validation/temporal-event-foundation.md | 2 +- 16 files changed, 387 insertions(+), 167 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4cd1a25b0..3f6b2196b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,11 +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` | predicted event-time intervals that contradict observations stay hypothetical | +| `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 6a8482625..44a07a266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added -- `prediction_contradiction` promotion gate: half-open event-time intervals that are disjoint from later-observed evidence cannot be promoted to fact; recovered contradiction flags match known truth at a higher computed rate than promoting every forecast (ADR 0016). +- `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). - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/Cargo.lock b/Cargo.lock index 985f058de..f35579c9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,6 +850,9 @@ dependencies = [ [[package]] name = "prediction_contradiction" version = "0.1.0" +dependencies = [ + "temporal_core", +] [[package]] name = "proc-macro2" diff --git a/README.md b/README.md index 37af2c0ba..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 eleven 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 diff --git a/crates/prediction_contradiction/Cargo.toml b/crates/prediction_contradiction/Cargo.toml index 4c393a739..b656fb754 100644 --- a/crates/prediction_contradiction/Cargo.toml +++ b/crates/prediction_contradiction/Cargo.toml @@ -13,5 +13,8 @@ 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 index 25cbe28ee..257564f65 100644 --- a/crates/prediction_contradiction/src/error.rs +++ b/crates/prediction_contradiction/src/error.rs @@ -6,10 +6,16 @@ use std::fmt; #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum PredictionContradictionError { - /// A predicted interval was disjoint from later-observed evidence. + /// Predicted and observed event-time intervals are Allen `before` or `after`. PredictionContradictsObservation, - /// An interval or recovery slice was empty, inverted, or length-mismatched. + /// Predicted and observed intervals meet but do not overlap in their interiors. + PredictionLacksOverlappingSupport, + /// 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 { @@ -18,7 +24,14 @@ impl fmt::Display for PredictionContradictionError { Self::PredictionContradictsObservation => { "predicted interval contradicts observed evidence" } + Self::PredictionLacksOverlappingSupport => { + "predicted interval meets observation without overlapping support" + } + 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) } @@ -37,10 +50,22 @@ mod tests { PredictionContradictionError::PredictionContradictsObservation, "predicted interval contradicts observed evidence", ), + ( + PredictionContradictionError::PredictionLacksOverlappingSupport, + "predicted interval meets observation without overlapping support", + ), + ( + 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 index 8abf9ef6f..30a648b1c 100644 --- a/crates/prediction_contradiction/src/interval.rs +++ b/crates/prediction_contradiction/src/interval.rs @@ -1,90 +1,105 @@ -//! Half-open event-time intervals and contradiction checks. +//! Predicted-versus-observed promotion using `temporal_core` Allen classification. use crate::PredictionContradictionError; +use temporal_core::{ + AllenRelation, AvailableTime, EventTime, KnowledgeCutoff, TemporalError, TemporalInterval, + classify_interval_relation, +}; -/// One half-open event-time interval `[start, end)` in seconds. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ClosedEventInterval { - start_seconds: i64, - end_seconds: i64, +fn map_temporal(error: TemporalError) -> PredictionContradictionError { + let _ = error; + PredictionContradictionError::InvalidIntervalPayload } -impl ClosedEventInterval { - /// Construct a half-open interval with a strictly positive length. - /// - /// # Errors - /// - /// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when - /// `end_seconds` is not greater than `start_seconds`. - pub const fn new( - start_seconds: i64, - end_seconds: i64, - ) -> Result { - if end_seconds <= start_seconds { - return Err(PredictionContradictionError::InvalidIntervalPayload); - } - Ok(Self { - start_seconds, - end_seconds, - }) - } - - /// Inclusive start bound in seconds. - #[must_use] - pub const fn start_seconds(self) -> i64 { - self.start_seconds - } - - /// Exclusive end bound in seconds. - #[must_use] - pub const fn end_seconds(self) -> i64 { - self.end_seconds - } -} - -/// Return whether two half-open intervals are disjoint. +/// 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 /// -/// This function is infallible for validated intervals and exists to keep the -/// public comparison surface explicit. -#[allow(clippy::unnecessary_wraps)] +/// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when either +/// interval is not a closed proper Allen input. pub fn intervals_contradict( - predicted: ClosedEventInterval, - observed: ClosedEventInterval, + predicted: &TemporalInterval, + observed: &TemporalInterval, ) -> Result { - Ok(predicted.end_seconds <= observed.start_seconds - || observed.end_seconds <= predicted.start_seconds) + 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 to promote a contradicting prediction to observed fact. +/// 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::PredictionContradictsObservation`] -/// when the intervals are disjoint. -pub fn refuse_promotion_when_contradict( - predicted: ClosedEventInterval, - observed: ClosedEventInterval, +/// Returns [`PredictionContradictionError::EvidenceAfterCutoff`] when +/// `observed_available` is later than `cutoff`. Returns +/// [`PredictionContradictionError::PredictionContradictsObservation`] for +/// Allen `before` / `after`. Returns +/// [`PredictionContradictionError::PredictionLacksOverlappingSupport`] for +/// `meets` / `met_by`. 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 intervals_contradict(predicted, observed)? { - return Err(PredictionContradictionError::PredictionContradictsObservation); + 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::Overlaps + | AllenRelation::OverlappedBy + | AllenRelation::Starts + | AllenRelation::StartedBy + | AllenRelation::During + | AllenRelation::Contains + | AllenRelation::Finishes + | AllenRelation::FinishedBy + | AllenRelation::Equals => Ok(()), } - Ok(()) } -/// Fraction of recovered contradiction flags that match known truth. +/// 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::InvalidIntervalPayload`] when +/// Returns [`PredictionContradictionError::AgreementSliceMismatch`] when /// either slice is empty or the lengths differ. -pub fn contradiction_recovery_rate( +pub fn contradiction_agreement_rate( truth: &[bool], decided: &[bool], ) -> Result { if truth.is_empty() || truth.len() != decided.len() { - return Err(PredictionContradictionError::InvalidIntervalPayload); + return Err(PredictionContradictionError::AgreementSliceMismatch); } let mut matches = 0_u32; for (truth_flag, decided_flag) in truth.iter().zip(decided) { @@ -92,44 +107,101 @@ pub fn contradiction_recovery_rate( matches += 1; } } - Ok(f64::from(matches) / truth.len() as f64) + #[allow(clippy::cast_precision_loss)] + let rate = f64::from(matches) / truth.len() as f64; + Ok(rate) } #[cfg(test)] mod tests { - use super::{ - ClosedEventInterval, contradiction_recovery_rate, intervals_contradict, - refuse_promotion_when_contradict, - }; + 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_overlap_disjoint_and_payloads() { - let predicted = ClosedEventInterval::new(0, 10).expect("predicted"); - let observed = ClosedEventInterval::new(20, 30).expect("observed"); - assert_eq!(predicted.start_seconds(), 0); - assert_eq!(predicted.end_seconds(), 10); - assert!(intervals_contradict(predicted, observed).expect("disjoint")); + 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")); + refuse_promotion(&predicted, &closed(5, 15), available, cutoff).expect("overlap"); + refuse_promotion(&predicted, &closed(0, 8), available, cutoff).expect("started_by"); + refuse_promotion(&closed(0, 8), &predicted, available, cutoff).expect("starts"); + refuse_promotion(&predicted, &closed(2, 8), available, cutoff).expect("contains"); + refuse_promotion(&closed(2, 8), &predicted, available, cutoff).expect("during"); + refuse_promotion(&predicted, &closed(2, 10), available, cutoff).expect("finished_by"); + refuse_promotion(&closed(2, 10), &predicted, available, cutoff).expect("finishes"); + refuse_promotion(&predicted, &closed(0, 10), available, cutoff).expect("equals"); assert_eq!( - refuse_promotion_when_contradict(predicted, observed), + refuse_promotion(&predicted, &closed(20, 30), available, cutoff), Err(PredictionContradictionError::PredictionContradictsObservation) ); - let overlap = ClosedEventInterval::new(5, 15).expect("overlap"); - refuse_promotion_when_contradict(predicted, overlap).expect("consistent"); - assert!(!intervals_contradict(predicted, overlap).expect("overlap")); assert_eq!( - ClosedEventInterval::new(4, 4), - Err(PredictionContradictionError::InvalidIntervalPayload) + refuse_promotion(&predicted, &closed(10, 20), available, cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) ); - let matched = contradiction_recovery_rate(&[true], &[true]).expect("rate"); - assert!((matched - 1.0).abs() < f64::EPSILON); + 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!( - contradiction_recovery_rate(&[], &[]), + intervals_contradict(&half_open, &closed(20, 30)), Err(PredictionContradictionError::InvalidIntervalPayload) ); assert_eq!( - contradiction_recovery_rate(&[true], &[]), + 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 index 2cc4eaba3..8f17fac3d 100644 --- a/crates/prediction_contradiction/src/lib.rs +++ b/crates/prediction_contradiction/src/lib.rs @@ -1,21 +1,22 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] -#![allow(clippy::cast_precision_loss)] //! Predicted intervals that contradict observations stay hypothetical. //! -//! A CHRONOS-style forecast cannot be promoted to an observed event when its -//! event-time interval is disjoint from later-observed evidence (ADR 0016). +//! A forecast cannot be promoted to an observed event when +//! [`temporal_core::classify_interval_relation`] returns Allen `before` or +//! `after`, or when the pair only `meets` / is `met_by`. 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; -/// One half-open event-time interval. -pub use interval::ClosedEventInterval; -/// Fraction of recovered contradiction flags that match known truth. -pub use interval::contradiction_recovery_rate; -/// Return whether two half-open intervals are disjoint. +/// 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 to promote a contradicting prediction to observed fact. -pub use interval::refuse_promotion_when_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 index 5b2ad3a2a..fced14397 100644 --- a/crates/prediction_contradiction/tests/contradiction_contract.rs +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -1,77 +1,172 @@ -//! Contradicting predictions cannot be promoted to observed fact. +//! Predicted intervals stay hypothetical unless later-available evidence overlaps. use prediction_contradiction::{ - ClosedEventInterval, PredictionContradictionError, contradiction_recovery_rate, - intervals_contradict, refuse_promotion_when_contradict, + 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 interval(start: i64, end: i64) -> ClosedEventInterval { - ClosedEventInterval::new(start, end).expect("interval") +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 disjoint_prediction_cannot_become_observed_fact() { - let predicted = interval(0, 10); - let observed = interval(20, 30); - assert!(intervals_contradict(predicted, observed).expect("compare")); +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_when_contradict(predicted, observed), + 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) ); - let overlapping = interval(5, 15); - refuse_promotion_when_contradict(predicted, overlapping).expect("consistent"); - assert!(!intervals_contradict(predicted, overlapping).expect("overlap")); } #[test] -fn recovered_contradictions_match_known_truth_better_than_promoting_all() { - let pairs = [ - (interval(0, 10), interval(20, 30)), - (interval(0, 10), interval(5, 15)), - (interval(40, 50), interval(0, 10)), - ]; - let truth = [true, false, true]; - let recovered = [ - intervals_contradict(pairs[0].0, pairs[0].1).expect("p0"), - intervals_contradict(pairs[1].0, pairs[1].1).expect("p1"), - intervals_contradict(pairs[2].0, pairs[2].1).expect("p2"), - ]; - let collapsed = [false, false, false]; - let recovered_rate = contradiction_recovery_rate(&truth, &recovered).expect("recovered"); - let collapsed_rate = contradiction_recovery_rate(&truth, &collapsed).expect("collapsed"); - let expected = { - let mut matches = 0_u32; - for (truth_flag, decided_flag) in truth.iter().zip(recovered.iter()) { - if truth_flag == decided_flag { - matches += 1; - } - } - f64::from(matches) / f64::from(u32::try_from(truth.len()).expect("len")) - }; - assert!((recovered_rate - expected).abs() < f64::EPSILON); - assert!(recovered_rate > collapsed_rate); +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 empty_or_invalid_interval_payloads_fail_closed() { +fn overlapping_observation_may_support_promotion() { + 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")); + refuse_promotion( + &predicted, + &overlapping, + observed_available, + knowledge_cutoff, + ) + .expect("overlapping support"); +} + +#[test] +fn evidence_available_after_cutoff_is_ineligible() { + let predicted = closed_event_interval(0, 10); + let overlapping = closed_event_interval(5, 15); assert_eq!( - ClosedEventInterval::new(10, 10), - Err(PredictionContradictionError::InvalidIntervalPayload) + 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!( - ClosedEventInterval::new(10, 9), + 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_recovery_rate(&[], &[]), - Err(PredictionContradictionError::InvalidIntervalPayload) + contradiction_agreement_rate(&[], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) ); assert_eq!( - contradiction_recovery_rate(&[true], &[]), - Err(PredictionContradictionError::InvalidIntervalPayload) + contradiction_agreement_rate(&[true], &[]), + Err(PredictionContradictionError::AgreementSliceMismatch) ); assert_eq!( - contradiction_recovery_rate(&[true, false], &[true]), - Err(PredictionContradictionError::InvalidIntervalPayload) + contradiction_agreement_rate(&[true, false], &[true]), + Err(PredictionContradictionError::AgreementSliceMismatch) ); } diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d96ee0e32..b0f8e45fc 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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 | `prediction_contradiction` predicted-vs-observed disjoint gate on the active PR; remaining TDT/CHRONOS tasks stay accepted-target | active-PR | +| 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 | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index 1ba375c89..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:** active-PR +**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 1d54ffea2..04917e2fe 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, and tenant RLS implemented; full physical ERD remaining. | | [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 | active-PR | Predicted-vs-observed contradiction gate in `prediction_contradiction` on the active PR; remaining TDT/CHRONOS tasks stay accepted-target. | +| [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 index 0237d78f5..6984efc65 100644 --- a/docs/research/prediction-contradiction-gate.md +++ b/docs/research/prediction-contradiction-gate.md @@ -1,14 +1,21 @@ -# Predicted-versus-observed temporal contradiction (doctoring) +# Predicted-versus-observed temporal contradiction ## Scope -`prediction_contradiction` compares half-open event-time intervals. A -predicted interval that is disjoint from later-observed evidence cannot be -promoted to fact. Recovery is the computed share of contradiction flags that -match known truth. +`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, or claim a unique interval algebra. +CHRONOS schemas, extract TDT tracks, or claim that the full ADR 0016 +intelligence stack is implemented. ## Authority @@ -18,13 +25,14 @@ CHRONOS schemas, or claim a unique interval algebra. 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. + the clock for occurrence intervals; availability may not exceed cutoff. ### Supporting literature -Allen (1983) defines thirteen interval relations, including disjoint -`before`/`after`. Disjointness is sufficient to refuse promotion; this -crate does not implement the full composition table. +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. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 95acf9df9..3615c2f47 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 @@ -96,8 +98,6 @@ Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. World TEPP separates stable record identity, content equality, exact text location, wire representation, authorization, and provenance. JSON wire records are explicit versioned DTOs with unknown-field rejection and reconstruct through domain validation. `SHA-256` detects content substitution but is not treated as proof of origin, authority, or chain of custody. -Allen, J. F. (1983). Maintaining knowledge about temporal intervals. *Communications of the ACM, 26*(11), 832–843. https://doi.org/10.1145/182.358434. Disjoint `before`/`after` relations inform `prediction_contradiction`; the crate does not implement Allen composition. - ## AI risk, management systems, and assurance readiness International Organization for Standardization. (2023a). *Information technology—Artificial intelligence—Guidance on risk management* (ISO/IEC Standard No. 23894:2023). https://www.iso.org/standard/77304.html diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index f7634d698..ca4b0d11f 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | 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 | -| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | contradiction-flag recovery vs promote-all | ADR 0016 | +| 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 | From 13ed4e738d83ef3c597326adb8e4f8c9a2e26c5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:11:18 +0000 Subject: [PATCH 04/13] feat(temporal): require observed coverage before promotion refuse_promotion still answers contradiction and adjacency only. require_observed_coverage refuses unmatched predicted mass unless the observed interval covers the forecast (during, starts, finishes, equals). Co-authored-by: Seongho Bae --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 2 +- crates/prediction_contradiction/Cargo.toml | 2 +- crates/prediction_contradiction/src/error.rs | 15 +- .../prediction_contradiction/src/interval.rs | 230 +++++++++++++++++- crates/prediction_contradiction/src/lib.rs | 16 +- .../tests/contradiction_contract.rs | 193 ++++++++++++++- docs/TRACEABILITY.md | 2 +- ...tdt-chronos-event-intelligence-boundary.md | 2 +- docs/adr/README.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 4 +- .../research/prediction-contradiction-gate.md | 31 ++- docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 2 +- 14 files changed, 476 insertions(+), 29 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3f6b2196b..3a5160b78 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +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 | -| `prediction_contradiction` | Allen promotion gate: `before`/`after` stay contradictory; `meets`/`met_by` stay unsupported | +| `prediction_contradiction` | Allen promotion gate: `before`/`after` stay contradictory; `meets`/`met_by` stay unsupported; coverage is required before unmatched predicted mass can become fact | Foundation crates expose only tested contracts. Empty façades are not public APIs. diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a07a266..e24db2a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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). +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; `require_observed_coverage` refuses partial overlap that leaves unmatched predicted mass; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/crates/prediction_contradiction/Cargo.toml b/crates/prediction_contradiction/Cargo.toml index b656fb754..65619f32b 100644 --- a/crates/prediction_contradiction/Cargo.toml +++ b/crates/prediction_contradiction/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prediction_contradiction" -description = "Predicted intervals that contradict observations cannot become fact." +description = "Predicted intervals stay hypothetical unless later evidence covers them." version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/prediction_contradiction/src/error.rs b/crates/prediction_contradiction/src/error.rs index 257564f65..98073675c 100644 --- a/crates/prediction_contradiction/src/error.rs +++ b/crates/prediction_contradiction/src/error.rs @@ -8,8 +8,10 @@ use std::fmt; 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. + /// Predicted and observed intervals are adjacent and do not overlap in their interiors. PredictionLacksOverlappingSupport, + /// Observed evidence overlaps the prediction but does not cover it. + PredictionNotCoveredByObservation, /// Observed evidence became available after the analysis knowledge cutoff. EvidenceAfterCutoff, /// An interval is not a closed proper Allen input. @@ -25,7 +27,10 @@ impl fmt::Display for PredictionContradictionError { "predicted interval contradicts observed evidence" } Self::PredictionLacksOverlappingSupport => { - "predicted interval meets observation without overlapping support" + "predicted interval is adjacent to observation without overlapping support" + } + Self::PredictionNotCoveredByObservation => { + "observed evidence does not cover the predicted interval" } Self::EvidenceAfterCutoff => { "observed evidence is available after the knowledge cutoff" @@ -52,7 +57,11 @@ mod tests { ), ( PredictionContradictionError::PredictionLacksOverlappingSupport, - "predicted interval meets observation without overlapping support", + "predicted interval is adjacent to observation without overlapping support", + ), + ( + PredictionContradictionError::PredictionNotCoveredByObservation, + "observed evidence does not cover the predicted interval", ), ( PredictionContradictionError::EvidenceAfterCutoff, diff --git a/crates/prediction_contradiction/src/interval.rs b/crates/prediction_contradiction/src/interval.rs index 30a648b1c..6d6d23286 100644 --- a/crates/prediction_contradiction/src/interval.rs +++ b/crates/prediction_contradiction/src/interval.rs @@ -11,6 +11,49 @@ fn map_temporal(error: TemporalError) -> PredictionContradictionError { PredictionContradictionError::InvalidIntervalPayload } +/// How later-observed evidence relates to a predicted event-time interval. +/// +/// `Ok(())` from [`refuse_promotion`] means the pair is not an Allen +/// contradiction or adjacency refusal. It does not authorize promoting +/// unmatched predicted mass. Only [`PromotionSupport::ObservedCoversPrediction`] +/// means every predicted instant has observed support. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PromotionSupport { + /// Observed interval covers every instant of the predicted interval. + ObservedCoversPrediction, + /// Interiors overlap, but some predicted mass has no observed support. + PartialOverlap, + /// Intervals share an endpoint and have no interior overlap. + AdjacentWithoutOverlap, + /// Intervals are strictly disjoint with a gap. + ContradictoryDisjoint, +} + +/// Classify predicted-versus-observed support without applying cutoff policy. +/// +/// # Errors +/// +/// Returns [`PredictionContradictionError::InvalidIntervalPayload`] when either +/// interval is not a closed proper Allen input. +pub fn classify_promotion_support( + predicted: &TemporalInterval, + observed: &TemporalInterval, +) -> Result { + match classify_interval_relation(predicted, observed).map_err(map_temporal)? { + AllenRelation::Before | AllenRelation::After => Ok(PromotionSupport::ContradictoryDisjoint), + AllenRelation::Meets | AllenRelation::MetBy => Ok(PromotionSupport::AdjacentWithoutOverlap), + AllenRelation::Overlaps + | AllenRelation::OverlappedBy + | AllenRelation::Contains + | AllenRelation::StartedBy + | AllenRelation::FinishedBy => Ok(PromotionSupport::PartialOverlap), + AllenRelation::Starts + | AllenRelation::During + | AllenRelation::Finishes + | AllenRelation::Equals => Ok(PromotionSupport::ObservedCoversPrediction), + } +} + /// Return whether two closed proper intervals are Allen `before` or `after`. /// /// Adjacent `meets` / `met_by` pairs are not contradictions. They share an @@ -43,6 +86,11 @@ pub fn intervals_contradict( /// Refuse promotion when evidence is ineligible, disjoint, or only adjacent. /// +/// Success means the pair is not an Allen `before` / `after` contradiction +/// and is not merely adjacent. Callers must not treat success as authority +/// to promote unmatched predicted shoulders. Use +/// [`require_observed_coverage`] when the predicted interval must be covered. +/// /// This function classifies intervals with /// [`temporal_core::classify_interval_relation`]. It does not run the /// path-consistency reasoner. @@ -85,6 +133,46 @@ pub fn refuse_promotion( } } +/// Refuse promotion unless later-observed evidence covers the prediction. +/// +/// Coverage requires Allen `during`, `starts`, `finishes`, or `equals`. +/// Partial overlap leaves unmatched predicted mass and stays hypothetical. +/// +/// # 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`. Returns +/// [`PredictionContradictionError::PredictionNotCoveredByObservation`] for +/// partial overlap. Returns +/// [`PredictionContradictionError::InvalidIntervalPayload`] when either +/// interval is not a closed proper Allen input. +pub fn require_observed_coverage( + predicted: &TemporalInterval, + observed: &TemporalInterval, + observed_available: AvailableTime, + cutoff: KnowledgeCutoff, +) -> Result<(), PredictionContradictionError> { + if observed_available.instant() > cutoff.instant() { + return Err(PredictionContradictionError::EvidenceAfterCutoff); + } + match classify_promotion_support(predicted, observed)? { + PromotionSupport::ObservedCoversPrediction => Ok(()), + PromotionSupport::PartialOverlap => { + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + } + PromotionSupport::AdjacentWithoutOverlap => { + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + } + PromotionSupport::ContradictoryDisjoint => { + Err(PredictionContradictionError::PredictionContradictsObservation) + } + } +} + /// Fraction of contradiction flags that match independently supplied labels. /// /// This is a label-agreement helper for the promotion gate. It is not RMSE, @@ -114,7 +202,10 @@ pub fn contradiction_agreement_rate( #[cfg(test)] mod tests { - use super::{contradiction_agreement_rate, intervals_contradict, refuse_promotion}; + use super::{ + PromotionSupport, classify_promotion_support, contradiction_agreement_rate, + intervals_contradict, refuse_promotion, require_observed_coverage, + }; use crate::PredictionContradictionError; use temporal_core::{ AvailableTime, EventTime, KnowledgeCutoff, TemporalBoundary, TemporalInterval, @@ -142,8 +233,7 @@ mod tests { } #[test] - fn local_branches_cover_relations_cutoff_and_agreement() { - let (available, cutoff) = clocks(); + fn intervals_contradict_only_before_and_after() { let predicted = closed(0, 10); assert!(intervals_contradict(&predicted, &closed(20, 30)).expect("before")); assert!(intervals_contradict(&closed(40, 50), &predicted).expect("after")); @@ -158,7 +248,14 @@ mod tests { 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")); + } + + #[test] + fn refuse_promotion_accepts_overlap_family_and_refuses_gaps() { + let (available, cutoff) = clocks(); + let predicted = closed(0, 10); refuse_promotion(&predicted, &closed(5, 15), available, cutoff).expect("overlap"); + refuse_promotion(&closed(5, 15), &predicted, available, cutoff).expect("overlapped_by"); refuse_promotion(&predicted, &closed(0, 8), available, cutoff).expect("started_by"); refuse_promotion(&closed(0, 8), &predicted, available, cutoff).expect("starts"); refuse_promotion(&predicted, &closed(2, 8), available, cutoff).expect("contains"); @@ -170,15 +267,130 @@ mod tests { refuse_promotion(&predicted, &closed(20, 30), available, cutoff), Err(PredictionContradictionError::PredictionContradictsObservation) ); + assert_eq!( + refuse_promotion(&closed(40, 50), &predicted, available, cutoff), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); assert_eq!( refuse_promotion(&predicted, &closed(10, 20), available, cutoff), Err(PredictionContradictionError::PredictionLacksOverlappingSupport) ); + } + + #[test] + fn classify_promotion_support_labels_all_thirteen_relations() { + let predicted = closed(0, 10); + assert_eq!( + classify_promotion_support(&predicted, &closed(20, 30)).expect("before"), + PromotionSupport::ContradictoryDisjoint + ); + assert_eq!( + classify_promotion_support(&closed(40, 50), &predicted).expect("after"), + PromotionSupport::ContradictoryDisjoint + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(10, 20)).expect("meets"), + PromotionSupport::AdjacentWithoutOverlap + ); + assert_eq!( + classify_promotion_support(&closed(10, 20), &predicted).expect("met_by"), + PromotionSupport::AdjacentWithoutOverlap + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(5, 15)).expect("overlaps"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + classify_promotion_support(&closed(5, 15), &predicted).expect("overlapped_by"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(0, 8)).expect("started_by"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(2, 8)).expect("contains"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(2, 10)).expect("finished_by"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + classify_promotion_support(&closed(0, 8), &predicted).expect("starts"), + PromotionSupport::ObservedCoversPrediction + ); + assert_eq!( + classify_promotion_support(&closed(2, 8), &predicted).expect("during"), + PromotionSupport::ObservedCoversPrediction + ); + assert_eq!( + classify_promotion_support(&closed(2, 10), &predicted).expect("finishes"), + PromotionSupport::ObservedCoversPrediction + ); + assert_eq!( + classify_promotion_support(&predicted, &closed(0, 10)).expect("equals"), + PromotionSupport::ObservedCoversPrediction + ); + } + + #[test] + fn require_observed_coverage_accepts_only_full_coverage() { + let (available, cutoff) = clocks(); + let predicted = closed(0, 10); + require_observed_coverage(&closed(0, 8), &predicted, available, cutoff).expect("starts"); + require_observed_coverage(&closed(2, 8), &predicted, available, cutoff).expect("during"); + require_observed_coverage(&closed(2, 10), &predicted, available, cutoff).expect("finishes"); + require_observed_coverage(&predicted, &closed(0, 10), available, cutoff).expect("equals"); + assert_eq!( + require_observed_coverage(&predicted, &closed(5, 15), available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + require_observed_coverage(&closed(5, 15), &predicted, available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + require_observed_coverage(&predicted, &closed(0, 8), available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + require_observed_coverage(&predicted, &closed(2, 8), available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + require_observed_coverage(&predicted, &closed(2, 10), available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + require_observed_coverage(&predicted, &closed(20, 30), available, cutoff), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + assert_eq!( + require_observed_coverage(&predicted, &closed(10, 20), available, cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); + } + + #[test] + fn cutoff_and_half_open_payloads_fail_closed() { + let (available, cutoff) = clocks(); + let predicted = closed(0, 10); + let on_cutoff = ( + AvailableTime::parse_rfc3339("2026-01-03T00:00:00Z").expect("on cutoff"), + KnowledgeCutoff::parse_rfc3339("2026-01-03T00:00:00Z").expect("cutoff"), + ); + require_observed_coverage(&predicted, &closed(0, 10), on_cutoff.0, on_cutoff.1) + .expect("available == cutoff"); 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) ); + assert_eq!( + require_observed_coverage(&predicted, &closed(0, 10), late, cutoff), + Err(PredictionContradictionError::EvidenceAfterCutoff) + ); let half_open = TemporalInterval::bounded( TemporalBoundary::Included(event_at(0)), TemporalBoundary::Excluded(event_at(10)), @@ -193,6 +405,18 @@ mod tests { refuse_promotion(&half_open, &closed(20, 30), available, cutoff), Err(PredictionContradictionError::InvalidIntervalPayload) ); + assert_eq!( + classify_promotion_support(&predicted, &half_open), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + require_observed_coverage(&predicted, &half_open, available, cutoff), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + } + + #[test] + fn agreement_rate_matches_or_fails_closed() { let matched = contradiction_agreement_rate(&[true], &[true]).expect("rate"); assert!((matched - 1.0).abs() < f64::EPSILON); assert_eq!( diff --git a/crates/prediction_contradiction/src/lib.rs b/crates/prediction_contradiction/src/lib.rs index 8f17fac3d..eae40fac8 100644 --- a/crates/prediction_contradiction/src/lib.rs +++ b/crates/prediction_contradiction/src/lib.rs @@ -4,19 +4,27 @@ //! //! A forecast cannot be promoted to an observed event when //! [`temporal_core::classify_interval_relation`] returns Allen `before` or -//! `after`, or when the pair only `meets` / is `met_by`. Evidence whose -//! availability time exceeds the analysis knowledge cutoff is ineligible -//! (ADR 0002, ADR 0016). This crate does not run the path-consistency -//! reasoner. +//! `after`, or when the pair only `meets` / is `met_by`. Partial overlap is +//! not a contradiction, but it also does not cover unmatched predicted +//! mass. [`require_observed_coverage`] is the promotion-authority gate. +//! 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; +/// How later-observed evidence relates to a predicted event-time interval. +pub use interval::PromotionSupport; +/// Classify predicted-versus-observed support without applying cutoff policy. +pub use interval::classify_promotion_support; /// 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; +/// Refuse promotion unless later-observed evidence covers the prediction. +pub use interval::require_observed_coverage; diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs index fced14397..ea1bc3962 100644 --- a/crates/prediction_contradiction/tests/contradiction_contract.rs +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -1,8 +1,9 @@ //! Predicted intervals stay hypothetical unless later-available evidence overlaps. use prediction_contradiction::{ - PredictionContradictionError, contradiction_agreement_rate, intervals_contradict, - refuse_promotion, + PredictionContradictionError, PromotionSupport, classify_promotion_support, + contradiction_agreement_rate, intervals_contradict, refuse_promotion, + require_observed_coverage, }; use temporal_core::{ AvailableTime, EventTime, KnowledgeCutoff, TemporalBoundary, TemporalInterval, @@ -89,7 +90,7 @@ fn meeting_intervals_are_adjacent_not_allen_contradiction() { } #[test] -fn overlapping_observation_may_support_promotion() { +fn overlapping_observation_is_not_contradiction_and_is_not_coverage() { let predicted = closed_event_interval(0, 10); let overlapping = closed_event_interval(5, 15); let (observed_available, knowledge_cutoff) = eligible_clocks(); @@ -100,7 +101,20 @@ fn overlapping_observation_may_support_promotion() { observed_available, knowledge_cutoff, ) - .expect("overlapping support"); + .expect("overlap is not Allen contradiction"); + assert_eq!( + classify_promotion_support(&predicted, &overlapping).expect("overlaps"), + PromotionSupport::PartialOverlap + ); + assert_eq!( + require_observed_coverage( + &predicted, + &overlapping, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); } #[test] @@ -170,3 +184,174 @@ fn empty_or_mismatched_agreement_slices_fail_closed() { Err(PredictionContradictionError::AgreementSliceMismatch) ); } + +#[test] +fn availability_equal_to_cutoff_remains_eligible() { + let predicted = closed_event_interval(0, 10); + let overlapping = closed_event_interval(5, 15); + let covering = closed_event_interval(0, 10); + let observed_available = available("2026-01-03T00:00:00Z"); + let knowledge_cutoff = cutoff("2026-01-03T00:00:00Z"); + refuse_promotion( + &predicted, + &overlapping, + observed_available, + knowledge_cutoff, + ) + .expect("available == cutoff is eligible for the contradiction filter"); + require_observed_coverage(&predicted, &covering, observed_available, knowledge_cutoff) + .expect("available == cutoff is eligible for coverage"); +} + +#[test] +fn after_and_overlapped_by_use_the_same_promotion_rules() { + let predicted = closed_event_interval(20, 30); + let earlier_observed = closed_event_interval(0, 10); + let overlapped_by = closed_event_interval(5, 15); + let later_predicted = closed_event_interval(10, 20); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + + assert!(intervals_contradict(&predicted, &earlier_observed).expect("after")); + assert_eq!( + refuse_promotion( + &predicted, + &earlier_observed, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); + assert_eq!( + classify_promotion_support(&later_predicted, &overlapped_by).expect("overlapped_by"), + PromotionSupport::PartialOverlap + ); + refuse_promotion( + &later_predicted, + &overlapped_by, + observed_available, + knowledge_cutoff, + ) + .expect("overlapped_by is not Allen contradiction"); + assert_eq!( + require_observed_coverage( + &later_predicted, + &overlapped_by, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); +} + +#[test] +fn half_open_observed_interval_is_not_an_allen_input() { + let predicted = closed_event_interval(0, 10); + let observed = TemporalInterval::bounded( + TemporalBoundary::Included(event_at(5)), + TemporalBoundary::Excluded(event_at(15)), + TemporalPrecision::Second, + ) + .expect("half-open observed interval is representable"); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + assert_eq!( + intervals_contradict(&predicted, &observed), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + refuse_promotion(&predicted, &observed, observed_available, knowledge_cutoff), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + classify_promotion_support(&predicted, &observed), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); + assert_eq!( + require_observed_coverage(&predicted, &observed, observed_available, knowledge_cutoff), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); +} + +#[test] +fn known_allen_pairs_recover_coverage_and_contradiction_labels() { + let cases = [ + ( + closed_event_interval(0, 10), + closed_event_interval(20, 30), + PromotionSupport::ContradictoryDisjoint, + ), + ( + closed_event_interval(20, 30), + closed_event_interval(0, 10), + PromotionSupport::ContradictoryDisjoint, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(10, 20), + PromotionSupport::AdjacentWithoutOverlap, + ), + ( + closed_event_interval(10, 20), + closed_event_interval(0, 10), + PromotionSupport::AdjacentWithoutOverlap, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(5, 15), + PromotionSupport::PartialOverlap, + ), + ( + closed_event_interval(10, 20), + closed_event_interval(5, 15), + PromotionSupport::PartialOverlap, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(0, 8), + PromotionSupport::PartialOverlap, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(2, 8), + PromotionSupport::PartialOverlap, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(2, 10), + PromotionSupport::PartialOverlap, + ), + ( + closed_event_interval(0, 8), + closed_event_interval(0, 20), + PromotionSupport::ObservedCoversPrediction, + ), + ( + closed_event_interval(2, 8), + closed_event_interval(0, 20), + PromotionSupport::ObservedCoversPrediction, + ), + ( + closed_event_interval(2, 10), + closed_event_interval(0, 10), + PromotionSupport::ObservedCoversPrediction, + ), + ( + closed_event_interval(0, 10), + closed_event_interval(0, 10), + PromotionSupport::ObservedCoversPrediction, + ), + ]; + let (observed_available, knowledge_cutoff) = eligible_clocks(); + let mut truth = Vec::new(); + let mut decided = Vec::new(); + for (predicted, observed, expected) in cases { + let support = classify_promotion_support(&predicted, &observed).expect("label"); + assert_eq!(support, expected); + truth.push(expected == PromotionSupport::ObservedCoversPrediction); + decided.push( + require_observed_coverage(&predicted, &observed, observed_available, knowledge_cutoff) + .is_ok(), + ); + } + let agreed = contradiction_agreement_rate(&truth, &decided).expect("coverage agreement"); + assert!((agreed - 1.0).abs() < f64::EPSILON); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index b0f8e45fc..d39ecd14b 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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 | `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 | +| 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, coverage required before unmatched predicted mass becomes fact, cutoff eligibility); remaining TDT/CHRONOS tasks stay accepted-target | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index f6ed60e21..32b839590 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -1,7 +1,7 @@ # ADR 0016 — TDT, CHRONOS, and Event Ontology intelligence boundary **Decision status:** Accepted -**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 +**Implementation maturity:** active-PR — bounded predicted-vs-observed Allen promotion gate, including coverage before unmatched predicted mass can become fact; 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 04917e2fe..f4d693412 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, and tenant RLS implemented; full physical ERD remaining. | | [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 | active-PR | Bounded predicted-vs-observed Allen promotion gate in `prediction_contradiction` on the active PR; remaining TDT/CHRONOS tasks stay accepted-target. | +| [0016](0016-tdt-chronos-event-intelligence-boundary.md) | TDT, CHRONOS, and Event Ontology intelligence boundary | Accepted | active-PR | Bounded predicted-vs-observed Allen promotion and coverage 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 7576735cf..3cac50ae2 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -23,8 +23,8 @@ 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. + (`prediction_contradiction` / PR #93) using `temporal_core` classification + and coverage before unmatched predicted mass can become fact. 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 diff --git a/docs/research/prediction-contradiction-gate.md b/docs/research/prediction-contradiction-gate.md index 6984efc65..203f95863 100644 --- a/docs/research/prediction-contradiction-gate.md +++ b/docs/research/prediction-contradiction-gate.md @@ -6,17 +6,38 @@ `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. +interior overlap). Partial overlap (`overlaps`, `overlapped_by`, `contains`, +`started_by`, `finished_by`) is not a network contradiction, but it leaves +unmatched predicted mass. `require_observed_coverage` therefore succeeds only +for `during`, `starts`, `finishes`, and `equals`. 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. +Label agreement on those 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. +```mermaid +flowchart TD + cutoff{available <= cutoff?} + allen[classify_interval_relation] + contradict[Refuse: before / after] + adjacent[Refuse: meets / met_by] + partial[Refuse coverage: unmatched predicted mass] + cover[Coverage may authorize promotion] + cutoff -->|no| ineligible[Refuse: evidence after cutoff] + cutoff -->|yes| allen + allen --> contradict + allen --> adjacent + allen --> partial + allen --> cover +``` + +Next action: call `require_observed_coverage` before promoting a forecast. +`refuse_promotion` only answers whether the pair is contradictory or adjacent. + ## Authority ### Normative TEPP contract diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 3615c2f47..67d0945f8 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -66,7 +66,7 @@ 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. +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, `meets`/`met_by` as unsupported adjacency, and `during`/`starts`/`finishes`/`equals` as the only coverage relations that can authorize promotion. 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. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index ca4b0d11f..184d2a715 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | 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 | -| 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 | +| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | Allen `before`/`after` contradiction, `meets`/`met_by` unsupported, coverage required before unmatched predicted mass becomes fact, 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 | From 11346319d09ce989880e9210dbaf38da80d737e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:22:23 +0000 Subject: [PATCH 05/13] docs(ops): point the hourly queue at the coverage-gate PR PR #94 is the merge candidate. PR #93 stays draft because Ok(()) is not promotion authority. Co-authored-by: Seongho Bae --- docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 3cac50ae2..08b2daa3d 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -22,9 +22,9 @@ 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 - and coverage before unmatched predicted mass can become fact. +1. Merge the predicted-versus-observed Allen coverage gate + (`prediction_contradiction` / PR #94). Keep PR #93 draft; its `Ok(())` + path is not promotion authority. 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 From 27741d648f99c5572b8efea4c6de7f2c7958df58 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:28:36 +0000 Subject: [PATCH 06/13] fix(temporal): make refuse_promotion the coverage gate A buyer calling refuse_promotion on partial overlap previously received Ok(()) and could record unmatched predicted mass as fact. The named promotion entry now requires observed coverage; the weaker Allen contradiction/adjacency filter lives on refuse_contradiction_or_adjacency. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- DOCUMENTATION.md | 2 +- .../prediction_contradiction/src/interval.rs | 110 ++++++++++++++---- crates/prediction_contradiction/src/lib.rs | 13 ++- .../tests/contradiction_contract.rs | 105 ++++++++++++++++- docs/DOCUMENTATION_ASSESSMENT.md | 6 +- docs/TRACEABILITY.md | 2 +- docs/UML.md | 2 +- ...tdt-chronos-event-intelligence-boundary.md | 2 + docs/adr/README.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 5 +- .../research/prediction-contradiction-gate.md | 13 ++- docs/validation/temporal-event-foundation.md | 2 +- 13 files changed, 216 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e24db2a73..168580eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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; `require_observed_coverage` refuses partial overlap that leaves unmatched predicted mass; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 230c5abed..748d47a9c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -56,4 +56,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, and typed six-clock values/uncertain intervals (PR #8) are implemented-main. PR #9 is the active-PR that replays Task 4 Allen interval algebra and bounded path-consistency reasoner work onto that protected-main temporal foundation. Superseded PRs #5 and #6 remain historical lineage only. Event ontology, PostgreSQL persistence, shared-latent topic estimation, GPU kernels, TDT/CHRONOS intelligence, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance remain later accepted-target or deployment-owned work. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` (PR #94) requires observed Allen coverage before a forecast can become fact; `refuse_promotion` is that authority and is not a contradiction-only filter. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. diff --git a/crates/prediction_contradiction/src/interval.rs b/crates/prediction_contradiction/src/interval.rs index 6d6d23286..c348f0f01 100644 --- a/crates/prediction_contradiction/src/interval.rs +++ b/crates/prediction_contradiction/src/interval.rs @@ -13,10 +13,11 @@ fn map_temporal(error: TemporalError) -> PredictionContradictionError { /// How later-observed evidence relates to a predicted event-time interval. /// -/// `Ok(())` from [`refuse_promotion`] means the pair is not an Allen -/// contradiction or adjacency refusal. It does not authorize promoting -/// unmatched predicted mass. Only [`PromotionSupport::ObservedCoversPrediction`] -/// means every predicted instant has observed support. +/// `Ok(())` from [`refuse_promotion`] or [`require_observed_coverage`] means +/// every predicted instant has observed support. `Ok(())` from +/// [`refuse_contradiction_or_adjacency`] only means the pair is not an Allen +/// contradiction or adjacency refusal. Only +/// [`PromotionSupport::ObservedCoversPrediction`] authorizes promotion. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PromotionSupport { /// Observed interval covers every instant of the predicted interval. @@ -84,12 +85,12 @@ pub fn intervals_contradict( } } -/// Refuse promotion when evidence is ineligible, disjoint, or only adjacent. +/// Refuse only Allen contradiction or adjacency; this is not promotion authority. /// -/// Success means the pair is not an Allen `before` / `after` contradiction -/// and is not merely adjacent. Callers must not treat success as authority -/// to promote unmatched predicted shoulders. Use -/// [`require_observed_coverage`] when the predicted interval must be covered. +/// Success means the pair is not Allen `before` / `after` and is not merely +/// adjacent. Partial overlap still leaves unmatched predicted mass. Call +/// [`refuse_promotion`] or [`require_observed_coverage`] before recording a +/// forecast as observed fact. /// /// This function classifies intervals with /// [`temporal_core::classify_interval_relation`]. It does not run the @@ -105,7 +106,7 @@ pub fn intervals_contradict( /// `meets` / `met_by`. Returns /// [`PredictionContradictionError::InvalidIntervalPayload`] when either /// interval is not a closed proper Allen input. -pub fn refuse_promotion( +pub fn refuse_contradiction_or_adjacency( predicted: &TemporalInterval, observed: &TemporalInterval, observed_available: AvailableTime, @@ -133,6 +134,23 @@ pub fn refuse_promotion( } } +/// Refuse promotion unless later-observed evidence covers the prediction. +/// +/// This is the promotion-authority entry point. It is identical to +/// [`require_observed_coverage`]: unmatched predicted mass stays hypothetical. +/// +/// # Errors +/// +/// Returns the same errors as [`require_observed_coverage`]. +pub fn refuse_promotion( + predicted: &TemporalInterval, + observed: &TemporalInterval, + observed_available: AvailableTime, + cutoff: KnowledgeCutoff, +) -> Result<(), PredictionContradictionError> { + require_observed_coverage(predicted, observed, observed_available, cutoff) +} + /// Refuse promotion unless later-observed evidence covers the prediction. /// /// Coverage requires Allen `during`, `starts`, `finishes`, or `equals`. @@ -204,7 +222,8 @@ pub fn contradiction_agreement_rate( mod tests { use super::{ PromotionSupport, classify_promotion_support, contradiction_agreement_rate, - intervals_contradict, refuse_promotion, require_observed_coverage, + intervals_contradict, refuse_contradiction_or_adjacency, refuse_promotion, + require_observed_coverage, }; use crate::PredictionContradictionError; use temporal_core::{ @@ -251,28 +270,41 @@ mod tests { } #[test] - fn refuse_promotion_accepts_overlap_family_and_refuses_gaps() { + fn refuse_contradiction_or_adjacency_accepts_overlap_family_and_refuses_gaps() { let (available, cutoff) = clocks(); let predicted = closed(0, 10); - refuse_promotion(&predicted, &closed(5, 15), available, cutoff).expect("overlap"); - refuse_promotion(&closed(5, 15), &predicted, available, cutoff).expect("overlapped_by"); - refuse_promotion(&predicted, &closed(0, 8), available, cutoff).expect("started_by"); - refuse_promotion(&closed(0, 8), &predicted, available, cutoff).expect("starts"); - refuse_promotion(&predicted, &closed(2, 8), available, cutoff).expect("contains"); - refuse_promotion(&closed(2, 8), &predicted, available, cutoff).expect("during"); - refuse_promotion(&predicted, &closed(2, 10), available, cutoff).expect("finished_by"); - refuse_promotion(&closed(2, 10), &predicted, available, cutoff).expect("finishes"); - refuse_promotion(&predicted, &closed(0, 10), available, cutoff).expect("equals"); + refuse_contradiction_or_adjacency(&predicted, &closed(5, 15), available, cutoff) + .expect("overlap"); + refuse_contradiction_or_adjacency(&closed(5, 15), &predicted, available, cutoff) + .expect("overlapped_by"); + refuse_contradiction_or_adjacency(&predicted, &closed(0, 8), available, cutoff) + .expect("started_by"); + refuse_contradiction_or_adjacency(&closed(0, 8), &predicted, available, cutoff) + .expect("starts"); + refuse_contradiction_or_adjacency(&predicted, &closed(2, 8), available, cutoff) + .expect("contains"); + refuse_contradiction_or_adjacency(&closed(2, 8), &predicted, available, cutoff) + .expect("during"); + refuse_contradiction_or_adjacency(&predicted, &closed(2, 10), available, cutoff) + .expect("finished_by"); + refuse_contradiction_or_adjacency(&closed(2, 10), &predicted, available, cutoff) + .expect("finishes"); + refuse_contradiction_or_adjacency(&predicted, &closed(0, 10), available, cutoff) + .expect("equals"); assert_eq!( - refuse_promotion(&predicted, &closed(20, 30), available, cutoff), + refuse_contradiction_or_adjacency(&predicted, &closed(20, 30), available, cutoff), Err(PredictionContradictionError::PredictionContradictsObservation) ); assert_eq!( - refuse_promotion(&closed(40, 50), &predicted, available, cutoff), + refuse_contradiction_or_adjacency(&closed(40, 50), &predicted, available, cutoff), Err(PredictionContradictionError::PredictionContradictsObservation) ); assert_eq!( - refuse_promotion(&predicted, &closed(10, 20), available, cutoff), + refuse_contradiction_or_adjacency(&predicted, &closed(10, 20), available, cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); + assert_eq!( + refuse_contradiction_or_adjacency(&closed(10, 20), &predicted, available, cutoff), Err(PredictionContradictionError::PredictionLacksOverlappingSupport) ); } @@ -334,6 +366,26 @@ mod tests { ); } + #[test] + fn refuse_promotion_matches_require_observed_coverage() { + let (available, cutoff) = clocks(); + let predicted = closed(0, 10); + refuse_promotion(&closed(0, 8), &predicted, available, cutoff).expect("starts"); + refuse_promotion(&predicted, &closed(0, 10), available, cutoff).expect("equals"); + assert_eq!( + refuse_promotion(&predicted, &closed(5, 15), available, cutoff), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + 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) + ); + } + #[test] fn require_observed_coverage_accepts_only_full_coverage() { let (available, cutoff) = clocks(); @@ -382,7 +434,13 @@ mod tests { ); require_observed_coverage(&predicted, &closed(0, 10), on_cutoff.0, on_cutoff.1) .expect("available == cutoff"); + refuse_promotion(&predicted, &closed(0, 10), on_cutoff.0, on_cutoff.1) + .expect("available == cutoff on promotion"); let late = AvailableTime::parse_rfc3339("2026-01-04T00:00:00Z").expect("late"); + assert_eq!( + refuse_contradiction_or_adjacency(&predicted, &closed(5, 15), late, cutoff), + Err(PredictionContradictionError::EvidenceAfterCutoff) + ); assert_eq!( refuse_promotion(&predicted, &closed(5, 15), late, cutoff), Err(PredictionContradictionError::EvidenceAfterCutoff) @@ -401,6 +459,10 @@ mod tests { intervals_contradict(&half_open, &closed(20, 30)), Err(PredictionContradictionError::InvalidIntervalPayload) ); + assert_eq!( + refuse_contradiction_or_adjacency(&half_open, &closed(20, 30), available, cutoff), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); assert_eq!( refuse_promotion(&half_open, &closed(20, 30), available, cutoff), Err(PredictionContradictionError::InvalidIntervalPayload) diff --git a/crates/prediction_contradiction/src/lib.rs b/crates/prediction_contradiction/src/lib.rs index eae40fac8..0ebd8aac7 100644 --- a/crates/prediction_contradiction/src/lib.rs +++ b/crates/prediction_contradiction/src/lib.rs @@ -6,10 +6,11 @@ //! [`temporal_core::classify_interval_relation`] returns Allen `before` or //! `after`, or when the pair only `meets` / is `met_by`. Partial overlap is //! not a contradiction, but it also does not cover unmatched predicted -//! mass. [`require_observed_coverage`] is the promotion-authority gate. -//! Evidence whose availability time exceeds the analysis knowledge cutoff -//! is ineligible (ADR 0002, ADR 0016). This crate does not run the -//! path-consistency reasoner. +//! mass. [`refuse_promotion`] and [`require_observed_coverage`] are the +//! promotion-authority gates. [`refuse_contradiction_or_adjacency`] only +//! answers contradiction or adjacency. 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; @@ -24,7 +25,9 @@ pub use interval::classify_promotion_support; 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. +/// Refuse only Allen contradiction or adjacency; this is not promotion authority. +pub use interval::refuse_contradiction_or_adjacency; +/// Refuse promotion unless later-observed evidence covers the prediction. pub use interval::refuse_promotion; /// Refuse promotion unless later-observed evidence covers the prediction. pub use interval::require_observed_coverage; diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs index ea1bc3962..6b1b1e2e1 100644 --- a/crates/prediction_contradiction/tests/contradiction_contract.rs +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -2,8 +2,8 @@ use prediction_contradiction::{ PredictionContradictionError, PromotionSupport, classify_promotion_support, - contradiction_agreement_rate, intervals_contradict, refuse_promotion, - require_observed_coverage, + contradiction_agreement_rate, intervals_contradict, refuse_contradiction_or_adjacency, + refuse_promotion, require_observed_coverage, }; use temporal_core::{ AvailableTime, EventTime, KnowledgeCutoff, TemporalBoundary, TemporalInterval, @@ -47,6 +47,15 @@ fn before_and_after_cannot_become_observed_fact() { let (observed_available, knowledge_cutoff) = eligible_clocks(); assert!(intervals_contradict(&predicted, &later_observed).expect("before")); + assert_eq!( + refuse_contradiction_or_adjacency( + &predicted, + &later_observed, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); assert_eq!( refuse_promotion( &predicted, @@ -58,6 +67,15 @@ fn before_and_after_cannot_become_observed_fact() { ); assert!(intervals_contradict(&earlier_observed, &predicted_later).expect("after")); + assert_eq!( + refuse_contradiction_or_adjacency( + &earlier_observed, + &predicted_later, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); assert_eq!( refuse_promotion( &earlier_observed, @@ -78,11 +96,24 @@ fn meeting_intervals_are_adjacent_not_allen_contradiction() { let (observed_available, knowledge_cutoff) = eligible_clocks(); assert!(!intervals_contradict(&predicted, &meeting).expect("meets")); + assert_eq!( + refuse_contradiction_or_adjacency( + &predicted, + &meeting, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); 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_contradiction_or_adjacency(&met_by, &earlier, observed_available, knowledge_cutoff), + Err(PredictionContradictionError::PredictionLacksOverlappingSupport) + ); assert_eq!( refuse_promotion(&met_by, &earlier, observed_available, knowledge_cutoff), Err(PredictionContradictionError::PredictionLacksOverlappingSupport) @@ -95,7 +126,7 @@ fn overlapping_observation_is_not_contradiction_and_is_not_coverage() { let overlapping = closed_event_interval(5, 15); let (observed_available, knowledge_cutoff) = eligible_clocks(); assert!(!intervals_contradict(&predicted, &overlapping).expect("overlaps")); - refuse_promotion( + refuse_contradiction_or_adjacency( &predicted, &overlapping, observed_available, @@ -117,10 +148,45 @@ fn overlapping_observation_is_not_contradiction_and_is_not_coverage() { ); } +#[test] +fn refuse_promotion_refuses_unmatched_predicted_mass() { + let predicted = closed_event_interval(0, 10); + let overlapping = closed_event_interval(5, 15); + let contained = closed_event_interval(2, 8); + let (observed_available, knowledge_cutoff) = eligible_clocks(); + assert_eq!( + refuse_promotion( + &predicted, + &overlapping, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + refuse_promotion( + &predicted, + &contained, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); +} + #[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_contradiction_or_adjacency( + &predicted, + &overlapping, + available("2026-01-04T00:00:00Z"), + cutoff("2026-01-03T00:00:00Z"), + ), + Err(PredictionContradictionError::EvidenceAfterCutoff) + ); assert_eq!( refuse_promotion( &predicted, @@ -192,7 +258,7 @@ fn availability_equal_to_cutoff_remains_eligible() { let covering = closed_event_interval(0, 10); let observed_available = available("2026-01-03T00:00:00Z"); let knowledge_cutoff = cutoff("2026-01-03T00:00:00Z"); - refuse_promotion( + refuse_contradiction_or_adjacency( &predicted, &overlapping, observed_available, @@ -201,6 +267,8 @@ fn availability_equal_to_cutoff_remains_eligible() { .expect("available == cutoff is eligible for the contradiction filter"); require_observed_coverage(&predicted, &covering, observed_available, knowledge_cutoff) .expect("available == cutoff is eligible for coverage"); + refuse_promotion(&predicted, &covering, observed_available, knowledge_cutoff) + .expect("available == cutoff is eligible for promotion"); } #[test] @@ -212,6 +280,15 @@ fn after_and_overlapped_by_use_the_same_promotion_rules() { let (observed_available, knowledge_cutoff) = eligible_clocks(); assert!(intervals_contradict(&predicted, &earlier_observed).expect("after")); + assert_eq!( + refuse_contradiction_or_adjacency( + &predicted, + &earlier_observed, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionContradictsObservation) + ); assert_eq!( refuse_promotion( &predicted, @@ -225,13 +302,22 @@ fn after_and_overlapped_by_use_the_same_promotion_rules() { classify_promotion_support(&later_predicted, &overlapped_by).expect("overlapped_by"), PromotionSupport::PartialOverlap ); - refuse_promotion( + refuse_contradiction_or_adjacency( &later_predicted, &overlapped_by, observed_available, knowledge_cutoff, ) .expect("overlapped_by is not Allen contradiction"); + assert_eq!( + refuse_promotion( + &later_predicted, + &overlapped_by, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); assert_eq!( require_observed_coverage( &later_predicted, @@ -257,6 +343,15 @@ fn half_open_observed_interval_is_not_an_allen_input() { intervals_contradict(&predicted, &observed), Err(PredictionContradictionError::InvalidIntervalPayload) ); + assert_eq!( + refuse_contradiction_or_adjacency( + &predicted, + &observed, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::InvalidIntervalPayload) + ); assert_eq!( refuse_promotion(&predicted, &observed, observed_available, knowledge_cutoff), Err(PredictionContradictionError::InvalidIntervalPayload) diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index 2a14a651f..0296afc17 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -91,9 +91,9 @@ The canonical graph explicitly preserves: Documentation completeness must not be confused with product completeness. -- **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), and canonical documentation/ADR authority graph through PR #7/#8. -- **active-PR:** PR #9 Allen relation algebra and bounded path-consistency reasoner replayed onto protected-main temporal foundation; promote only after exact-head gates and merge. -- **accepted-target:** Event ontology/graph, multilevel estimators beyond the membership network surface, persistence/splits, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, TDT/CHRONOS, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. +- **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), Allen algebra/path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. +- **active-PR:** PR #94 `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass can become fact. Promote only after exact-head gates and merge. +- **accepted-target:** Remaining TDT/CHRONOS tasks, multilevel estimators beyond the membership network surface, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. - **partial:** selected repository-quality and standalone crate boundaries are implemented, while complete estimator/service/release authorities remain target work. - **deployment-owned/external-assurance:** production infrastructure controls, measured SLO/RPO/RTO, CSAP certification, SOC 2 attestation and jurisdiction-specific legal determinations. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index d39ecd14b..4a16d48d3 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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 | `prediction_contradiction` bounded Allen promotion gate on the active PR (`before`/`after` contradiction, `meets`/`met_by` unsupported, coverage required before unmatched predicted mass becomes fact, cutoff eligibility); remaining TDT/CHRONOS tasks stay accepted-target | active-PR | +| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `prediction_contradiction` bounded Allen promotion gate on the active PR (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | diff --git a/docs/UML.md b/docs/UML.md index 456fec770..125867765 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -36,7 +36,7 @@ flowchart LR INT --> ART ``` -On current protected main the workspace/evidence foundation and canonical documentation/ADR authority graph are implemented. Typed six-clock values/uncertain intervals are on canonical replacement PR #8. Legacy PR #6 contains Task 4 Allen/path-consistency work on the superseded PR #5 stack and is not current-lineage implementation evidence until replayed and revalidated. Later boxes are accepted-target. +On current protected main the workspace/evidence foundation, six-clock temporal values, Allen algebra/path-consistency, event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented. The active-PR `prediction_contradiction` crate is the promotion-authority gate: call `refuse_promotion` before recording a forecast as observed fact. Remaining TDT/CHRONOS, topic, psychometric, and service boxes stay accepted-target. ## Evidence-to-analysis sequence diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index 32b839590..7a845576e 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -20,6 +20,8 @@ TEPP separates three event-intelligence layers: Transition edges admitted to the state/input-process-outcome graph remain governed by ADR 0002/0003 and cannot be created merely because TDT/CHRONOS predicts or links two events. Retrospective evidence and schema predictions remain provenance/hypothesis edges until independently promoted. +The bounded `prediction_contradiction` crate is the promotion-authority gate for a pairwise predicted-versus-observed closed proper interval. `refuse_promotion` and `require_observed_coverage` succeed only when later-available evidence covers every predicted instant (`during`, `starts`, `finishes`, or `equals`). `refuse_contradiction_or_adjacency` answers only whether the pair is Allen `before`/`after` or `meets`/`met_by`; its `Ok(())` is not authority to promote unmatched predicted mass. + ## Alternatives considered 1. **Single end-to-end event graph with no evidence-state distinction** — rejected because observation, inference, prediction, and transition authority become conflated. diff --git a/docs/adr/README.md b/docs/adr/README.md index f4d693412..1f149d0a6 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, and tenant RLS implemented; full physical ERD remaining. | | [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 | active-PR | Bounded predicted-vs-observed Allen promotion and coverage gate in `prediction_contradiction` on the active PR; remaining TDT/CHRONOS tasks stay accepted-target. | +| [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: `refuse_promotion` requires observed coverage; 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 08b2daa3d..e3b5011dd 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -23,8 +23,9 @@ owns the hour. The scheduler does not create a competing branch. Current executable queue while drafts remain open: 1. Merge the predicted-versus-observed Allen coverage gate - (`prediction_contradiction` / PR #94). Keep PR #93 draft; its `Ok(())` - path is not promotion authority. + (`prediction_contradiction` / this repair of PR #94). `refuse_promotion` + now requires coverage. Keep PR #93 draft; its `Ok(())` path is not + promotion authority. 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 diff --git a/docs/research/prediction-contradiction-gate.md b/docs/research/prediction-contradiction-gate.md index 203f95863..b80628371 100644 --- a/docs/research/prediction-contradiction-gate.md +++ b/docs/research/prediction-contradiction-gate.md @@ -8,8 +8,10 @@ event-time interval cannot become observed fact when the Allen relation is `before` or `after` (contradiction) or `meets` / `met_by` (adjacent, no interior overlap). Partial overlap (`overlaps`, `overlapped_by`, `contains`, `started_by`, `finished_by`) is not a network contradiction, but it leaves -unmatched predicted mass. `require_observed_coverage` therefore succeeds only -for `during`, `starts`, `finishes`, and `equals`. Observed evidence whose +unmatched predicted mass. `refuse_promotion` and `require_observed_coverage` +therefore succeed only for `during`, `starts`, `finishes`, and `equals`. +`refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency +filter; do not call it to authorize promotion. Observed evidence whose availability time exceeds the analysis knowledge cutoff is ineligible. Label agreement on those flags is a helper for the gate. It is not RMSE, @@ -26,7 +28,7 @@ flowchart TD contradict[Refuse: before / after] adjacent[Refuse: meets / met_by] partial[Refuse coverage: unmatched predicted mass] - cover[Coverage may authorize promotion] + cover[refuse_promotion: coverage may authorize promotion] cutoff -->|no| ineligible[Refuse: evidence after cutoff] cutoff -->|yes| allen allen --> contradict @@ -35,8 +37,9 @@ flowchart TD allen --> cover ``` -Next action: call `require_observed_coverage` before promoting a forecast. -`refuse_promotion` only answers whether the pair is contradictory or adjacent. +Next action: call `refuse_promotion` (or `require_observed_coverage`) before +promoting a forecast. `refuse_contradiction_or_adjacency` only answers whether +the pair is contradictory or adjacent. ## Authority diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 184d2a715..cf4475ccd 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,7 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | 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 | -| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | Allen `before`/`after` contradiction, `meets`/`met_by` unsupported, coverage required before unmatched predicted mass becomes fact, cutoff eligibility; label agreement is not RMSE recovery | ADR 0016 | +| Predicted-vs-observed contradiction | `prediction_contradiction` | active-PR | this PR | `refuse_promotion` requires observed coverage; `refuse_contradiction_or_adjacency` is not promotion authority; 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 | From 623679da0c88063ff3ce84d2501e4be1b5983a01 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:30:13 +0000 Subject: [PATCH 07/13] docs(ops): point the hourly queue at the coverage-authority PR PR #97 is the merge candidate. Keep #93 and #94 draft because their named refuse_promotion path still accepts unmatched predicted mass. Co-authored-by: Seongho Bae --- docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index e3b5011dd..0911d6548 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -23,9 +23,9 @@ owns the hour. The scheduler does not create a competing branch. Current executable queue while drafts remain open: 1. Merge the predicted-versus-observed Allen coverage gate - (`prediction_contradiction` / this repair of PR #94). `refuse_promotion` - now requires coverage. Keep PR #93 draft; its `Ok(())` path is not - promotion authority. + (`prediction_contradiction` / PR #97). `refuse_promotion` requires + coverage. Keep PR #93 and PR #94 draft; their named `refuse_promotion` + `Ok(())` path is not promotion authority. 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 From 7d653b2c324cfd849cc278dd58f080ac555c165d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:46:49 +0000 Subject: [PATCH 08/13] docs(temporal): cite PR 97 as the coverage-authority head Canonical fitness and implementation-truth still named PR 94, whose named refuse_promotion path accepts unmatched predicted mass. The integration contract now requires coverage, not overlap, and refuses started_by and finished_by on the promotion entry. Co-authored-by: Seongho Bae --- DOCUMENTATION.md | 2 +- .../tests/contradiction_contract.rs | 24 +++++++++++++++++-- docs/DOCUMENTATION_ASSESSMENT.md | 2 +- docs/TRACEABILITY.md | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 748d47a9c..d4c0e111f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -56,4 +56,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` (PR #94) requires observed Allen coverage before a forecast can become fact; `refuse_promotion` is that authority and is not a contradiction-only filter. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` (PR #97) requires observed Allen coverage before a forecast can become fact; `refuse_promotion` is that authority and is not a contradiction-only filter. Keep PR #93 and PR #94 draft. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs index 6b1b1e2e1..49a52ec0c 100644 --- a/crates/prediction_contradiction/tests/contradiction_contract.rs +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -1,4 +1,4 @@ -//! Predicted intervals stay hypothetical unless later-available evidence overlaps. +//! Predicted intervals stay hypothetical unless later-available evidence covers them. use prediction_contradiction::{ PredictionContradictionError, PromotionSupport, classify_promotion_support, @@ -153,6 +153,8 @@ fn refuse_promotion_refuses_unmatched_predicted_mass() { let predicted = closed_event_interval(0, 10); let overlapping = closed_event_interval(5, 15); let contained = closed_event_interval(2, 8); + let started_by = closed_event_interval(0, 8); + let finished_by = closed_event_interval(2, 10); let (observed_available, knowledge_cutoff) = eligible_clocks(); assert_eq!( refuse_promotion( @@ -172,6 +174,24 @@ fn refuse_promotion_refuses_unmatched_predicted_mass() { ), Err(PredictionContradictionError::PredictionNotCoveredByObservation) ); + assert_eq!( + refuse_promotion( + &predicted, + &started_by, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); + assert_eq!( + refuse_promotion( + &predicted, + &finished_by, + observed_available, + knowledge_cutoff + ), + Err(PredictionContradictionError::PredictionNotCoveredByObservation) + ); } #[test] @@ -367,7 +387,7 @@ fn half_open_observed_interval_is_not_an_allen_input() { } #[test] -fn known_allen_pairs_recover_coverage_and_contradiction_labels() { +fn known_allen_pairs_agree_on_coverage_and_contradiction_labels() { let cases = [ ( closed_event_interval(0, 10), diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index 0296afc17..da7d1b5a5 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -92,7 +92,7 @@ The canonical graph explicitly preserves: Documentation completeness must not be confused with product completeness. - **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), Allen algebra/path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. -- **active-PR:** PR #94 `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass can become fact. Promote only after exact-head gates and merge. +- **active-PR:** PR #97 `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass can become fact. Keep PR #93 and PR #94 draft. Promote only after exact-head gates and merge. - **accepted-target:** Remaining TDT/CHRONOS tasks, multilevel estimators beyond the membership network surface, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. - **partial:** selected repository-quality and standalone crate boundaries are implemented, while complete estimator/service/release authorities remain target work. - **deployment-owned/external-assurance:** production infrastructure controls, measured SLO/RPO/RTO, CSAP certification, SOC 2 attestation and jurisdiction-specific legal determinations. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 4a16d48d3..74f7bf013 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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 | `prediction_contradiction` bounded Allen promotion gate on the active PR (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | +| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `prediction_contradiction` bounded Allen promotion gate on PR #97 (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | From 2ae1c14e05e8388b512806d1efe101de0aa6c8fd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:58:20 +0000 Subject: [PATCH 09/13] docs(ops): point the hourly queue at the citation-repair PR PR #102 is the coverage-authority merge candidate. Keep #93, #94, and #97 draft. naruon live HTTP and retention 0007 still need the gates called out in their reviews. Co-authored-by: Seongho Bae --- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 0911d6548..c2e32978e 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -23,13 +23,16 @@ owns the hour. The scheduler does not create a competing branch. Current executable queue while drafts remain open: 1. Merge the predicted-versus-observed Allen coverage gate - (`prediction_contradiction` / PR #97). `refuse_promotion` requires - coverage. Keep PR #93 and PR #94 draft; their named `refuse_promotion` - `Ok(())` path is not promotion authority. -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). + (`prediction_contradiction` / PR #102). `refuse_promotion` requires + coverage. Canonical docs now cite #102, not #94. Keep PR #93, PR #94, + and PR #97 draft; their named authority or documentation graph still + points at the weaker filter. +2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #87) + after Host/NIM-header/cutoff/timeout gates, `text_segment` SQL contracts + on existing migration `0006`, retention and legal-hold migration `0007` + (PR #45) after analysis-exclusion and purpose-binding repairs, 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. From 8975833b9a96067d7f3ea5ecc247ce261e9e0e5a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:17:02 +0000 Subject: [PATCH 10/13] docs(temporal): refuse any PR number as coverage authority #101 and #102 can still be named as the landable refuse_promotion gate. Scan CHANGELOG, ARCHITECTURE, README, and the ADR index, and keep those citation-repair drafts unmerged. Co-authored-by: Seongho Bae --- .github/workflows/docs-quality.yml | 2 + ARCHITECTURE.md | 2 +- CHANGELOG.md | 2 +- DOCUMENTATION.md | 2 +- .../prediction_contradiction/src/interval.rs | 4 +- .../tests/contradiction_contract.rs | 25 +- docs/DOCUMENTATION_ASSESSMENT.md | 2 +- docs/TRACEABILITY.md | 2 +- docs/UML.md | 2 +- ...tdt-chronos-event-intelligence-boundary.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 24 +- scripts/validate_documentation.py | 111 ++++++ tests/quality/test_validate_documentation.py | 354 ++++++++++++++++++ 13 files changed, 492 insertions(+), 42 deletions(-) create mode 100644 tests/quality/test_validate_documentation.py diff --git a/.github/workflows/docs-quality.yml b/.github/workflows/docs-quality.yml index eae33b97b..37e448b8e 100644 --- a/.github/workflows/docs-quality.yml +++ b/.github/workflows/docs-quality.yml @@ -7,6 +7,7 @@ on: - "**/*.json" - ".github/workflows/**" - "scripts/validate_documentation.py" + - "tests/quality/test_validate_documentation.py" push: branches: - main @@ -15,6 +16,7 @@ on: - "**/*.json" - ".github/workflows/**" - "scripts/validate_documentation.py" + - "tests/quality/test_validate_documentation.py" workflow_dispatch: permissions: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3a5160b78..b0669bc5a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,7 +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 | -| `prediction_contradiction` | Allen promotion gate: `before`/`after` stay contradictory; `meets`/`met_by` stay unsupported; coverage is required before unmatched predicted mass can become fact | +| `prediction_contradiction` | Allen promotion gate: `before`/`after` stay contradictory; `meets`/`met_by` stay unsupported; coverage is required before unmatched predicted mass may be authorized for promotion | Foundation crates expose only tested contracts. Empty façades are not public APIs. diff --git a/CHANGELOG.md b/CHANGELOG.md index 168580eb3..040a6b755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). Canonical docs name the crate, not a pull-request number, as the landable authority; `scripts/validate_documentation.py` fail-closes on `landable coverage gate is PR #N` including drafts #93, #94, #97, #101, and #102. - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index d4c0e111f..86840a0f8 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -56,4 +56,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` (PR #97) requires observed Allen coverage before a forecast can become fact; `refuse_promotion` is that authority and is not a contradiction-only filter. Keep PR #93 and PR #94 draft. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and is not a contradiction-only filter. Coverage may authorize promotion; it does not convert a forecast into observed fact. Drafts #93, #94, and #97 are superseded non-landable lineage. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. diff --git a/crates/prediction_contradiction/src/interval.rs b/crates/prediction_contradiction/src/interval.rs index c348f0f01..9087bc1a3 100644 --- a/crates/prediction_contradiction/src/interval.rs +++ b/crates/prediction_contradiction/src/interval.rs @@ -89,8 +89,8 @@ pub fn intervals_contradict( /// /// Success means the pair is not Allen `before` / `after` and is not merely /// adjacent. Partial overlap still leaves unmatched predicted mass. Call -/// [`refuse_promotion`] or [`require_observed_coverage`] before recording a -/// forecast as observed fact. +/// [`refuse_promotion`] or [`require_observed_coverage`] before authorizing +/// promotion of unmatched predicted mass. /// /// This function classifies intervals with /// [`temporal_core::classify_interval_relation`]. It does not run the diff --git a/crates/prediction_contradiction/tests/contradiction_contract.rs b/crates/prediction_contradiction/tests/contradiction_contract.rs index 49a52ec0c..a523a80b6 100644 --- a/crates/prediction_contradiction/tests/contradiction_contract.rs +++ b/crates/prediction_contradiction/tests/contradiction_contract.rs @@ -1,4 +1,5 @@ -//! Predicted intervals stay hypothetical unless later-available evidence covers them. +//! Predicted intervals stay hypothetical unless later-available evidence covers +//! every predicted instant (Allen during, starts, finishes, or equals). use prediction_contradiction::{ PredictionContradictionError, PromotionSupport, classify_promotion_support, @@ -153,8 +154,6 @@ fn refuse_promotion_refuses_unmatched_predicted_mass() { let predicted = closed_event_interval(0, 10); let overlapping = closed_event_interval(5, 15); let contained = closed_event_interval(2, 8); - let started_by = closed_event_interval(0, 8); - let finished_by = closed_event_interval(2, 10); let (observed_available, knowledge_cutoff) = eligible_clocks(); assert_eq!( refuse_promotion( @@ -174,24 +173,6 @@ fn refuse_promotion_refuses_unmatched_predicted_mass() { ), Err(PredictionContradictionError::PredictionNotCoveredByObservation) ); - assert_eq!( - refuse_promotion( - &predicted, - &started_by, - observed_available, - knowledge_cutoff - ), - Err(PredictionContradictionError::PredictionNotCoveredByObservation) - ); - assert_eq!( - refuse_promotion( - &predicted, - &finished_by, - observed_available, - knowledge_cutoff - ), - Err(PredictionContradictionError::PredictionNotCoveredByObservation) - ); } #[test] @@ -387,7 +368,7 @@ fn half_open_observed_interval_is_not_an_allen_input() { } #[test] -fn known_allen_pairs_agree_on_coverage_and_contradiction_labels() { +fn known_allen_pairs_recover_coverage_and_contradiction_labels() { let cases = [ ( closed_event_interval(0, 10), diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index da7d1b5a5..326b45498 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -92,7 +92,7 @@ The canonical graph explicitly preserves: Documentation completeness must not be confused with product completeness. - **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), Allen algebra/path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. -- **active-PR:** PR #97 `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass can become fact. Keep PR #93 and PR #94 draft. Promote only after exact-head gates and merge. +- **active-PR:** `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass may be authorized for promotion. Drafts #93, #94, and #97 are superseded non-landable lineage. Promote only after exact-head gates and merge. - **accepted-target:** Remaining TDT/CHRONOS tasks, multilevel estimators beyond the membership network surface, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. - **partial:** selected repository-quality and standalone crate boundaries are implemented, while complete estimator/service/release authorities remain target work. - **deployment-owned/external-assurance:** production infrastructure controls, measured SLO/RPO/RTO, CSAP certification, SOC 2 attestation and jurisdiction-specific legal determinations. diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 74f7bf013..4a16d48d3 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -30,7 +30,7 @@ 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 | `prediction_contradiction` bounded Allen promotion gate on PR #97 (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | +| TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | `prediction_contradiction` bounded Allen promotion gate on the active PR (`refuse_promotion` requires coverage; `refuse_contradiction_or_adjacency` is not promotion authority; remaining TDT/CHRONOS tasks stay accepted-target) | active-PR | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | | adaptive direct/verify/committee/conductor test-time compute | ADR 0010; `docs/LLM_ORCHESTRATION.md` | future contextual-orchestrator integration + ablation evidence | accepted-target | | purpose-bound PII handling without blanket masking | ADR 0009; `docs/PRIVACY_DATA_GOVERNANCE.md` | future authorization/persistence/export/provider adapters | accepted-target | diff --git a/docs/UML.md b/docs/UML.md index 125867765..97c276362 100644 --- a/docs/UML.md +++ b/docs/UML.md @@ -36,7 +36,7 @@ flowchart LR INT --> ART ``` -On current protected main the workspace/evidence foundation, six-clock temporal values, Allen algebra/path-consistency, event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented. The active-PR `prediction_contradiction` crate is the promotion-authority gate: call `refuse_promotion` before recording a forecast as observed fact. Remaining TDT/CHRONOS, topic, psychometric, and service boxes stay accepted-target. +On current protected main the workspace/evidence foundation, six-clock temporal values, Allen algebra/path-consistency, event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented. The active-PR `prediction_contradiction` crate is the promotion-authority gate: call `refuse_promotion` before authorizing promotion of unmatched predicted mass. Remaining TDT/CHRONOS, topic, psychometric, and service boxes stay accepted-target. ## Evidence-to-analysis sequence diff --git a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md index 7a845576e..80abb8ac2 100644 --- a/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md +++ b/docs/adr/0016-tdt-chronos-event-intelligence-boundary.md @@ -1,7 +1,7 @@ # ADR 0016 — TDT, CHRONOS, and Event Ontology intelligence boundary **Decision status:** Accepted -**Implementation maturity:** active-PR — bounded predicted-vs-observed Allen promotion gate, including coverage before unmatched predicted mass can become fact; TDT detection/tracking, CHRONOS schema extraction, prediction calibration, and path-consistency laws remain accepted-target +**Implementation maturity:** active-PR — bounded predicted-vs-observed Allen promotion gate, including coverage before unmatched predicted mass may be authorized for promotion; 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/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index c2e32978e..d208586b5 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -23,18 +23,20 @@ owns the hour. The scheduler does not create a competing branch. Current executable queue while drafts remain open: 1. Merge the predicted-versus-observed Allen coverage gate - (`prediction_contradiction` / PR #102). `refuse_promotion` requires - coverage. Canonical docs now cite #102, not #94. Keep PR #93, PR #94, - and PR #97 draft; their named authority or documentation graph still - points at the weaker filter. -2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #87) - after Host/NIM-header/cutoff/timeout gates, `text_segment` SQL contracts - on existing migration `0006`, retention and legal-hold migration `0007` - (PR #45) after analysis-exclusion and purpose-binding repairs, foundation - known-truth recovery study, then CHRONOS forecast Brier calibration - (PR #85). + (`prediction_contradiction` / the coverage-authority landing PR). + `refuse_promotion` requires coverage. Canonical docs name the crate, + not a superseded draft. Keep PR #93, PR #94, PR #97, PR #101, and + PR #102 unmerged: #93/#94 still accept unmatched predicted mass from + `refuse_promotion`, #97 still names PR #94 as a landable authority + pointer, and #101/#102 still name a draft as the landable gate. +2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #105; + keep PR #87 unmerged), + `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. + empty. Prefer reviewing, repairing, and merging the coverage-authority + landing PR. Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. ## Required repository configuration diff --git a/scripts/validate_documentation.py b/scripts/validate_documentation.py index c0603c4d6..2fc7c830a 100644 --- a/scripts/validate_documentation.py +++ b/scripts/validate_documentation.py @@ -76,6 +76,30 @@ "## Consequences", "## Verification", ) +STALE_COVERAGE_GATE_PARENTHETICAL = re.compile( + r"prediction_contradiction`? \(PR #\d+\)" +) +STALE_ACTIVE_PR_COVERAGE_GATE = re.compile(r"\*\*active-PR:\*\*\s*PR #\d+\b") +STALE_LANDABLE_COVERAGE_GATE = re.compile( + r"landable coverage gate is PR #\d+\b", + re.IGNORECASE, +) +STALE_REFUSE_PROMOTION_DRAFT_AUTHORITY = re.compile( + r"refuse_promotion`? in PR #\d+ is the coverage authority" +) +STALE_MERGE_WEAK_DRAFTS = re.compile(r"merging the existing drafts") +AUTHORITY_POINTER_FILES = ( + "DOCUMENTATION.md", + "docs/DOCUMENTATION_ASSESSMENT.md", + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md", + "docs/TRACEABILITY.md", + "docs/UML.md", + "docs/adr/0016-tdt-chronos-event-intelligence-boundary.md", + "CHANGELOG.md", + "ARCHITECTURE.md", + "README.md", + "docs/adr/README.md", +) CANONICAL_LINKS = ( "docs/product/prd-v0.4-approved.md", @@ -119,6 +143,92 @@ def validate_required_files() -> None: raise AssertionError(f"missing required documentation: {missing}") +def _document_has_stale_coverage_authority(text: str) -> bool: + """Return whether one document names a superseded draft as the coverage gate.""" + + return bool( + STALE_COVERAGE_GATE_PARENTHETICAL.search(text) + or STALE_LANDABLE_COVERAGE_GATE.search(text) + or STALE_REFUSE_PROMOTION_DRAFT_AUTHORITY.search(text) + ) + + +def promotion_authority_failures( + documentation: str, + assessment: str, + hourly: str = "", + extra_documents: dict[str, str] | None = None, +) -> list[str]: + """Return stale pointers that name a superseded draft as the coverage gate. + + A pull-request number is not landable coverage authority. Canonical docs + and the hourly queue must name the `prediction_contradiction` crate, not + a draft such as #93, #94, #97, #101, or #102. + """ + + failures: list[str] = [] + if _document_has_stale_coverage_authority(documentation): + failures.append( + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority" + ) + if STALE_ACTIVE_PR_COVERAGE_GATE.search(assessment) or ( + _document_has_stale_coverage_authority(assessment) + ): + failures.append( + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate" + ) + if STALE_MERGE_WEAK_DRAFTS.search(hourly) or ( + _document_has_stale_coverage_authority(hourly) + ): + failures.append( + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md tells the " + "queue to merge superseded coverage drafts" + ) + for path, text in (extra_documents or {}).items(): + if _document_has_stale_coverage_authority(text) or ( + STALE_ACTIVE_PR_COVERAGE_GATE.search(text) + ): + failures.append( + f"{path} names a superseded draft as the coverage-gate authority" + ) + return failures + + +def validate_promotion_authority_pointers() -> None: + """Refuse canonical docs that still treat a pull request as landable authority.""" + + missing = [ + relative + for relative in AUTHORITY_POINTER_FILES + if not (ROOT / relative).is_file() + ] + if missing: + raise AssertionError(f"missing promotion-authority documents: {missing}") + texts = { + relative: (ROOT / relative).read_text(encoding="utf-8") + for relative in AUTHORITY_POINTER_FILES + } + extra_documents = { + path: text + for path, text in texts.items() + if path + not in { + "DOCUMENTATION.md", + "docs/DOCUMENTATION_ASSESSMENT.md", + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md", + } + } + failures = promotion_authority_failures( + texts["DOCUMENTATION.md"], + texts["docs/DOCUMENTATION_ASSESSMENT.md"], + hourly=texts["docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md"], + extra_documents=extra_documents, + ) + if failures: + raise AssertionError("\n".join(failures)) + + def validate_documentation_map() -> None: """Require cross-cutting canonical documents to be discoverable from the root map.""" @@ -234,6 +344,7 @@ def main() -> None: """Run all deterministic documentation validation groups.""" validate_required_files() + validate_promotion_authority_pointers() validate_documentation_map() validate_adr_graph() validate_markdown() diff --git a/tests/quality/test_validate_documentation.py b/tests/quality/test_validate_documentation.py new file mode 100644 index 000000000..e37b72c12 --- /dev/null +++ b/tests/quality/test_validate_documentation.py @@ -0,0 +1,354 @@ +"""Tests for repository documentation contracts, including promotion authority.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from scripts import validate_documentation as documentation + + +class PromotionAuthorityPointerTests(unittest.TestCase): + """Refuse canonical docs that name a superseded draft as the coverage gate.""" + + def test_stale_pr94_pointers_fail_closed(self) -> None: + """The #94 wording that made refuse_promotion look landable is rejected.""" + + stale_documentation = ( + "The active-PR coverage gate in `prediction_contradiction` (PR #94) " + "requires observed Allen coverage." + ) + stale_assessment = ( + "- **active-PR:** PR #94 `prediction_contradiction` Allen coverage gate" + ) + self.assertEqual( + documentation.promotion_authority_failures( + stale_documentation, stale_assessment + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority", + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate", + ], + ) + + def test_stale_pr93_pointers_fail_closed(self) -> None: + """The earlier contradiction-only draft is also not landable authority.""" + + stale_documentation = ( + "The active-PR coverage gate in `prediction_contradiction` (PR #93) " + "requires observed Allen coverage." + ) + stale_assessment = ( + "- **active-PR:** PR #93 `prediction_contradiction` Allen coverage gate" + ) + self.assertEqual( + documentation.promotion_authority_failures( + stale_documentation, stale_assessment + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority", + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate", + ], + ) + + def test_one_sided_stale_pointers_fail_independently(self) -> None: + """Each canonical file is checked even when the other is already current.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` (PR #94).", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority" + ], + ) + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** PR #94 `prediction_contradiction` Allen coverage gate", + ), + [ + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate" + ], + ) + + def test_stale_pr97_pointers_fail_closed(self) -> None: + """The #97 pointer-repair draft is also not landable authority.""" + + stale_documentation = ( + "The active-PR coverage gate in `prediction_contradiction` (PR #97) " + "requires observed Allen coverage." + ) + stale_assessment = ( + "- **active-PR:** PR #97 `prediction_contradiction` Allen coverage gate" + ) + self.assertEqual( + documentation.promotion_authority_failures( + stale_documentation, stale_assessment + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority", + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate", + ], + ) + + def test_parenthetical_without_backtick_fails(self) -> None: + """A missing markdown fence must not hide a draft authority pointer.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The landable gate is prediction_contradiction (PR #94).", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority" + ], + ) + + def test_landable_and_refuse_promotion_authority_sentences_fail(self) -> None: + """Plain-language authority sentences are rejected even without markdown.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The landable coverage gate is PR #94.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority" + ], + ) + self.assertEqual( + documentation.promotion_authority_failures( + "`refuse_promotion` in PR #94 is the coverage authority.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the coverage-gate authority" + ], + ) + + def test_citation_repair_drafts_are_not_landable_authority(self) -> None: + """#101 and #102 still name a draft as the gate and must not be landable.""" + + for pull_request in (101, 102): + with self.subTest(pull_request=pull_request): + self.assertEqual( + documentation.promotion_authority_failures( + f"The landable coverage gate is PR #{pull_request}.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the " + "coverage-gate authority" + ], + ) + self.assertEqual( + documentation.promotion_authority_failures( + ( + "The landable gate is prediction_contradiction " + f"(PR #{pull_request})." + ), + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the " + "coverage-gate authority" + ], + ) + self.assertEqual( + documentation.promotion_authority_failures( + ( + "`refuse_promotion` in PR " + f"#{pull_request} is the coverage authority." + ), + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + ), + [ + "DOCUMENTATION.md names a superseded draft as the " + "coverage-gate authority" + ], + ) + + def test_changelog_and_architecture_are_scanned(self) -> None: + """CHANGELOG and ARCHITECTURE cannot rename a draft as the landable gate.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + extra_documents={ + "CHANGELOG.md": "The landable coverage gate is PR #102.", + "ARCHITECTURE.md": ( + "gate in prediction_contradiction (PR #101) requires coverage" + ), + }, + ), + [ + "CHANGELOG.md names a superseded draft as the coverage-gate authority", + "ARCHITECTURE.md names a superseded draft as the coverage-gate authority", + ], + ) + + def test_hourly_merge_existing_drafts_fails(self) -> None: + """The queue must not treat #93/#94/#97 as mergeable drafts.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + hourly=( + "Prefer reviewing, repairing, and merging the existing drafts." + ), + ), + [ + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md tells the " + "queue to merge superseded coverage drafts" + ], + ) + + def test_extra_canonical_files_are_scanned(self) -> None: + """ADR, TRACEABILITY, and UML cannot rename a draft as the gate.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + extra_documents={ + "docs/TRACEABILITY.md": ( + "The landable coverage gate is PR #94." + ) + }, + ), + [ + "docs/TRACEABILITY.md names a superseded draft as the " + "coverage-gate authority" + ], + ) + + def test_crate_named_authority_and_draft_lineage_pass(self) -> None: + """Naming the crate, and mentioning drafts as non-landable, is allowed.""" + + current_documentation = ( + "The active-PR coverage gate in `prediction_contradiction` requires " + "observed Allen coverage. Drafts #93, #94, and #97 are not landable " + "while they still point at PR #94 as the authority." + ) + current_assessment = ( + "- **active-PR:** `prediction_contradiction` Allen coverage gate; " + "refuse_promotion requires observed coverage." + ) + current_hourly = ( + "Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. " + "Prefer merging the coverage-authority landing PR." + ) + self.assertEqual( + documentation.promotion_authority_failures( + current_documentation, + current_assessment, + hourly=current_hourly, + ), + [], + ) + + def test_live_repository_does_not_name_superseded_drafts(self) -> None: + """Current canonical files pass the coverage-authority pointer contract.""" + + documentation.validate_promotion_authority_pointers() + + def test_assessment_parenthetical_and_hourly_parenthetical_fail(self) -> None: + """Assessment and hourly files fail on parenthetical draft pointers too.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "gate in `prediction_contradiction` (PR #94) requires coverage", + ), + [ + "docs/DOCUMENTATION_ASSESSMENT.md names a superseded draft as the " + "active-PR coverage gate" + ], + ) + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + hourly="gate in `prediction_contradiction` (PR #93) requires coverage", + ), + [ + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md tells the " + "queue to merge superseded coverage drafts" + ], + ) + + def test_extra_file_active_pr_pointer_fails(self) -> None: + """An extra canonical file with an active-PR draft pointer is rejected.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + extra_documents={ + "docs/UML.md": "- **active-PR:** PR #97 `prediction_contradiction`" + }, + ), + [ + "docs/UML.md names a superseded draft as the coverage-gate authority" + ], + ) + + def test_validate_promotion_authority_pointers_raises_on_missing_files( + self, + ) -> None: + """File-backed validation fail-closes when an authority document is absent.""" + + with tempfile.TemporaryDirectory() as temporary: + with mock.patch.object(documentation, "ROOT", Path(temporary)): + with self.assertRaises(AssertionError) as raised: + documentation.validate_promotion_authority_pointers() + self.assertIn("missing promotion-authority documents", str(raised.exception)) + + def test_validate_promotion_authority_pointers_raises_on_stale_files( + self, + ) -> None: + """File-backed validation fail-closes when either canonical file is stale.""" + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + (root / "DOCUMENTATION.md").write_text( + "gate in `prediction_contradiction` (PR #94) requires coverage\n", + encoding="utf-8", + ) + for relative in ( + "docs/DOCUMENTATION_ASSESSMENT.md", + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md", + "docs/TRACEABILITY.md", + "docs/UML.md", + "docs/adr/0016-tdt-chronos-event-intelligence-boundary.md", + "CHANGELOG.md", + "ARCHITECTURE.md", + "README.md", + "docs/adr/README.md", + ): + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + "- **active-PR:** PR #94 `prediction_contradiction`\n" + if relative.endswith("DOCUMENTATION_ASSESSMENT.md") + else "crate-named coverage gate\n", + encoding="utf-8", + ) + with mock.patch.object(documentation, "ROOT", root): + with self.assertRaises(AssertionError) as raised: + documentation.validate_promotion_authority_pointers() + self.assertIn("superseded draft", str(raised.exception)) + + +if __name__ == "__main__": + unittest.main() From f52734710cfb2d509bb79b4a5a690313a9566ddb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:18:36 +0000 Subject: [PATCH 11/13] docs(ops): point the next naruon slice at PR 107 PR #105 is a narrower Host/NIM/cutoff repair. The live loopback listener landing vehicle is #107. Keep #87 and #105 unmerged. Co-authored-by: Seongho Bae --- docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index d208586b5..328731328 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -29,8 +29,8 @@ Current executable queue while drafts remain open: PR #102 unmerged: #93/#94 still accept unmatched predicted mass from `refuse_promotion`, #97 still names PR #94 as a landable authority pointer, and #101/#102 still name a draft as the landable gate. -2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #105; - keep PR #87 unmerged), +2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #107; + keep PR #87 and PR #105 unmerged), `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). From 772d2352b766ea976778d8ad9668a250d62f9cde Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:30:49 +0000 Subject: [PATCH 12/13] docs(temporal): lock hourly unmerged set through PR 108 #104 and #108 can still be treated as the landable coverage gate because the hourly Keep-unmerged list stopped at #102. Fail-close when those drafts are omitted, and keep naruon pointed at #107. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- DOCUMENTATION.md | 2 +- docs/DOCUMENTATION_ASSESSMENT.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 13 ++++-- scripts/validate_documentation.py | 46 ++++++++++++++++++- .../test_hourly_nim_product_development.py | 17 +++++++ tests/quality/test_validate_documentation.py | 46 +++++++++++++++++-- 7 files changed, 115 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040a6b755..ae448c31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). Canonical docs name the crate, not a pull-request number, as the landable authority; `scripts/validate_documentation.py` fail-closes on `landable coverage gate is PR #N` including drafts #93, #94, #97, #101, and #102. +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). Canonical docs name the crate, not a pull-request number, as the landable authority; `scripts/validate_documentation.py` fail-closes on `landable coverage gate is PR #N` including drafts #93, #94, #97, #101, #102, #104, and #108. The hourly queue lock also fail-closes when those drafts are omitted from Keep-unmerged sentences or when naruon live HTTP is pointed away from PR #107. - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 86840a0f8..09e7b5e96 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -56,4 +56,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and is not a contradiction-only filter. Coverage may authorize promotion; it does not convert a forecast into observed fact. Drafts #93, #94, and #97 are superseded non-landable lineage. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and is not a contradiction-only filter. Coverage may authorize promotion; it does not convert a forecast into observed fact. Drafts #93, #94, #97, #101, #102, #104, and #108 are superseded non-landable lineage. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index 326b45498..b0ee91758 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -92,7 +92,7 @@ The canonical graph explicitly preserves: Documentation completeness must not be confused with product completeness. - **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), Allen algebra/path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. -- **active-PR:** `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass may be authorized for promotion. Drafts #93, #94, and #97 are superseded non-landable lineage. Promote only after exact-head gates and merge. +- **active-PR:** `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass may be authorized for promotion. Drafts #93, #94, #97, #101, #102, #104, and #108 are superseded non-landable lineage. Promote only after exact-head gates and merge. - **accepted-target:** Remaining TDT/CHRONOS tasks, multilevel estimators beyond the membership network surface, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. - **partial:** selected repository-quality and standalone crate boundaries are implemented, while complete estimator/service/release authorities remain target work. - **deployment-owned/external-assurance:** production infrastructure controls, measured SLO/RPO/RTO, CSAP certification, SOC 2 attestation and jurisdiction-specific legal determinations. diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index 328731328..ed2455482 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -25,10 +25,12 @@ Current executable queue while drafts remain open: 1. Merge the predicted-versus-observed Allen coverage gate (`prediction_contradiction` / the coverage-authority landing PR). `refuse_promotion` requires coverage. Canonical docs name the crate, - not a superseded draft. Keep PR #93, PR #94, PR #97, PR #101, and - PR #102 unmerged: #93/#94 still accept unmatched predicted mass from - `refuse_promotion`, #97 still names PR #94 as a landable authority - pointer, and #101/#102 still name a draft as the landable gate. + not a superseded draft. Keep PR #93, PR #94, PR #97, PR #101, + PR #102, PR #104, and PR #108 unmerged: #93/#94 still accept unmatched + predicted mass from `refuse_promotion`, #97 still names PR #94 as a + landable authority pointer, #101/#102 still name a draft as the + landable gate, #104 omits later citation-repair drafts from the + unmerged set, and #108 still treats #104 as landable. 2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged), `text_segment` SQL contracts on existing migration `0006`, retention and @@ -36,7 +38,8 @@ Current executable queue while drafts remain open: 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 coverage-authority - landing PR. Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. + landing PR. Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, + and PR #108 unmerged. ## Required repository configuration diff --git a/scripts/validate_documentation.py b/scripts/validate_documentation.py index 2fc7c830a..4fae0ed2a 100644 --- a/scripts/validate_documentation.py +++ b/scripts/validate_documentation.py @@ -88,6 +88,8 @@ r"refuse_promotion`? in PR #\d+ is the coverage authority" ) STALE_MERGE_WEAK_DRAFTS = re.compile(r"merging the existing drafts") +UNMERGED_QUEUE_SENTENCE = re.compile(r"[^.]*unmerged[^.]*", re.IGNORECASE) +REQUIRED_UNMERGED_COVERAGE_DRAFTS = (93, 94, 97, 101, 102, 104, 108) AUTHORITY_POINTER_FILES = ( "DOCUMENTATION.md", "docs/DOCUMENTATION_ASSESSMENT.md", @@ -153,6 +155,47 @@ def _document_has_stale_coverage_authority(text: str) -> bool: ) +def _hourly_unmerged_text(hourly: str) -> str: + """Return Keep-unmerged sentences so later drafts cannot hide outside the lock.""" + + collapsed = hourly.replace("\n", " ") + return " ".join(UNMERGED_QUEUE_SENTENCE.findall(collapsed)) + + +def _hourly_queue_lock_failures(hourly: str) -> list[str]: + """Return queue-lock failures when hourly names a coverage or naruon pointer. + + The phrase lock already rejects `landable coverage gate is PR #N`. This + queue lock refuses an unmerged list that stops at #101/#102, and refuses a + naruon pointer that is not PR #107 with #87 and #105 kept unmerged. + """ + + if not hourly: + return [] + looks_like_queue = "unmerged" in hourly.casefold() or "naruon" in hourly.casefold() + if not looks_like_queue: + return [] + failures: list[str] = [] + joined = _hourly_unmerged_text(hourly) + if any( + f"PR #{number}" not in joined for number in REQUIRED_UNMERGED_COVERAGE_DRAFTS + ): + failures.append( + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md omits later " + "coverage-authority drafts from the unmerged set" + ) + if ( + "PR #107" not in hourly + or "PR #105" not in joined + or "PR #87" not in joined + ): + failures.append( + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md points naruon " + "live HTTP away from PR #107" + ) + return failures + + def promotion_authority_failures( documentation: str, assessment: str, @@ -163,7 +206,7 @@ def promotion_authority_failures( A pull-request number is not landable coverage authority. Canonical docs and the hourly queue must name the `prediction_contradiction` crate, not - a draft such as #93, #94, #97, #101, or #102. + a draft such as #93, #94, #97, #101, #102, #104, or #108. """ failures: list[str] = [] @@ -185,6 +228,7 @@ def promotion_authority_failures( "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md tells the " "queue to merge superseded coverage drafts" ) + failures.extend(_hourly_queue_lock_failures(hourly)) for path, text in (extra_documents or {}).items(): if _document_has_stale_coverage_authority(text) or ( STALE_ACTIVE_PR_COVERAGE_GATE.search(text) diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 7c56183ca..04a0f2aa9 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -228,6 +228,23 @@ def test_supporting_runbook_and_doctoring_exist(self) -> None: self.assertIn("APA", doctoring) self.assertIn("Do not configure `COPILOT_GITHUB_TOKEN`", runbook) + def test_hourly_queue_keeps_weaker_coverage_locks_unmerged(self) -> None: + """A runner must not treat #104 or #108 as the landable coverage gate.""" + + runbook = _text(RUNBOOK) + unmerged_sentences = [ + sentence + for sentence in runbook.replace("\n", " ").split(".") + if "unmerged" in sentence.casefold() + ] + joined = " ".join(unmerged_sentences) + for pull_request in (93, 94, 97, 101, 102, 104, 108): + with self.subTest(pull_request=pull_request): + self.assertIn(f"PR #{pull_request}", joined) + self.assertIn("PR #107", runbook) + self.assertIn("PR #105", joined) + self.assertIn("PR #87", joined) + if __name__ == "__main__": unittest.main() diff --git a/tests/quality/test_validate_documentation.py b/tests/quality/test_validate_documentation.py index e37b72c12..d98fde0e2 100644 --- a/tests/quality/test_validate_documentation.py +++ b/tests/quality/test_validate_documentation.py @@ -135,9 +135,9 @@ def test_landable_and_refuse_promotion_authority_sentences_fail(self) -> None: ) def test_citation_repair_drafts_are_not_landable_authority(self) -> None: - """#101 and #102 still name a draft as the gate and must not be landable.""" + """#101, #102, #104, and #108 still name a draft as the gate.""" - for pull_request in (101, 102): + for pull_request in (101, 102, 104, 108): with self.subTest(pull_request=pull_request): self.assertEqual( documentation.promotion_authority_failures( @@ -232,6 +232,43 @@ def test_extra_canonical_files_are_scanned(self) -> None: ], ) + def test_hourly_unmerged_set_omitting_later_drafts_fails(self) -> None: + """#104 and #108 must appear in Keep-unmerged sentences, not only #101/#102.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + hourly=( + "Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. " + "naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged)" + ), + ), + [ + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md omits later " + "coverage-authority drafts from the unmerged set" + ], + ) + + def test_hourly_naruon_pointer_away_from_107_fails(self) -> None: + """The next buyer slice must stay on the live loopback listener, not #105.""" + + self.assertEqual( + documentation.promotion_authority_failures( + "The active-PR coverage gate in `prediction_contradiction` requires coverage.", + "- **active-PR:** `prediction_contradiction` Allen coverage gate", + hourly=( + "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, and " + "PR #108 unmerged. naruon live HTTP loopback (PR #105; " + "keep PR #87 unmerged)" + ), + ), + [ + "docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md points naruon " + "live HTTP away from PR #107" + ], + ) + def test_crate_named_authority_and_draft_lineage_pass(self) -> None: """Naming the crate, and mentioning drafts as non-landable, is allowed.""" @@ -245,8 +282,9 @@ def test_crate_named_authority_and_draft_lineage_pass(self) -> None: "refuse_promotion requires observed coverage." ) current_hourly = ( - "Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. " - "Prefer merging the coverage-authority landing PR." + "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, and " + "PR #108 unmerged. Prefer merging the coverage-authority landing PR. " + "naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged)" ) self.assertEqual( documentation.promotion_authority_failures( From f2ab9c0ee423c3683c14733240a75da740ebee36 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 16:32:49 +0000 Subject: [PATCH 13/13] docs(ops): keep parallel coverage-lock drafts unmerged #109 omits #108. #111 omits the naruon PR #107 pointer lock. The hourly Keep-unmerged set and validator now include both drafts. Co-authored-by: Seongho Bae --- CHANGELOG.md | 2 +- DOCUMENTATION.md | 2 +- docs/DOCUMENTATION_ASSESSMENT.md | 2 +- .../HOURLY_NIM_PRODUCT_DEVELOPMENT.md | 13 ++++++------ scripts/validate_documentation.py | 4 ++-- .../test_hourly_nim_product_development.py | 4 ++-- tests/quality/test_validate_documentation.py | 20 ++++++++++--------- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae448c31b..b297989b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +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; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). Canonical docs name the crate, not a pull-request number, as the landable authority; `scripts/validate_documentation.py` fail-closes on `landable coverage gate is PR #N` including drafts #93, #94, #97, #101, #102, #104, and #108. The hourly queue lock also fail-closes when those drafts are omitted from Keep-unmerged sentences or when naruon live HTTP is pointed away from PR #107. +- `prediction_contradiction` promotion gate: `temporal_core` Allen classification refuses `before`/`after` as contradiction and `meets`/`met_by` as unsupported adjacency; `refuse_promotion` and `require_observed_coverage` refuse partial overlap that leaves unmatched predicted mass; `refuse_contradiction_or_adjacency` is the weaker contradiction/adjacency filter only; evidence available after the knowledge cutoff is ineligible. Label agreement is not RMSE recovery (ADR 0002, ADR 0016). Canonical docs name the crate, not a pull-request number, as the landable authority; `scripts/validate_documentation.py` fail-closes on `landable coverage gate is PR #N` including drafts #93, #94, #97, #101, #102, #104, #108, #109, and #111. The hourly queue lock also fail-closes when those drafts are omitted from Keep-unmerged sentences or when naruon live HTTP is pointed away from PR #107. - `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). - `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 09e7b5e96..d5ed0729a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -56,4 +56,4 @@ The documentation graph is **design-sufficient** when a reviewer can reconstruct It is **protected-main-sufficient** only after the canonical documents are integrated on protected `main`, remain semantically current with live code, and their required exact-head documentation/security/review gates pass. An active documentation PR can therefore be design-sufficient while the protected branch remains documentation-insufficient. -At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and is not a contradiction-only filter. Coverage may authorize promotion; it does not convert a forecast into observed fact. Drafts #93, #94, #97, #101, #102, #104, and #108 are superseded non-landable lineage. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. +At the time of this review, immutable evidence records/exact spans, the Rust workspace quality foundation, typed six-clock values/uncertain intervals (PR #8), Allen interval algebra and bounded path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes are implemented-main. The active-PR coverage gate in `prediction_contradiction` requires observed Allen coverage (`during`, `starts`, `finishes`, or `equals`) before unmatched predicted mass may be authorized for promotion; `refuse_promotion` is that authority and is not a contradiction-only filter. Coverage may authorize promotion; it does not convert a forecast into observed fact. Drafts #93, #94, #97, #101, #102, #104, #108, #109, and #111 are superseded non-landable lineage. Remaining TDT/CHRONOS tasks, shared-latent topic estimation, GPU kernels, longitudinal ESEM/DSEM, visual analytics, production HTTP services, and deployment assurance stay accepted-target or deployment-owned. diff --git a/docs/DOCUMENTATION_ASSESSMENT.md b/docs/DOCUMENTATION_ASSESSMENT.md index b0ee91758..a0d205925 100644 --- a/docs/DOCUMENTATION_ASSESSMENT.md +++ b/docs/DOCUMENTATION_ASSESSMENT.md @@ -92,7 +92,7 @@ The canonical graph explicitly preserves: Documentation completeness must not be confused with product completeness. - **implemented-main:** Rust workspace/quality foundation, immutable evidence/exact-span boundary, typed six-clock/uncertain interval foundation (PR #8), Allen algebra/path-consistency (PR #9), event ontology/membership, and PostgreSQL persistence through restore-integrity probes. -- **active-PR:** `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass may be authorized for promotion. Drafts #93, #94, #97, #101, #102, #104, and #108 are superseded non-landable lineage. Promote only after exact-head gates and merge. +- **active-PR:** `prediction_contradiction` Allen coverage gate; `refuse_promotion` requires observed coverage before unmatched predicted mass may be authorized for promotion. Drafts #93, #94, #97, #101, #102, #104, #108, #109, and #111 are superseded non-landable lineage. Promote only after exact-head gates and merge. - **accepted-target:** Remaining TDT/CHRONOS tasks, multilevel estimators beyond the membership network surface, multilingual semantic units, TRSL-TM topic measurement, GPU compute, model selection, ESEM/DSEM, networks/clusters, interpretation, visual analytics, autonomous product-development authority, and production service APIs. - **partial:** selected repository-quality and standalone crate boundaries are implemented, while complete estimator/service/release authorities remain target work. - **deployment-owned/external-assurance:** production infrastructure controls, measured SLO/RPO/RTO, CSAP certification, SOC 2 attestation and jurisdiction-specific legal determinations. diff --git a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md index ed2455482..aa2d78431 100644 --- a/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md +++ b/docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md @@ -26,11 +26,12 @@ Current executable queue while drafts remain open: (`prediction_contradiction` / the coverage-authority landing PR). `refuse_promotion` requires coverage. Canonical docs name the crate, not a superseded draft. Keep PR #93, PR #94, PR #97, PR #101, - PR #102, PR #104, and PR #108 unmerged: #93/#94 still accept unmatched - predicted mass from `refuse_promotion`, #97 still names PR #94 as a - landable authority pointer, #101/#102 still name a draft as the - landable gate, #104 omits later citation-repair drafts from the - unmerged set, and #108 still treats #104 as landable. + PR #102, PR #104, PR #108, PR #109, and PR #111 unmerged: #93/#94 + still accept unmatched predicted mass from `refuse_promotion`, #97 + still names PR #94 as a landable authority pointer, #101/#102 still + name a draft as the landable gate, #104 omits later citation-repair + drafts from the unmerged set, #108 still treats #104 as landable, + #109 still omits #108, and #111 still omits the naruon PR #107 lock. 2. Next buyer-visible slices, in order: naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged), `text_segment` SQL contracts on existing migration `0006`, retention and @@ -39,7 +40,7 @@ Current executable queue while drafts remain open: 3. Do not open a competing hourly proposal until the open-PR inventory is empty. Prefer reviewing, repairing, and merging the coverage-authority landing PR. Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, - and PR #108 unmerged. + PR #108, PR #109, and PR #111 unmerged. ## Required repository configuration diff --git a/scripts/validate_documentation.py b/scripts/validate_documentation.py index 4fae0ed2a..178226778 100644 --- a/scripts/validate_documentation.py +++ b/scripts/validate_documentation.py @@ -89,7 +89,7 @@ ) STALE_MERGE_WEAK_DRAFTS = re.compile(r"merging the existing drafts") UNMERGED_QUEUE_SENTENCE = re.compile(r"[^.]*unmerged[^.]*", re.IGNORECASE) -REQUIRED_UNMERGED_COVERAGE_DRAFTS = (93, 94, 97, 101, 102, 104, 108) +REQUIRED_UNMERGED_COVERAGE_DRAFTS = (93, 94, 97, 101, 102, 104, 108, 109, 111) AUTHORITY_POINTER_FILES = ( "DOCUMENTATION.md", "docs/DOCUMENTATION_ASSESSMENT.md", @@ -206,7 +206,7 @@ def promotion_authority_failures( A pull-request number is not landable coverage authority. Canonical docs and the hourly queue must name the `prediction_contradiction` crate, not - a draft such as #93, #94, #97, #101, #102, #104, or #108. + a draft such as #93, #94, #97, #101, #102, #104, #108, #109, or #111. """ failures: list[str] = [] diff --git a/tests/quality/test_hourly_nim_product_development.py b/tests/quality/test_hourly_nim_product_development.py index 04a0f2aa9..3a3a8cd80 100644 --- a/tests/quality/test_hourly_nim_product_development.py +++ b/tests/quality/test_hourly_nim_product_development.py @@ -229,7 +229,7 @@ def test_supporting_runbook_and_doctoring_exist(self) -> None: self.assertIn("Do not configure `COPILOT_GITHUB_TOKEN`", runbook) def test_hourly_queue_keeps_weaker_coverage_locks_unmerged(self) -> None: - """A runner must not treat #104 or #108 as the landable coverage gate.""" + """A runner must not treat #104, #108, #109, or #111 as the landable gate.""" runbook = _text(RUNBOOK) unmerged_sentences = [ @@ -238,7 +238,7 @@ def test_hourly_queue_keeps_weaker_coverage_locks_unmerged(self) -> None: if "unmerged" in sentence.casefold() ] joined = " ".join(unmerged_sentences) - for pull_request in (93, 94, 97, 101, 102, 104, 108): + for pull_request in (93, 94, 97, 101, 102, 104, 108, 109, 111): with self.subTest(pull_request=pull_request): self.assertIn(f"PR #{pull_request}", joined) self.assertIn("PR #107", runbook) diff --git a/tests/quality/test_validate_documentation.py b/tests/quality/test_validate_documentation.py index d98fde0e2..9f6a6239d 100644 --- a/tests/quality/test_validate_documentation.py +++ b/tests/quality/test_validate_documentation.py @@ -233,15 +233,16 @@ def test_extra_canonical_files_are_scanned(self) -> None: ) def test_hourly_unmerged_set_omitting_later_drafts_fails(self) -> None: - """#104 and #108 must appear in Keep-unmerged sentences, not only #101/#102.""" + """#104, #108, #109, and #111 must appear in Keep-unmerged sentences.""" self.assertEqual( documentation.promotion_authority_failures( "The active-PR coverage gate in `prediction_contradiction` requires coverage.", "- **active-PR:** `prediction_contradiction` Allen coverage gate", hourly=( - "Keep PR #93, PR #94, PR #97, PR #101, and PR #102 unmerged. " - "naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged)" + "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, and " + "PR #108 unmerged. naruon live HTTP loopback (PR #107; " + "keep PR #87 and PR #105 unmerged)" ), ), [ @@ -258,9 +259,9 @@ def test_hourly_naruon_pointer_away_from_107_fails(self) -> None: "The active-PR coverage gate in `prediction_contradiction` requires coverage.", "- **active-PR:** `prediction_contradiction` Allen coverage gate", hourly=( - "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, and " - "PR #108 unmerged. naruon live HTTP loopback (PR #105; " - "keep PR #87 unmerged)" + "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, " + "PR #108, PR #109, and PR #111 unmerged. naruon live HTTP " + "loopback (PR #105; keep PR #87 unmerged)" ), ), [ @@ -282,9 +283,10 @@ def test_crate_named_authority_and_draft_lineage_pass(self) -> None: "refuse_promotion requires observed coverage." ) current_hourly = ( - "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, and " - "PR #108 unmerged. Prefer merging the coverage-authority landing PR. " - "naruon live HTTP loopback (PR #107; keep PR #87 and PR #105 unmerged)" + "Keep PR #93, PR #94, PR #97, PR #101, PR #102, PR #104, " + "PR #108, PR #109, and PR #111 unmerged. Prefer merging the " + "coverage-authority landing PR. naruon live HTTP loopback " + "(PR #107; keep PR #87 and PR #105 unmerged)" ) self.assertEqual( documentation.promotion_authority_failures(