From 4b8c8f3001b65c9e1e1b5edf2b75a038e8f96c5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 17:00:49 +0900 Subject: [PATCH] feat(evidence): refuse untrusted payloads as estimator authority Documents, external metadata, serialized records, and LLM outputs stay outside estimator and posterior authority until a scientific role validates (ADR 0008/0014). An LLM output is not source evidence. Identity, size, and authorization bounds are not that semantics gate. Recovery is the computed share of recovered roles that match known truth versus collapsing every payload to an estimator. --- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + Cargo.lock | 4 + Cargo.toml | 2 + README.md | 3 +- crates/payload_semantics/Cargo.toml | 17 ++ crates/payload_semantics/src/error.rs | 73 +++++ crates/payload_semantics/src/lib.rs | 24 ++ crates/payload_semantics/src/semantics.rs | 256 ++++++++++++++++++ .../payload_semantics/tests/crate_contract.rs | 7 + .../tests/payload_semantics_contract.rs | 102 +++++++ docs/TRACEABILITY.md | 2 +- ...e-evidence-identities-digests-and-spans.md | 2 +- ...ic-claim-promotion-and-release-evidence.md | 2 +- docs/adr/README.md | 4 +- docs/research/payload-semantics-identity.md | 39 +++ docs/research/standards-and-literature.md | 2 +- docs/validation/temporal-event-foundation.md | 1 + scripts/check_workspace_contract.py | 1 + tests/quality/test_check_docstrings.py | 3 +- 20 files changed, 538 insertions(+), 8 deletions(-) create mode 100644 crates/payload_semantics/Cargo.toml create mode 100644 crates/payload_semantics/src/error.rs create mode 100644 crates/payload_semantics/src/lib.rs create mode 100644 crates/payload_semantics/src/semantics.rs create mode 100644 crates/payload_semantics/tests/crate_contract.rs create mode 100644 crates/payload_semantics/tests/payload_semantics_contract.rs create mode 100644 docs/research/payload-semantics-identity.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db..732c3476 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 | +| `payload_semantics` | untrusted payloads are not estimator or posterior authority | 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..99fbfe91 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 +- `payload_semantics` scientific-role gate: documents, external metadata, serialized records, and LLM outputs cannot become estimator or posterior authority; an LLM output is not source evidence; identity/size/authorization bounds are not scientific semantics; recovered roles match known truth at a higher computed rate than collapsing every payload to an estimator (ADR 0008/0014). - `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..6507f6bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,10 @@ dependencies = [ "windows-link", ] +[[package]] +name = "payload_semantics" +version = "0.1.0" + [[package]] name = "percent-encoding" version = "2.3.2" diff --git a/Cargo.toml b/Cargo.toml index 92565940..dcfb2921 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/payload_semantics", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/payload_semantics", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d..b0865dc8 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/payload_semantics ``` ## Local verification diff --git a/crates/payload_semantics/Cargo.toml b/crates/payload_semantics/Cargo.toml new file mode 100644 index 00000000..d231f124 --- /dev/null +++ b/crates/payload_semantics/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "payload_semantics" +description = "Untrusted payloads fail closed until scientific semantics validate." +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/payload_semantics/src/error.rs b/crates/payload_semantics/src/error.rs new file mode 100644 index 00000000..cd26e670 --- /dev/null +++ b/crates/payload_semantics/src/error.rs @@ -0,0 +1,73 @@ +//! Fail-closed payload-semantics errors. + +use std::fmt; + +/// A fail-closed payload-semantics error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PayloadSemanticsError { + /// An untrusted payload was treated as estimator or posterior authority. + UntrustedPayloadIsNotEstimator, + /// An LLM output was treated as source evidence. + LlmOutputIsNotEvidence, + /// A document, metadata, or serialized record was treated as interpretation. + EvidenceIsNotInterpretation, + /// Identity, size, or authorization bounds were treated as semantics. + BoundsAreNotSemantics, + /// A recovery slice was empty or length-mismatched. + InvalidSemanticsPayload, +} + +impl fmt::Display for PayloadSemanticsError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::UntrustedPayloadIsNotEstimator => { + "an untrusted payload is not estimator or posterior authority" + } + Self::LlmOutputIsNotEvidence => "an llm output is not source evidence", + Self::EvidenceIsNotInterpretation => { + "a document, metadata, or serialized record is not interpretation" + } + Self::BoundsAreNotSemantics => { + "identity, size, and authorization bounds are not scientific semantics" + } + Self::InvalidSemanticsPayload => "invalid payload-semantics payload", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for PayloadSemanticsError {} + +#[cfg(test)] +mod tests { + use super::PayloadSemanticsError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + PayloadSemanticsError::UntrustedPayloadIsNotEstimator, + "an untrusted payload is not estimator or posterior authority", + ), + ( + PayloadSemanticsError::LlmOutputIsNotEvidence, + "an llm output is not source evidence", + ), + ( + PayloadSemanticsError::EvidenceIsNotInterpretation, + "a document, metadata, or serialized record is not interpretation", + ), + ( + PayloadSemanticsError::BoundsAreNotSemantics, + "identity, size, and authorization bounds are not scientific semantics", + ), + ( + PayloadSemanticsError::InvalidSemanticsPayload, + "invalid payload-semantics payload", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/payload_semantics/src/lib.rs b/crates/payload_semantics/src/lib.rs new file mode 100644 index 00000000..55be9cc6 --- /dev/null +++ b/crates/payload_semantics/src/lib.rs @@ -0,0 +1,24 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Untrusted payloads fail closed until scientific semantics validate. +//! +//! Documents, external metadata, serialized records, and LLM outputs stay +//! untrusted as estimator or posterior authority. Passing identity, size, +//! or authorization bounds is not scientific semantics (ADR 0008/0014). + +mod error; +mod semantics; + +/// Fail-closed payload-semantics errors. +pub use error::PayloadSemanticsError; +/// Closed vocabulary of untrusted inbound payload kinds. +pub use semantics::PayloadKind; +/// Closed vocabulary of claimed scientific roles. +pub use semantics::ScientificRole; +/// Refuse to treat identity, size, or authorization bounds as semantics. +pub use semantics::refuse_bounds_as_semantics; +/// Refuse an untrusted payload that claims an unauthorized scientific role. +pub use semantics::refuse_untrusted_scientific_claim; +/// Fraction of recovered scientific roles that match known truth. +pub use semantics::semantics_recovery_rate; diff --git a/crates/payload_semantics/src/semantics.rs b/crates/payload_semantics/src/semantics.rs new file mode 100644 index 00000000..a9ed5582 --- /dev/null +++ b/crates/payload_semantics/src/semantics.rs @@ -0,0 +1,256 @@ +//! Scientific-role gates for untrusted payloads. + +use crate::PayloadSemanticsError; + +/// Closed vocabulary of untrusted inbound payload kinds. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PayloadKind { + /// External document bytes. + Document, + /// External metadata that is not the document body. + ExternalMetadata, + /// Serialized domain or wire record. + SerializedRecord, + /// LLM or agent output. + LlmOutput, +} + +impl PayloadKind { + /// Return the stable wire payload-kind name. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::Document => "document", + Self::ExternalMetadata => "external_metadata", + Self::SerializedRecord => "serialized_record", + Self::LlmOutput => "llm_output", + } + } + + /// Parse a stable wire payload-kind name. + /// + /// # Errors + /// + /// Returns [`PayloadSemanticsError::InvalidSemanticsPayload`] for + /// unrecognized names. + pub fn from_wire_name(name: &str) -> Result { + match name { + "document" => Ok(Self::Document), + "external_metadata" => Ok(Self::ExternalMetadata), + "serialized_record" => Ok(Self::SerializedRecord), + "llm_output" => Ok(Self::LlmOutput), + _ => Err(PayloadSemanticsError::InvalidSemanticsPayload), + } + } +} + +/// Closed vocabulary of claimed scientific roles. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ScientificRole { + /// Source or metadata evidence context. + EvidenceContext, + /// CPU `f64` estimator result. + EstimatorResult, + /// Posterior summary produced by an estimator. + PosteriorSummary, + /// Interpretation narrative that is not a measurement. + InterpretationNarrative, +} + +impl ScientificRole { + /// Return the stable wire scientific-role name. + #[must_use] + pub const fn wire_name(self) -> &'static str { + match self { + Self::EvidenceContext => "evidence_context", + Self::EstimatorResult => "estimator_result", + Self::PosteriorSummary => "posterior_summary", + Self::InterpretationNarrative => "interpretation_narrative", + } + } + + /// Parse a stable wire scientific-role name. + /// + /// # Errors + /// + /// Returns [`PayloadSemanticsError::InvalidSemanticsPayload`] for + /// unrecognized names. + pub fn from_wire_name(name: &str) -> Result { + match name { + "evidence_context" => Ok(Self::EvidenceContext), + "estimator_result" => Ok(Self::EstimatorResult), + "posterior_summary" => Ok(Self::PosteriorSummary), + "interpretation_narrative" => Ok(Self::InterpretationNarrative), + _ => Err(PayloadSemanticsError::InvalidSemanticsPayload), + } + } +} + +/// Refuse an untrusted payload that claims an unauthorized scientific role. +/// +/// Identity, provenance, size, and depth are owned by `payload_bound`. +/// Grant presence is owned by `intake_authorization`. Checkpoints versus +/// the CPU `f64` estimator are owned by `checkpoint_authority`. +/// +/// # Errors +/// +/// Returns a role-mismatch error when the payload kind cannot hold `role`. +pub fn refuse_untrusted_scientific_claim( + kind: PayloadKind, + role: ScientificRole, +) -> Result<(), PayloadSemanticsError> { + match role { + ScientificRole::EvidenceContext => match kind { + PayloadKind::Document + | PayloadKind::ExternalMetadata + | PayloadKind::SerializedRecord => Ok(()), + PayloadKind::LlmOutput => Err(PayloadSemanticsError::LlmOutputIsNotEvidence), + }, + ScientificRole::EstimatorResult | ScientificRole::PosteriorSummary => { + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + } + ScientificRole::InterpretationNarrative => match kind { + PayloadKind::LlmOutput => Ok(()), + PayloadKind::Document + | PayloadKind::ExternalMetadata + | PayloadKind::SerializedRecord => { + Err(PayloadSemanticsError::EvidenceIsNotInterpretation) + } + }, + } +} + +/// Refuse to treat identity, size, or authorization bounds as semantics. +/// +/// # Errors +/// +/// Always returns [`PayloadSemanticsError::BoundsAreNotSemantics`]. +pub fn refuse_bounds_as_semantics() -> Result<(), PayloadSemanticsError> { + Err(PayloadSemanticsError::BoundsAreNotSemantics) +} + +/// Fraction of recovered scientific roles that match known truth. +/// +/// # Errors +/// +/// Returns [`PayloadSemanticsError::InvalidSemanticsPayload`] when either +/// slice is empty or the lengths differ. +pub fn semantics_recovery_rate( + truth: &[ScientificRole], + decided: &[ScientificRole], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(PayloadSemanticsError::InvalidSemanticsPayload); + } + let mut matches = 0_u32; + for (truth_role, decided_role) in truth.iter().zip(decided) { + if truth_role == decided_role { + matches += 1; + } + } + Ok(f64::from(matches) / truth.len() as f64) +} + +#[cfg(test)] +mod tests { + use super::{ + PayloadKind, ScientificRole, refuse_bounds_as_semantics, refuse_untrusted_scientific_claim, + semantics_recovery_rate, + }; + use crate::PayloadSemanticsError; + + #[test] + fn local_branches_cover_kinds_roles_and_payloads() { + for kind in [ + PayloadKind::Document, + PayloadKind::ExternalMetadata, + PayloadKind::SerializedRecord, + ] { + refuse_untrusted_scientific_claim(kind, ScientificRole::EvidenceContext) + .expect("evidence"); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::EstimatorResult), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::PosteriorSummary), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::InterpretationNarrative), + Err(PayloadSemanticsError::EvidenceIsNotInterpretation) + ); + assert_eq!( + PayloadKind::from_wire_name(kind.wire_name()).expect("kind"), + kind + ); + } + refuse_untrusted_scientific_claim( + PayloadKind::LlmOutput, + ScientificRole::InterpretationNarrative, + ) + .expect("interpretation"); + assert_eq!( + refuse_untrusted_scientific_claim( + PayloadKind::LlmOutput, + ScientificRole::EvidenceContext + ), + Err(PayloadSemanticsError::LlmOutputIsNotEvidence) + ); + assert_eq!( + refuse_untrusted_scientific_claim( + PayloadKind::LlmOutput, + ScientificRole::EstimatorResult + ), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim( + PayloadKind::LlmOutput, + ScientificRole::PosteriorSummary + ), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_bounds_as_semantics(), + Err(PayloadSemanticsError::BoundsAreNotSemantics) + ); + for role in [ + ScientificRole::EvidenceContext, + ScientificRole::EstimatorResult, + ScientificRole::PosteriorSummary, + ScientificRole::InterpretationNarrative, + ] { + assert_eq!( + ScientificRole::from_wire_name(role.wire_name()).expect("role"), + role + ); + } + assert_eq!( + PayloadKind::from_wire_name("trusted"), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + assert_eq!( + ScientificRole::from_wire_name("causal_effect"), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + assert_eq!( + PayloadKind::from_wire_name(PayloadKind::LlmOutput.wire_name()).expect("llm"), + PayloadKind::LlmOutput + ); + let matched = semantics_recovery_rate( + &[ScientificRole::EvidenceContext], + &[ScientificRole::EvidenceContext], + ) + .expect("rate"); + assert!((matched - 1.0).abs() < f64::EPSILON); + assert_eq!( + semantics_recovery_rate(&[], &[]), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + assert_eq!( + semantics_recovery_rate(&[ScientificRole::EvidenceContext], &[]), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + } +} diff --git a/crates/payload_semantics/tests/crate_contract.rs b/crates/payload_semantics/tests/crate_contract.rs new file mode 100644 index 00000000..7b435a84 --- /dev/null +++ b/crates/payload_semantics/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `payload_semantics` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "payload_semantics"); +} diff --git a/crates/payload_semantics/tests/payload_semantics_contract.rs b/crates/payload_semantics/tests/payload_semantics_contract.rs new file mode 100644 index 00000000..acd4d837 --- /dev/null +++ b/crates/payload_semantics/tests/payload_semantics_contract.rs @@ -0,0 +1,102 @@ +//! Untrusted payloads fail closed until scientific semantics validate. + +use payload_semantics::{ + PayloadKind, PayloadSemanticsError, ScientificRole, refuse_bounds_as_semantics, + refuse_untrusted_scientific_claim, semantics_recovery_rate, +}; + +#[test] +fn untrusted_payloads_fail_closed_until_scientific_semantics_validate() { + for kind in [ + PayloadKind::Document, + PayloadKind::ExternalMetadata, + PayloadKind::SerializedRecord, + ] { + refuse_untrusted_scientific_claim(kind, ScientificRole::EvidenceContext) + .expect("evidence context"); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::EstimatorResult), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::PosteriorSummary), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim(kind, ScientificRole::InterpretationNarrative), + Err(PayloadSemanticsError::EvidenceIsNotInterpretation) + ); + } + + refuse_untrusted_scientific_claim( + PayloadKind::LlmOutput, + ScientificRole::InterpretationNarrative, + ) + .expect("interpretation"); + assert_eq!( + refuse_untrusted_scientific_claim(PayloadKind::LlmOutput, ScientificRole::EvidenceContext), + Err(PayloadSemanticsError::LlmOutputIsNotEvidence) + ); + assert_eq!( + refuse_untrusted_scientific_claim(PayloadKind::LlmOutput, ScientificRole::EstimatorResult), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_untrusted_scientific_claim(PayloadKind::LlmOutput, ScientificRole::PosteriorSummary), + Err(PayloadSemanticsError::UntrustedPayloadIsNotEstimator) + ); + assert_eq!( + refuse_bounds_as_semantics(), + Err(PayloadSemanticsError::BoundsAreNotSemantics) + ); +} + +#[test] +fn recovered_roles_match_known_truth_better_than_estimator_collapse() { + let truth = [ + ScientificRole::EvidenceContext, + ScientificRole::InterpretationNarrative, + ScientificRole::EstimatorResult, + ]; + let recovered = truth; + let collapsed = [ + ScientificRole::EstimatorResult, + ScientificRole::EstimatorResult, + ScientificRole::EstimatorResult, + ]; + let recovered_rate = semantics_recovery_rate(&truth, &recovered).expect("recovered"); + let collapsed_rate = semantics_recovery_rate(&truth, &collapsed).expect("collapsed"); + let expected = { + let mut matches = 0_u32; + for (truth_role, decided_role) in truth.iter().zip(recovered.iter()) { + if truth_role == decided_role { + 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_role_payloads_fail_closed() { + assert_eq!( + semantics_recovery_rate(&[], &[]), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + assert_eq!( + semantics_recovery_rate(&[ScientificRole::EvidenceContext], &[]), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); + assert_eq!( + semantics_recovery_rate( + &[ + ScientificRole::EvidenceContext, + ScientificRole::InterpretationNarrative + ], + &[ScientificRole::EvidenceContext] + ), + Err(PayloadSemanticsError::InvalidSemanticsPayload) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index c29d9743..b9aaed64 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -42,7 +42,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | autonomous model proposal separated from verification/publication/review/merge | ADR 0015 | future safe OpenCode/NVIDIA autonomous-development workflow | accepted-target | | contextual-orchestrator execution boundary | ADR 0010/0011 | provider-neutral orchestration port; TEPP retains scientific authority | accepted-target | | foundation validation / release-readiness ledger | ADR 0014; Test Strategy | PR #24 `docs/validation/temporal-event-foundation.md` on protected main | implemented-main | -| scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | documentation/CI/domain validation/release evidence | partial | +| scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | `payload_semantics` scientific-role gate on the active PR; documentation/CI/domain validation/release evidence | partial | | CSAP/SOC 2/ISO/NIST assurance readiness | `docs/COMPLIANCE_READINESS.md`; research register | repository controls + future deployment evidence | accepted-target / deployment-owned | | threat-model controls and scientific-integrity security | `SECURITY.md`; `docs/THREAT_MODEL.md` | deterministic security/privacy/scientific validation gates | partial | | accessible bitemporal/network/drift/invariance views | PRD/UML | future `visual_analytics`; Figma in approved visual phase | accepted-target | diff --git a/docs/adr/0008-immutable-evidence-identities-digests-and-spans.md b/docs/adr/0008-immutable-evidence-identities-digests-and-spans.md index a13e1d4a..9d99410e 100644 --- a/docs/adr/0008-immutable-evidence-identities-digests-and-spans.md +++ b/docs/adr/0008-immutable-evidence-identities-digests-and-spans.md @@ -41,7 +41,7 @@ A content digest proves only byte equality/difference under the selected algorit ## Consequences -Evidence records remain stable even when identical content is ingested into distinct provenance contexts. Exact spans round-trip across Unicode/page evidence. Strict wire reconstruction detects substitution, stale ownership, unknown extensions, unsupported versions, and hostile coordinate changes. Locale/language segmentation remains a separate concern under ADR 0004. +Evidence records remain stable even when identical content is ingested into distinct provenance contexts. Exact spans round-trip across Unicode/page evidence. Strict wire reconstruction detects substitution, stale ownership, unknown extensions, unsupported versions, and hostile coordinate changes. Untrusted payloads still require a scientific-role gate (`payload_semantics` on the active PR) before they can be treated as estimator or posterior authority. Locale/language segmentation remains a separate concern under ADR 0004. ## Failure and recovery diff --git a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md index 81ebb373..d47c670f 100644 --- a/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md +++ b/docs/adr/0014-scientific-claim-promotion-and-release-evidence.md @@ -1,7 +1,7 @@ # ADR 0014 — Scientific claim promotion and release evidence authority **Decision status:** Accepted -**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; full package/image release bundle and scientific claim promotion packages remain accepted-target +**Implementation maturity:** partial — claim/promotion authority documented; repository SBOM/provenance evidence generator and CI validation implemented; untrusted-payload scientific-role refusal is `payload_semantics` on the active PR; full package/image release bundle and scientific claim promotion packages remain accepted-target **Date:** 2026-08-12 **Supersedes:** None; extends ADR 0007 from repository quality tooling to product/scientific claim authority. diff --git a/docs/adr/README.md b/docs/adr/README.md index 5d3aa546..791ad06c 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -13,13 +13,13 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [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. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | -| [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | +| [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | Untrusted-payload scientific-role refusal is `payload_semantics` on the active PR. ADR 0013 governs future persistence/reproducibility/split authority. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | partial | Persistence retention/deletion/legal-hold (`0007`) is on the active PR; authorization/export/provider adapters and deployment evidence remain accepted-target. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | | [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold on the active PR; remaining physical ERD/backup accepted-target. | -| [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | +| [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; untrusted-payload scientific-role refusal is `payload_semantics` on the active PR; 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. | diff --git a/docs/research/payload-semantics-identity.md b/docs/research/payload-semantics-identity.md new file mode 100644 index 00000000..9c27250f --- /dev/null +++ b/docs/research/payload-semantics-identity.md @@ -0,0 +1,39 @@ +# Untrusted payloads require scientific semantics (doctoring) + +## Scope + +`payload_semantics` keeps documents, external metadata, serialized +records, and LLM outputs out of estimator and posterior authority. +An LLM output is interpretation, not source evidence. Identity, size, +and authorization bounds are not that scientific-role gate. Recovery is +the computed share of recovered roles that match known truth. + +This slice does not persist payloads, allocate migration `0008`, or +replace `payload_bound` (identity/provenance/size/depth), +`intake_authorization` (grant presence), or `checkpoint_authority` +(checkpoint versus estimator). + +## Authority + +### Normative TEPP contract + +- `docs/adr/0008-immutable-evidence-identities-digests-and-spans.md` — + untrusted wire and source payloads reconstruct only through validated + domain constructors. +- `docs/adr/0014-scientific-claim-promotion-and-release-evidence.md` — + an LLM output cannot promote a scientific claim or replace the CPU + `f64` estimator. +- `AGENTS.md` — documents, external metadata, serialized payloads, + checkpoints, and LLM outputs are untrusted until identity, provenance, + size/depth, authorization, and scientific semantics validate. + +### Supporting literature + +The *Standards for Educational and Psychological Testing* treat score +meaning as an interpretive argument that requires validity evidence. +A narrative, metadata record, or serialized buffer is not that score. + +American Educational Research Association, American Psychological +Association, & National Council on Measurement in Education. (2014). +*Standards for educational and psychological testing*. American +Educational Research Association. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index 75710ed3..7d163e0a 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -12,7 +12,7 @@ Asparouhov, T., & Muthén, B. (2009). Exploratory structural equation modeling. Marsh, H. W., Morin, A. J. S., Parker, P. D., & Kaur, G. (2014). Exploratory structural equation modeling: An integration of the best features of exploratory and confirmatory factor analysis. *Annual Review of Clinical Psychology, 10*, 85–110. https://doi.org/10.1146/annurev-clinpsy-032813-153700 -TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. +TEPP applies these sources to construct definition, score interpretation, reliability, validity evidence, uncertainty, consequences, longitudinal invariance, ESEM cross-loadings, and DSEM. Topic outputs are treated as fallible indicators or components only after their construct role is evaluated. An untrusted document, metadata record, serialized buffer, or LLM narrative is not that score; `payload_semantics` refuses estimator and posterior claims until a validated scientific role is present (AERA, APA, & NCME, 2014). ## Structural, correlated, dynamic, relational, and multilingual topic models diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 295fbae0..2596f1aa 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -25,6 +25,7 @@ This report tracks exact-head scientific and engineering evidence required befor | 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 | | 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 | +| Untrusted payload scientific semantics | `payload_semantics` | accepted-target | active PR | refuse estimator/posterior claims + refuse LLM-as-evidence + refuse bounds-as-semantics + recovery vs estimator collapse | ADR 0008/0014; AGENTS.md | ## Scientific acceptance checklist (foundation) diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf5..6b4df8ea 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "payload_semantics", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = ( diff --git a/tests/quality/test_check_docstrings.py b/tests/quality/test_check_docstrings.py index 2c11f7a5..56d553d2 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), [])