Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ boundaries above remain the target modular MSA architecture.
| `relation_graph` | typed relations and forward-transition validation |
| `membership_core` | time-varying cross-classified multiple membership, Kish ESS, nested ICC with non-nested refusal |
| `role_contradiction` | customer and competitor cannot occupy the same group |
| `relation_absence` | unobserved relation pairs are not evidence of no relationship |
| `persistence_postgres` | PostgreSQL repositories and migrations |
| `corpus_split` | cutoff-safe, relation-aware partitioning |
| `tepp_simulation` | known-truth temporal/event data generation |
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang
- `psychometric_core` posterior-aware structural input gates: construct classification, refusal of raw-proportion Pearson/OLS, explicit ALR-versus-ILR geometry boundaries, CPU `f64` OLS recovery, posterior-draw loading point-estimate averaging without Rubin uncertainty claims, invariance-gated latent-mean comparison, and causal-heuristic refusal (ADR 0005 first production slice; no new migration).
### Added

- `relation_absence` identity gate: unobserved relation pairs cannot become evidence of no relationship; recovered observed/inferred/unobserved statuses match known truth at a higher computed rate than collapsing every status to observed (ADR 0003).
- `orchestrator_live` loopback HTTP/1.1 listener: `POST /v1/interpretation-runs` binds loopback only, replays matching idempotency keys, and refuses non-loopback binds, table-access hosts, review/Copilot/GitHub credentials, and scientific-authority promotion. Accepted output is always hypothetical. Not TLS termination or model execution (ADR 0010; ADR 0011).
- `role_contradiction` identity gate: customer and competitor cannot occupy the same group; recovered commercial-role labels match known truth at a higher computed rate than collapsing every role to customer (ADR 0003).
- `episode_membership` identity gate: a document's episode membership cannot
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ members = [
"crates/compute_backend",
"crates/membership_target",
"crates/role_contradiction",
"crates/relation_absence",
"crates/analysis_engine",
"crates/topic_measurement",
"crates/psychometric_core",
Expand Down Expand Up @@ -111,6 +112,7 @@ default-members = [
"crates/compute_backend",
"crates/membership_target",
"crates/role_contradiction",
"crates/relation_absence",
"crates/analysis_engine",
"crates/topic_measurement",
"crates/psychometric_core",
Expand Down
2 changes: 2 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Scientific claim-promotion gate doctoring | [`docs/research/scientific-claim-promotion-gates.md`](docs/research/scientific-claim-promotion-gates.md) |
| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) |
| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) |
| Relation absence is not negative evidence | [`docs/research/relation-absence-not-negative.md`](docs/research/relation-absence-not-negative.md) |
| Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) |
| Mention-confidence Brier doctoring | [`docs/research/mention-confidence-brier.md`](docs/research/mention-confidence-brier.md) |
| Event-intelligence status-gate doctoring | [`docs/research/event-intelligence-status-gates.md`](docs/research/event-intelligence-status-gates.md) |
Expand Down Expand Up @@ -143,6 +144,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Retention/deletion/legal-hold doctoring | [`docs/research/retention-deletion-legal-hold.md`](docs/research/retention-deletion-legal-hold.md) |
| Stopword-deletion doctoring | [`docs/research/stopword-deletion.md`](docs/research/stopword-deletion.md) |
| Provider-payload minimization doctoring | [`docs/research/provider-payload-minimization.md`](docs/research/provider-payload-minimization.md) |
| Relation absence is not negative evidence | [`docs/research/relation-absence-not-negative.md`](docs/research/relation-absence-not-negative.md) |
| Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) |
| Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) |
| Corpus-split leakage-audit wire doctoring | [`docs/research/corpus-split-manifest-wire.md`](docs/research/corpus-split-manifest-wire.md) |
Expand Down
2 changes: 2 additions & 0 deletions README.md

@devin-ai-integration devin-ai-integration Bot Aug 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Stale README crate counts

README.md still states 50 and 54 crates (README.md:21, README.md:28); the workspace now has 57 members after relation_absence. The counts were already stale before this PR and lie outside the changed hunks.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ crates/prompt_source
crates/provider_receipt
crates/psychometric_core
crates/psychometric_fit
crates/relation_absence
crates/relation_graph
crates/retrospective_edge
crates/revision_order
crates/role_contradiction
crates/semantic_core
crates/service_tls
crates/stopword_deletion
Expand Down
17 changes: 17 additions & 0 deletions crates/relation_absence/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "relation_absence"
description = "Unobserved relation pairs are not evidence of no relationship."
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
48 changes: 48 additions & 0 deletions crates/relation_absence/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Fail-closed relation-absence errors.

use std::fmt;

/// A fail-closed relation-absence error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RelationAbsenceError {
/// An unobserved pair was treated as evidence of no relationship.
AbsenceIsNotNegative,
/// A recovery slice was empty or length-mismatched.
InvalidObservationPayload,
}

impl fmt::Display for RelationAbsenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::AbsenceIsNotNegative => {
"unobserved relation pairs are not evidence of no relationship"
}
Self::InvalidObservationPayload => "invalid relation-absence payload",
};
formatter.write_str(message)
}
}

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

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

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
RelationAbsenceError::AbsenceIsNotNegative,
"unobserved relation pairs are not evidence of no relationship",
),
(
RelationAbsenceError::InvalidObservationPayload,
"invalid relation-absence payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
19 changes: 19 additions & 0 deletions crates/relation_absence/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Unobserved relation pairs are not evidence of no relationship.
//!
//! Observed and inferred statuses stay distinct. Missing pairs remain
//! unobserved and never become negative edges (ADR 0003).

mod error;
mod status;

/// Fail-closed relation-absence errors.
pub use error::RelationAbsenceError;
/// Closed vocabulary of observed, inferred, and unobserved statuses.
pub use status::ObservationStatus;
/// Refuse to treat an unobserved pair as evidence of no relationship.
pub use status::refuse_absence_as_negative;
/// Fraction of recovered observation statuses that match known truth.
pub use status::status_recovery_rate;
128 changes: 128 additions & 0 deletions crates/relation_absence/src/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! Observed, inferred, and unobserved relation statuses stay distinct.

use crate::RelationAbsenceError;

/// Closed vocabulary of relation observation statuses.
///
/// Unobserved is a missing-status, not a negative edge.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ObservationStatus {
/// Directly observed in source documents or authoritative systems.
Observed,
/// Derived by a model, reasoner, or heuristic and not yet promoted.
Inferred,
/// No observed or inferred evidence exists for this pair.
Unobserved,
}

impl ObservationStatus {
/// Return the stable wire status name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Observed => "observed",
Self::Inferred => "inferred",
Self::Unobserved => "unobserved",
}
}

/// Parse a stable wire status name.
///
/// # Errors
///
/// Returns [`RelationAbsenceError::InvalidObservationPayload`] for
/// unrecognized names, including `no_relationship`.
pub fn from_wire_name(name: &str) -> Result<Self, RelationAbsenceError> {
match name {
"observed" => Ok(Self::Observed),
"inferred" => Ok(Self::Inferred),
"unobserved" => Ok(Self::Unobserved),
_ => Err(RelationAbsenceError::InvalidObservationPayload),
}
}
}

/// Refuse to treat an unobserved pair as evidence of no relationship.
///
/// Observed and inferred statuses are presence evidence. They are not
/// absence, so this gate lets them through.
///
/// # Errors
///
/// Returns [`RelationAbsenceError::AbsenceIsNotNegative`] when `status` is
/// [`ObservationStatus::Unobserved`].
pub fn refuse_absence_as_negative(status: ObservationStatus) -> Result<(), RelationAbsenceError> {
match status {
ObservationStatus::Unobserved => Err(RelationAbsenceError::AbsenceIsNotNegative),
ObservationStatus::Observed | ObservationStatus::Inferred => Ok(()),
}
}

/// Fraction of recovered observation statuses that match known truth.
///
/// # Errors
///
/// Returns [`RelationAbsenceError::InvalidObservationPayload`] when either
/// slice is empty or the lengths differ.
pub fn status_recovery_rate(
truth: &[ObservationStatus],
decided: &[ObservationStatus],
) -> Result<f64, RelationAbsenceError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(RelationAbsenceError::InvalidObservationPayload);
}
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::{ObservationStatus, refuse_absence_as_negative, status_recovery_rate};
use crate::RelationAbsenceError;

#[test]
fn local_branches_cover_statuses_payloads_and_wire_names() {
assert_eq!(
refuse_absence_as_negative(ObservationStatus::Unobserved),
Err(RelationAbsenceError::AbsenceIsNotNegative)
);
refuse_absence_as_negative(ObservationStatus::Observed).expect("observed");
refuse_absence_as_negative(ObservationStatus::Inferred).expect("inferred");
for status in [
ObservationStatus::Observed,
ObservationStatus::Inferred,
ObservationStatus::Unobserved,
] {
assert_eq!(
ObservationStatus::from_wire_name(status.wire_name()).expect("round-trip"),
status
);
}
assert_eq!(
ObservationStatus::from_wire_name("no_relationship"),
Err(RelationAbsenceError::InvalidObservationPayload)
);
let truth = [ObservationStatus::Observed, ObservationStatus::Unobserved];
let matched = status_recovery_rate(&truth, &truth).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
let partial = status_recovery_rate(
&truth,
&[ObservationStatus::Observed, ObservationStatus::Observed],
)
.expect("partial");
assert!((partial - 0.5).abs() < f64::EPSILON);
assert_eq!(
status_recovery_rate(&[], &[]),
Err(RelationAbsenceError::InvalidObservationPayload)
);
assert_eq!(
status_recovery_rate(&truth, &[]),
Err(RelationAbsenceError::InvalidObservationPayload)
);
}
}
62 changes: 62 additions & 0 deletions crates/relation_absence/tests/absence_status_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Observed, inferred, and unobserved statuses stay distinct.

use relation_absence::{
ObservationStatus, RelationAbsenceError, refuse_absence_as_negative, status_recovery_rate,
};

#[test]
fn unobserved_pairs_cannot_become_negative_evidence() {
assert_eq!(
refuse_absence_as_negative(ObservationStatus::Unobserved),
Err(RelationAbsenceError::AbsenceIsNotNegative)
);
refuse_absence_as_negative(ObservationStatus::Observed).expect("observed is not absence");
refuse_absence_as_negative(ObservationStatus::Inferred).expect("inferred is not absence");
}

#[test]
fn recovered_statuses_match_known_truth_better_than_an_absence_collapse() {
let truth = [
ObservationStatus::Observed,
ObservationStatus::Inferred,
ObservationStatus::Unobserved,
];
let recovered = truth;
let collapsed = [
ObservationStatus::Observed,
ObservationStatus::Observed,
ObservationStatus::Observed,
];
let recovered_rate = status_recovery_rate(&truth, &recovered).expect("recovered");
let collapsed_rate = status_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!(
status_recovery_rate(&[], &[]),
Err(RelationAbsenceError::InvalidObservationPayload)
);
assert_eq!(
status_recovery_rate(&[ObservationStatus::Observed], &[]),
Err(RelationAbsenceError::InvalidObservationPayload)
);
assert_eq!(
status_recovery_rate(
&[ObservationStatus::Observed, ObservationStatus::Unobserved],
&[ObservationStatus::Observed]
),
Err(RelationAbsenceError::InvalidObservationPayload)
);
}
7 changes: 7 additions & 0 deletions crates/relation_absence/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `relation_absence` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "relation_absence");
}
1 change: 1 addition & 0 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| six distinct clocks and uncertain intervals | PRD; ADR 0002; ISO 24617-1:2012; Hobbs & Pan (2017) | merged PR #8 `temporal_core` on protected main; PR #5 historical lineage only | implemented-main |
| Allen relation algebra/bounded closure | ADR 0002; Allen (1983) | merged 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 |
| forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main; `relation_absence` unobserved-is-not-negative gate on the active PR | partial |
| 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; Brier calibration on the active PR; full intelligence stack remaining | partial |
| no unidentified causal language from association/precedence | ADR 0002/0003; research | `relation_graph` causal-identification gate on the active PR | active-PR |
| 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 |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0003-relational-event-multiple-membership.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
**Implementation maturity:** partial — membership network, event mention/instance separation, inferred/evidential/retrospective status gates, summary/source identity separation, template-copy/source identity separation, typed forward-only relation graph, strict input-process-outcome ordering, nested ICC refusal, and subevent parent-window containment are implemented-main; full multilevel/MMMC estimators and remaining persistence remain accepted-target.
**Date:** 2026-08-24
**Decision status:** Accepted
**Implementation maturity:** partial — membership network/roles with Kish ESS and nested ICC (cross-classified/multiple-membership refusal), event mention/instance separation, the typed forward-only relation graph, and the copy/summary/outcome-order/support/inferred-status/retrospective-reporting/location identity gates are implemented-main; typed target-kind membership identity in `membership_target` is on PR #131; multilevel psychometric estimators and remaining persistence details follow ADR 0013 and [`docs/TRACEABILITY.md`](../TRACEABILITY.md) as accepted-target. The customer/competitor role-contradiction gate ships in `role_contradiction` on this PR.
**Implementation maturity:** partial — membership network/roles with Kish ESS and nested ICC (cross-classified/multiple-membership refusal), event mention/instance separation, the typed forward-only relation graph, and the copy/summary/outcome-order/support/inferred-status/retrospective-reporting/location identity gates are implemented-main; typed target-kind membership identity in `membership_target` is on PR #131; multilevel psychometric estimators and remaining persistence details follow ADR 0013 and [`docs/TRACEABILITY.md`](../TRACEABILITY.md) as accepted-target. The customer/competitor role-contradiction gate ships in `role_contradiction` on this PR. The absence-is-not-negative identity gate ships in `relation_absence` on this PR.
**Supersedes:** None. ADR 0016 owns TDT/CHRONOS event-intelligence task semantics; this ADR remains authoritative for ontology, relation, role, and membership structure.

## Context
Expand Down
Loading
Loading