From 3ae2d7fbb89f19898c51c493eb3ff8717cfcd8b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 11:00:01 +0900 Subject: [PATCH 1/3] feat(relation): refuse inferred status as observed evidence Inferred relations stay inferred (ADR 0003). They cannot be treated as observed documentary evidence or as forward state transitions. Recovery is the computed share of statuses that match known truth versus collapsing every status to observed. --- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + Cargo.lock | 4 + Cargo.toml | 2 + README.md | 3 +- crates/inferred_status/Cargo.toml | 17 +++ crates/inferred_status/src/error.rs | 53 +++++++ crates/inferred_status/src/lib.rs | 23 +++ crates/inferred_status/src/status.rs | 143 ++++++++++++++++++ .../inferred_status/tests/crate_contract.rs | 7 + .../tests/inferred_status_contract.rs | 70 +++++++++ docs/TRACEABILITY.md | 2 +- ...03-relational-event-multiple-membership.md | 2 +- docs/adr/README.md | 2 +- docs/research/inferred-status-identity.md | 30 ++++ docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 1 + scripts/check_workspace_contract.py | 1 + 18 files changed, 359 insertions(+), 5 deletions(-) create mode 100644 crates/inferred_status/Cargo.toml create mode 100644 crates/inferred_status/src/error.rs create mode 100644 crates/inferred_status/src/lib.rs create mode 100644 crates/inferred_status/src/status.rs create mode 100644 crates/inferred_status/tests/crate_contract.rs create mode 100644 crates/inferred_status/tests/inferred_status_contract.rs create mode 100644 docs/research/inferred-status-identity.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db..5455a5ac 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 | +| `inferred_status` | inferred relations cannot be promoted to observed evidence or transitions | 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 f3764d25..97f10d91 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 +- `inferred_status` identity gate: inferred relations cannot be promoted to observed evidence or to state transitions; recovered observed/inferred labels match known truth at a higher computed rate than treating every status as observed (ADR 0003). - `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose. - `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. diff --git a/Cargo.lock b/Cargo.lock index 372a55f4..b4accb26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,10 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "inferred_status" +version = "0.1.0" + [[package]] name = "io-uring" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 92565940..d27be453 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/inferred_status", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/inferred_status", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d..302aed37 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/inferred_status ``` ## Local verification diff --git a/crates/inferred_status/Cargo.toml b/crates/inferred_status/Cargo.toml new file mode 100644 index 00000000..5ac8b929 --- /dev/null +++ b/crates/inferred_status/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "inferred_status" +description = "Inferred relations cannot be promoted to observed evidence or transitions." +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/inferred_status/src/error.rs b/crates/inferred_status/src/error.rs new file mode 100644 index 00000000..0513f8f9 --- /dev/null +++ b/crates/inferred_status/src/error.rs @@ -0,0 +1,53 @@ +//! Fail-closed inferred-status errors. + +use std::fmt; + +/// A fail-closed inferred-status error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum InferredStatusError { + /// An inferred relation was treated as observed evidence. + InferredIsNotObserved, + /// An inferred relation was treated as a state transition. + InferredIsNotTransition, + /// A recovery slice was empty or length-mismatched. + InvalidStatusPayload, +} + +impl fmt::Display for InferredStatusError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InferredIsNotObserved => "inferred relation is not observed evidence", + Self::InferredIsNotTransition => "inferred relation is not a state transition", + Self::InvalidStatusPayload => "invalid inferred-status payload", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for InferredStatusError {} + +#[cfg(test)] +mod tests { + use super::InferredStatusError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + InferredStatusError::InferredIsNotObserved, + "inferred relation is not observed evidence", + ), + ( + InferredStatusError::InferredIsNotTransition, + "inferred relation is not a state transition", + ), + ( + InferredStatusError::InvalidStatusPayload, + "invalid inferred-status payload", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/inferred_status/src/lib.rs b/crates/inferred_status/src/lib.rs new file mode 100644 index 00000000..5223fbc9 --- /dev/null +++ b/crates/inferred_status/src/lib.rs @@ -0,0 +1,23 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Inferred relations cannot be promoted to observed evidence or transitions. +//! +//! LLM, reasoner, and heuristic proposals stay inferred until deterministic +//! schema, evidence, and scientific validation promote them (ADR 0003). + +mod error; +mod status; + +/// Fail-closed inferred-status errors. +pub use error::InferredStatusError; +/// Fraction of recovered evidence statuses that match known truth. +pub use status::identity_recovery_rate; +/// Refuse to treat an inferred relation as observed evidence. +pub use status::refuse_inferred_as_observed; +/// Refuse to treat an inferred relation as a state transition. +pub use status::refuse_inferred_as_transition; +/// Return whether a status is observed evidence. +pub use status::status_is_observed; +/// Closed vocabulary of presence evidence that is not yet a transition. +pub use status::EvidenceStatus; diff --git a/crates/inferred_status/src/status.rs b/crates/inferred_status/src/status.rs new file mode 100644 index 00000000..65c2fb63 --- /dev/null +++ b/crates/inferred_status/src/status.rs @@ -0,0 +1,143 @@ +//! Observed versus inferred relation evidence status. + +use crate::InferredStatusError; + +/// Closed vocabulary of presence evidence that is not yet a transition. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EvidenceStatus { + /// Directly observed in source documents or authoritative systems. + Observed, + /// Derived by a model, reasoner, or heuristic and not yet promoted. + Inferred, +} + +impl EvidenceStatus { + /// Return the stable wire status name. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Observed => "observed", + Self::Inferred => "inferred", + } + } + + /// Parse a stable wire status name. + /// + /// # Errors + /// + /// Returns [`InferredStatusError::InvalidStatusPayload`] for unrecognized + /// names. + pub fn from_wire_name(name: &str) -> Result { + match name { + "observed" => Ok(Self::Observed), + "inferred" => Ok(Self::Inferred), + _ => Err(InferredStatusError::InvalidStatusPayload), + } + } +} + +/// Return whether a status is observed evidence. +/// +/// # Errors +/// +/// This function is infallible for the closed vocabulary and exists to keep +/// the public comparison surface explicit. +#[allow(clippy::unnecessary_wraps)] +pub fn status_is_observed(status: EvidenceStatus) -> Result { + Ok(matches!(status, EvidenceStatus::Observed)) +} + +/// Refuse to treat an inferred relation as observed evidence. +/// +/// # Errors +/// +/// Returns [`InferredStatusError::InferredIsNotObserved`] when `status` is +/// [`EvidenceStatus::Inferred`]. +pub fn refuse_inferred_as_observed(status: EvidenceStatus) -> Result<(), InferredStatusError> { + match status { + EvidenceStatus::Inferred => Err(InferredStatusError::InferredIsNotObserved), + EvidenceStatus::Observed => Ok(()), + } +} + +/// Refuse to treat an inferred relation as a state transition. +/// +/// # Errors +/// +/// Returns [`InferredStatusError::InferredIsNotTransition`] when `status` is +/// [`EvidenceStatus::Inferred`]. +pub fn refuse_inferred_as_transition(status: EvidenceStatus) -> Result<(), InferredStatusError> { + match status { + EvidenceStatus::Inferred => Err(InferredStatusError::InferredIsNotTransition), + EvidenceStatus::Observed => Ok(()), + } +} + +/// Fraction of recovered evidence statuses that match known truth. +/// +/// # Errors +/// +/// Returns [`InferredStatusError::InvalidStatusPayload`] when either slice is +/// empty or the lengths differ. +pub fn identity_recovery_rate( + truth: &[EvidenceStatus], + decided: &[EvidenceStatus], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(InferredStatusError::InvalidStatusPayload); + } + let mut matches = 0_u32; + for (truth_status, decided_status) in truth.iter().zip(decided) { + if truth_status == decided_status { + matches += 1; + } + } + Ok(f64::from(matches) / truth.len() as f64) +} + +#[cfg(test)] +mod tests { + use super::{ + identity_recovery_rate, refuse_inferred_as_observed, refuse_inferred_as_transition, + status_is_observed, EvidenceStatus, + }; + use crate::InferredStatusError; + + #[test] + fn local_branches_cover_statuses_payloads_and_wire_names() { + assert_eq!( + refuse_inferred_as_observed(EvidenceStatus::Inferred), + Err(InferredStatusError::InferredIsNotObserved) + ); + assert_eq!( + refuse_inferred_as_transition(EvidenceStatus::Inferred), + Err(InferredStatusError::InferredIsNotTransition) + ); + refuse_inferred_as_observed(EvidenceStatus::Observed).expect("observed"); + refuse_inferred_as_transition(EvidenceStatus::Observed).expect("observed"); + assert!(status_is_observed(EvidenceStatus::Observed).expect("observed")); + assert!(!status_is_observed(EvidenceStatus::Inferred).expect("inferred")); + for status in [EvidenceStatus::Observed, EvidenceStatus::Inferred] { + assert_eq!( + EvidenceStatus::from_wire_name(status.wire_name()).expect("round-trip"), + status + ); + } + assert_eq!( + EvidenceStatus::from_wire_name("promoted"), + Err(InferredStatusError::InvalidStatusPayload) + ); + let matched = + identity_recovery_rate(&[EvidenceStatus::Inferred], &[EvidenceStatus::Inferred]) + .expect("rate"); + assert!((matched - 1.0).abs() < f64::EPSILON); + assert_eq!( + identity_recovery_rate(&[], &[]), + Err(InferredStatusError::InvalidStatusPayload) + ); + assert_eq!( + identity_recovery_rate(&[EvidenceStatus::Inferred], &[]), + Err(InferredStatusError::InvalidStatusPayload) + ); + } +} diff --git a/crates/inferred_status/tests/crate_contract.rs b/crates/inferred_status/tests/crate_contract.rs new file mode 100644 index 00000000..f37adc44 --- /dev/null +++ b/crates/inferred_status/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `inferred_status` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "inferred_status"); +} diff --git a/crates/inferred_status/tests/inferred_status_contract.rs b/crates/inferred_status/tests/inferred_status_contract.rs new file mode 100644 index 00000000..3ac0529c --- /dev/null +++ b/crates/inferred_status/tests/inferred_status_contract.rs @@ -0,0 +1,70 @@ +//! Inferred relations cannot be promoted to observed evidence or transitions. + +use inferred_status::{ + identity_recovery_rate, refuse_inferred_as_observed, refuse_inferred_as_transition, + status_is_observed, EvidenceStatus, InferredStatusError, +}; + +#[test] +fn inferred_status_cannot_become_observed_or_a_transition() { + assert_eq!( + refuse_inferred_as_observed(EvidenceStatus::Inferred), + Err(InferredStatusError::InferredIsNotObserved) + ); + assert_eq!( + refuse_inferred_as_transition(EvidenceStatus::Inferred), + Err(InferredStatusError::InferredIsNotTransition) + ); + refuse_inferred_as_observed(EvidenceStatus::Observed).expect("observed stays observed"); + refuse_inferred_as_transition(EvidenceStatus::Observed) + .expect("observed may be considered for promotion elsewhere"); + assert!(status_is_observed(EvidenceStatus::Observed).expect("observed")); + assert!(!status_is_observed(EvidenceStatus::Inferred).expect("inferred")); +} + +#[test] +fn recovered_statuses_match_known_truth_better_than_an_observed_collapse() { + let truth = [ + EvidenceStatus::Observed, + EvidenceStatus::Inferred, + EvidenceStatus::Inferred, + ]; + let recovered = truth; + let collapsed = [ + EvidenceStatus::Observed, + EvidenceStatus::Observed, + EvidenceStatus::Observed, + ]; + let recovered_rate = identity_recovery_rate(&truth, &recovered).expect("recovered"); + let collapsed_rate = identity_recovery_rate(&truth, &collapsed).expect("collapsed"); + let expected = { + let mut matches = 0_u32; + for (truth_status, decided_status) in truth.iter().zip(recovered.iter()) { + if truth_status == decided_status { + 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_mismatched_status_payloads_fail_closed() { + assert_eq!( + identity_recovery_rate(&[], &[]), + Err(InferredStatusError::InvalidStatusPayload) + ); + assert_eq!( + identity_recovery_rate(&[EvidenceStatus::Inferred], &[]), + Err(InferredStatusError::InvalidStatusPayload) + ); + assert_eq!( + identity_recovery_rate( + &[EvidenceStatus::Observed, EvidenceStatus::Inferred], + &[EvidenceStatus::Observed] + ), + Err(InferredStatusError::InvalidStatusPayload) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c29d9743..109161f1 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -14,7 +14,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | Allen relation algebra/bounded closure | ADR 0002; temporal research | PR #9 `temporal_core` path-consistency on protected main | implemented-main | | forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main | implemented-main | | event ontology/evidence mentions | PRD; ADR 0003 | `event_core` mention/instance separation on protected main; `persistence_postgres` mention SQL implemented-main refuses mention-as-instance; event-instance SQL (#39 implemented-main) refuses inverted windows; full intelligence stack remaining | partial | -| time-varying cross-classified multiple membership | PRD; ADR 0003 | `membership_core` network on protected main; multilevel estimators remaining | partial | +| time-varying cross-classified multiple membership | PRD; ADR 0003 | `membership_core` network on protected main; `inferred_status` inferred-versus-observed identity on the active PR; multilevel estimators remaining | partial | | leakage-safe availability/cutoff snapshots | PRD; ADR 0002/0013 | `corpus_split` on protected main | implemented-main | | recovery metrics (RMSE, bias, coverage, graph, temporal order, Monte Carlo SE gates) | PRD; Test Strategy; ADR 0007/0014 | `validation_core` on protected main (PR #19); SE-aware Monte Carlo gates included | implemented-main | | PostgreSQL bitemporal/lineage persistence | ADR 0013; Architecture/ERD | `persistence_postgres` migration contracts, in-memory adapters, live SQL session/document SQL port, tenant RLS (`0002` + session GUC/role helpers), `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` driver, exact-head live PostgreSQL CI with isolation proof, append-only immutability triggers (`0004`), temporal interval ordering CHECKs (`0005`), typed membership assignment (`0006` implemented-main), event-relation/mention/instance SQL (#37–#39 implemented-main), source-artifact SQL (#40 implemented-main), audit-event SQL (#41 implemented-main), concurrent document-write stress (#43 implemented-main), backup/restore integrity revalidation (active PR); remaining physical ERD constraints | partial | diff --git a/docs/adr/0003-relational-event-multiple-membership.md b/docs/adr/0003-relational-event-multiple-membership.md index c5b1a154..4bd40808 100644 --- a/docs/adr/0003-relational-event-multiple-membership.md +++ b/docs/adr/0003-relational-event-multiple-membership.md @@ -1,7 +1,7 @@ # ADR 0003 — Relational event ontology and time-varying multiple membership **Decision status:** Accepted -**Implementation maturity:** partial — membership network and event mention/instance separation implemented-main; typed relation graph with forward-only transitions active-PR; multilevel estimators and persistence remain accepted-target +**Implementation maturity:** partial — membership network and event mention/instance separation implemented-main; inferred-versus-observed identity in `inferred_status` on the active PR; typed relation graph with forward-only transitions active-PR; multilevel estimators and persistence remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0016 owns TDT/CHRONOS event-intelligence task semantics; this ADR remains authoritative for ontology, relation, role, and membership structure. diff --git a/docs/adr/README.md b/docs/adr/README.md index 5d3aa546..aa4ec8f5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,7 +8,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio |---|---|---|---|---| | [0001](0001-rust-first-modular-msa.md) | Rust-first numerical core and CPU `f64` reference | Accepted | partial | ADR 0011 owns cross-service/MSA authority; 0001 retains numerical/backend authority. | | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | -| [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | +| [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles are implemented-main (PR #12); inferred-versus-observed identity is `inferred_status` on the active PR; full multilevel estimators and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | | [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | accepted-target | Downstream psychometric authority; upstream topic/network model is clarified by ADR 0012. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | diff --git a/docs/research/inferred-status-identity.md b/docs/research/inferred-status-identity.md new file mode 100644 index 00000000..46a8cbf9 --- /dev/null +++ b/docs/research/inferred-status-identity.md @@ -0,0 +1,30 @@ +# Inferred status is not observed evidence (doctoring) + +## Scope + +`inferred_status` keeps model, reasoner, and heuristic proposals +inferred. They cannot be treated as observed documentary evidence or as +forward state transitions. Recovery is the computed share of recovered +statuses that match known truth. + +This slice does not persist the graph, allocate migration `0008`, or +replace `relation_graph` or `relation_absence`. + +## Authority + +### Normative TEPP contract + +- `docs/adr/0003-relational-event-multiple-membership.md` — observed + relation evidence, inferred relations, and promoted transition edges + remain distinct. Untrusted LLM output may propose mentions or + relations but cannot promote them without deterministic schema, + evidence, authorization, and scientific validation. + +### Supporting literature + +Moreau and Missier (2013) distinguish generated/derived activity from +the entity it describes. Inference is a derivation, not an observation +of the source. + +Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data +model*. World Wide Web Consortium. https://www.w3.org/TR/prov-dm/ diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 75710ed3..03452601 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -94,7 +94,7 @@ Lebo, T., Sahoo, S., & McGuinness, D. (Eds.). (2013). *PROV-O: The PROV ontology Moreau, L., & Missier, P. (Eds.). (2013). *PROV-DM: The PROV data model*. World Wide Web Consortium. https://www.w3.org/TR/prov-dm/ -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. +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. Inferred relations are a PROV derivation, not an observation, and cannot be promoted to observed evidence or to a state transition without a separate validation gate (Moreau & Missier, 2013). ## Privacy lifecycle, retention, and legal hold diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0..6d568896 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -18,6 +18,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Event mention/instance | `event_core` | partial | — | unit + fail-closed promotion | Task 5 / PR #13 | | Multiple membership | `membership_core` | partial | — | unit + ESS weights | Task 7 / PR #12 + #25 | | Forward transition DAG | `relation_graph` | implemented-main | — | unit + cycle rejection | Task 6 / PR #14 | +| Inferred-versus-observed promotion | `inferred_status` | accepted-target | active PR | refuse inferred-as-observed/transition + recovery vs observed collapse | ADR 0003 | | Bitemporal persistence + live SQL port | `persistence_postgres` | partial | backup/restore integrity | migration contracts + recording transport + optional PgPool + live CI + tenant RLS + `0005`/`0006` + event relation/mention/instance + source-artifact + audit-event + concurrent-write (#37–#43 implemented-main) + restore integrity probes (active PR) | Task 8 / PR #16 + #23 + #26 + #27 + #29 + #30–#43 + restore integrity | | Leakage-safe splits | `corpus_split` | implemented-main | — | cutoff + co-partition tests | Task 9 / PR #17 | | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf5..15903dd4 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "inferred_status", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( From f9aa224728934442d0c36a05ab2bcd0345f0e764 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:16:58 +0900 Subject: [PATCH 2/3] style: apply workspace rustfmt --- crates/inferred_status/src/lib.rs | 4 ++-- crates/inferred_status/src/status.rs | 4 ++-- crates/inferred_status/tests/inferred_status_contract.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/inferred_status/src/lib.rs b/crates/inferred_status/src/lib.rs index 5223fbc9..0ed6bb63 100644 --- a/crates/inferred_status/src/lib.rs +++ b/crates/inferred_status/src/lib.rs @@ -11,6 +11,8 @@ mod status; /// Fail-closed inferred-status errors. pub use error::InferredStatusError; +/// Closed vocabulary of presence evidence that is not yet a transition. +pub use status::EvidenceStatus; /// Fraction of recovered evidence statuses that match known truth. pub use status::identity_recovery_rate; /// Refuse to treat an inferred relation as observed evidence. @@ -19,5 +21,3 @@ pub use status::refuse_inferred_as_observed; pub use status::refuse_inferred_as_transition; /// Return whether a status is observed evidence. pub use status::status_is_observed; -/// Closed vocabulary of presence evidence that is not yet a transition. -pub use status::EvidenceStatus; diff --git a/crates/inferred_status/src/status.rs b/crates/inferred_status/src/status.rs index 65c2fb63..0ee4805d 100644 --- a/crates/inferred_status/src/status.rs +++ b/crates/inferred_status/src/status.rs @@ -98,8 +98,8 @@ pub fn identity_recovery_rate( #[cfg(test)] mod tests { use super::{ - identity_recovery_rate, refuse_inferred_as_observed, refuse_inferred_as_transition, - status_is_observed, EvidenceStatus, + EvidenceStatus, identity_recovery_rate, refuse_inferred_as_observed, + refuse_inferred_as_transition, status_is_observed, }; use crate::InferredStatusError; diff --git a/crates/inferred_status/tests/inferred_status_contract.rs b/crates/inferred_status/tests/inferred_status_contract.rs index 3ac0529c..ade53ccf 100644 --- a/crates/inferred_status/tests/inferred_status_contract.rs +++ b/crates/inferred_status/tests/inferred_status_contract.rs @@ -1,8 +1,8 @@ //! Inferred relations cannot be promoted to observed evidence or transitions. use inferred_status::{ - identity_recovery_rate, refuse_inferred_as_observed, refuse_inferred_as_transition, - status_is_observed, EvidenceStatus, InferredStatusError, + EvidenceStatus, InferredStatusError, identity_recovery_rate, refuse_inferred_as_observed, + refuse_inferred_as_transition, status_is_observed, }; #[test] From 5f910623ada55f78cc85b5b9563dd160e8476a67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 05:26:54 +0900 Subject: [PATCH 3/3] test: derive crate contract from workspace manifest --- 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 2c11f7a5..da59f799 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 workspace_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(workspace_contract.EXPECTED_CRATES)) self.assertTrue(set(crate_roots).issubset(sources)) self.assertGreaterEqual(len(sources), len(crate_roots)) self.assertEqual(docstrings.validate_repository(REPOSITORY_ROOT), [])