diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c2e8dd9..5f282da34 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 +- `relation_graph` causal-identification gate: only `causes` and `intervenes_on` may be described as causal; association, temporal precedence, production, and provenance fail closed. - `tepp_api` naruon live loopback HTTP/1.1 listener: `serve_one` installs a read/write deadline, requires a loopback `Host`, refuses `Transfer-Encoding` and NIM/proxy credential headers, parses `knowledge_cutoff` as RFC 3339 and refuses a future cutoff, keys analysis-run idempotency by tenant plus key, and proves both analysis-run and export POSTs over a real `TcpStream`. Not a production TLS/`$PORT` service (ADR 0011). - `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target. - `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3f0949473..729d869bf 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -33,6 +33,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) | | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | +| Causal-identification gate doctoring | [`docs/research/causal-identification-gate.md`](docs/research/causal-identification-gate.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) | | Adaptive orchestration router doctoring | [`docs/research/adaptive-orchestration-router.md`](docs/research/adaptive-orchestration-router.md) | diff --git a/crates/relation_graph/src/error.rs b/crates/relation_graph/src/error.rs index 234197e47..7f1684d78 100644 --- a/crates/relation_graph/src/error.rs +++ b/crates/relation_graph/src/error.rs @@ -22,6 +22,8 @@ pub enum RelationError { InvalidWirePayload, /// A wire payload used a schema version this crate does not support. UnsupportedWireVersion, + /// An association, precedence, or provenance edge was treated as causation. + CausalClaimNotIdentified, } impl fmt::Display for RelationError { @@ -35,6 +37,7 @@ impl fmt::Display for RelationError { Self::DuplicateRelationEdge => "duplicate relation edge", Self::InvalidWirePayload => "invalid relation wire payload", Self::UnsupportedWireVersion => "unsupported relation wire version", + Self::CausalClaimNotIdentified => "causal claim is not identified", }; formatter.write_str(message) } @@ -72,6 +75,10 @@ mod tests { RelationError::UnsupportedWireVersion, "unsupported relation wire version", ), + ( + RelationError::CausalClaimNotIdentified, + "causal claim is not identified", + ), ] { assert_eq!(error.to_string(), message); } diff --git a/crates/relation_graph/src/kind.rs b/crates/relation_graph/src/kind.rs index aedc1d7d5..7eac2630e 100644 --- a/crates/relation_graph/src/kind.rs +++ b/crates/relation_graph/src/kind.rs @@ -45,6 +45,16 @@ pub enum RelationKind { } impl RelationKind { + /// Return whether this kind may carry an identified causal claim. + /// + /// `Causes` and `IntervenesOn` are the only vocabulary members that may be + /// described as causal. Temporal precedence, enabling, production, and + /// provenance remain non-causal until a later identified design. + #[must_use] + pub const fn is_identified_causal_claim(self) -> bool { + matches!(self, Self::Causes | Self::IntervenesOn) + } + /// Return whether this kind is a forward state-transition edge. #[must_use] pub const fn is_transition_edge(self) -> bool { @@ -112,11 +122,33 @@ impl RelationKind { } } +/// Refuse treating association, precedence, or provenance as causation. +/// +/// # Errors +/// +/// Returns [`RelationError::CausalClaimNotIdentified`] unless `kind` is +/// [`RelationKind::Causes`] or [`RelationKind::IntervenesOn`]. +pub fn refuse_association_as_cause(kind: RelationKind) -> Result<(), RelationError> { + if kind.is_identified_causal_claim() { + Ok(()) + } else { + Err(RelationError::CausalClaimNotIdentified) + } +} + #[cfg(test)] mod tests { use super::RelationKind; use crate::RelationError; + #[test] + fn identified_causal_kinds_are_only_causes_and_intervention() { + assert!(RelationKind::Causes.is_identified_causal_claim()); + assert!(RelationKind::IntervenesOn.is_identified_causal_claim()); + assert!(!RelationKind::LeadsTo.is_identified_causal_claim()); + super::refuse_association_as_cause(RelationKind::Causes).expect("causes"); + } + #[test] fn transition_vocabulary_matches_erd_contract() { for kind in [ diff --git a/crates/relation_graph/src/lib.rs b/crates/relation_graph/src/lib.rs index d349f9a02..a398f09d1 100644 --- a/crates/relation_graph/src/lib.rs +++ b/crates/relation_graph/src/lib.rs @@ -28,6 +28,8 @@ pub use identifier::RelationEdgeId; pub use identifier::RelationEndpointId; /// Closed relation vocabulary with derived transition classification. pub use kind::RelationKind; +/// Refuse treating association or precedence as causation. +pub use kind::refuse_association_as_cause; /// Observed versus inferred relation evidence status. pub use provenance::RelationEvidenceStatus; /// Validate forward-only event-time order for transition edges. diff --git a/crates/relation_graph/tests/causal_identification_contract.rs b/crates/relation_graph/tests/causal_identification_contract.rs new file mode 100644 index 000000000..3aaff5005 --- /dev/null +++ b/crates/relation_graph/tests/causal_identification_contract.rs @@ -0,0 +1,32 @@ +//! Association and temporal precedence are not causal identification. + +use relation_graph::{RelationError, RelationKind, refuse_association_as_cause}; + +#[test] +fn identified_causal_vocabulary_is_allowed_and_associations_are_not() { + refuse_association_as_cause(RelationKind::Causes).expect("causes"); + refuse_association_as_cause(RelationKind::IntervenesOn).expect("intervention"); + + for kind in [ + RelationKind::LeadsTo, + RelationKind::Enables, + RelationKind::References, + RelationKind::Summarizes, + RelationKind::Revises, + RelationKind::Translates, + RelationKind::RetrospectivelyReports, + RelationKind::Supports, + RelationKind::Contradicts, + RelationKind::OutcomeOf, + RelationKind::InputTo, + RelationKind::ProcessTo, + RelationKind::Produces, + RelationKind::TransitionsTo, + ] { + assert_eq!( + refuse_association_as_cause(kind), + Err(RelationError::CausalClaimNotIdentified), + "{kind:?} must not be treated as identified causation" + ); + } +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index a3e674cfb..8c3b500bb 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -13,6 +13,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; PR #5 historical only | implemented-main | | 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 | +| 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 | | time-varying cross-classified multiple membership | PRD; ADR 0003 | `membership_core` network on protected main; multilevel estimators remaining | partial | | leakage-safe availability/cutoff snapshots | PRD; ADR 0002/0013 | `corpus_split` on protected main | implemented-main | diff --git a/docs/adr/0011-standalone-modular-msa-boundary.md b/docs/adr/0011-standalone-modular-msa-boundary.md index d545ee23f..04181fb38 100644 --- a/docs/adr/0011-standalone-modular-msa-boundary.md +++ b/docs/adr/0011-standalone-modular-msa-boundary.md @@ -1,7 +1,7 @@ # ADR 0011 — Standalone operation and modular CWL MSA boundary **Decision status:** Accepted -**Implementation maturity:** partial — Rust crates are independently usable; naruon HTTP interchange and loopback live listener (`POST /v1/analysis-runs` and `/v1/exports`, fail-closed table-access, NIM/proxy headers, RFC 3339 cutoff, stream deadline) are on the active PR (not implemented-main); production TLS/`$PORT` and remaining persistence integrations remain accepted-target +**Implementation maturity:** partial — Rust crates are independently usable; naruon HTTP interchange and loopback live listener (`POST /v1/analysis-runs` and `/v1/exports`, fail-closed table-access, NIM/proxy headers, RFC 3339 cutoff, stream deadline) are on the active PR (not implemented-main); production TLS/`$PORT` and remaining persistence integrations remain accepted-target **Date:** 2026-08-10 **Supersedes:** The broad cross-service ownership wording in ADR 0001. ADR 0001 remains authoritative for Rust-first numerical architecture. diff --git a/docs/connectors/naruon-artifact-consumer.md b/docs/connectors/naruon-artifact-consumer.md index 2e4f4d6c0..5fe0424c4 100644 --- a/docs/connectors/naruon-artifact-consumer.md +++ b/docs/connectors/naruon-artifact-consumer.md @@ -1,6 +1,6 @@ # naruon modular consumer contract for TEPP artifacts -**Status:** Partial — versioned DTO, HTTP interchange, and loopback live listener on the active PR; production TLS/`$PORT` remaining +**Status:** Partial — versioned DTO, HTTP interchange, and loopback live listener on the active PR; production TLS/`$PORT` remaining **Last reviewed:** 2026-08-16 ## Boundary diff --git a/docs/research/causal-identification-gate.md b/docs/research/causal-identification-gate.md new file mode 100644 index 000000000..2f99ee822 --- /dev/null +++ b/docs/research/causal-identification-gate.md @@ -0,0 +1,25 @@ +# Causal identification versus association + +## Scope + +This note doctors the `relation_graph` gate that keeps TEPP from converting association, temporal precedence, or document links into causal language: + +1. only `causes` and `intervenes_on` may be described as identified causal claims; +2. `leads_to`, `enables`, production, input/process, and all provenance kinds fail closed. + +No database migration is allocated. A later identified design can widen the allowed set with an ADR. + +## Authoritative sources + +Pearl, J. (2009). *Causality: Models, reasoning, and inference* (2nd ed.). Cambridge University Press. + +Holland, P. W. (1986). Statistics and causal inference. *Journal of the American Statistical Association, 81*(396), 945–960. https://doi.org/10.1080/01621459.1986.10478354 + +## Application + +Holland (1986) and Pearl (2009) distinguish association and temporal order from an identified causal effect. TEPP therefore refuses to treat `references`, `leads_to`, or `enables` as `causes` without a later identification argument (Holland, 1986; Pearl, 2009). + +## Verification + +- `refuse_association_as_cause(Causes)` and `IntervenesOn` succeed; +- every other closed vocabulary kind returns `CausalClaimNotIdentified`. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index aae1a06e7..b0fda0c0b 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -22,6 +22,7 @@ This report tracks exact-head scientific and engineering evidence required befor | 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 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | +| Causal-identification gate | `relation_graph` | active-PR | association ≠ cause | LeadsTo/References denied | ADR 0003; `docs/research/causal-identification-gate.md` | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | | Purpose-bound provider payloads | `tepp_api` | implemented-main | provider-payload minimization | expired/not-yet-valid/inverted/cross-tenant/impossible-calendar grant, mapping refusal, audited elevated re-id replay | ADR 0009; `docs/research/provider-payload-minimization.md` | | Adaptive orchestration router | `tepp_api` | accepted-target | active PR | mode selection, document-control denial, ablation, credential-free bind | ADR 0010; `docs/research/adaptive-orchestration-router.md` |