Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

## [Unreleased]

- `tepp_api` keeps `AnalysisRunRequest` / `AnalysisRunAccepted` metric-free and allows `tepp.scientific_acceptance.v1` only on a succeeded terminal result with output profile `scientific_acceptance_v1`. Receipt RMSE/bias/coverage/SE-gate keys (including both standard errors, Wilson upper, and temporal-order accuracy), a scientific-acceptance profile without the artifact, a failed terminal carrying the artifact, an all-zero or run-id-mismatched binding digest, a model that does not match the request, a future or malformed cutoff, negative RMSE/SEs, out-of-range coverage, inverted Wilson bounds, and an `se_gate_accepted` flag inconsistent with `|RMSE| ≤ k · SE(RMSE)` fail closed. This is the GAP-003A API wire slice for issue #166; `analysis_engine` library binding stays on live #356; persistence remains GAP-003B.

- `event_core` adds bounded Allen interval-consistency classification, atomic path-consistency closure, contradiction/resource refusals, and an explicit dependency-error fallback without claiming unrestricted global satisfiability.

- `psychometric_core` recovers the Driver, Oud, and Voelkle (2017, Table 2, p. 12 `MANIFESTTRAITVAR`; §7.1, p. 19; p. 16 `MANIFESTTRAITVARstd`; footnote 4; 2017-era ctsem `summary.ctsemFit.R`; JSS PDF re-opened 2026-08-27T14:20Z from https://www.jstatsoft.org/index.php/jss/article/download/v077i05/1104) scalar standardised manifest-trait variance on current main after `0ce16e8` dropped the pre-consolidation code while research notes already named the map (register items 83–84). Table 2 names `MANIFESTTRAITVAR` `Ψ_τ` the additional time-invariant variance-covariance on the measurement level and sets it `NULL` when there is no manifest trait. Equation 5 writes `Γ ~ N(τ, Ψ)` and names that covariance the manifest traits. Section 7.1 names manifest traits stable individual differences in indicator levels, distinct from process-level `TRAITVAR` `φ_ξ`. Page 16 prints standardised matrices with the suffix `std` when appropriate. The printed example on p. 16 is `discreteDRIFTstd`, not `MANIFESTTRAITVARstd`. Footnote 4 standardises using only the relevant variance, not the total. The relevant variance for that named indicator-level correlation is `MANIFESTTRAITVAR`, not process-level `TRAITVAR` and not residual `MANIFESTVAR` `θ`. The 2017-era source forms `MANIFESTTRAITVARstd` only when `MANIFESTTRAITVAR != 0`, as `solve(sqrt(diag(MANIFESTTRAITVAR) + ridging)) %&% MANIFESTTRAITVAR` when `verbose = TRUE`. OpenMx `%&%` is `t(A) %*% B %*% A`. Unlike `TRAITVARstd`, that formation adds `diag(c(ridging), n.manifest)`. The default `ridging = FALSE` adds 0, not `0.0001`; that ridge is a numerical hack and is not this exact map. The scalar correlation is `ψ / ψ = 1` after strictly positive `MANIFESTTRAITVAR`. Form strictly positive `ψ` first, then `1 / √ψ`, then `(1 / √ψ) ψ (1 / √ψ)`. Unstandardised `MANIFESTTRAITVAR` is defined for a zero trait; standardised `MANIFESTTRAITVAR` is not. Zero `MANIFESTTRAITVAR` skips forming `MANIFESTTRAITVARstd` in the 2017-era source and fails closed here. Indicator-level trait variance is an event-time structural quantity, so a non-event clock fails closed. `MANIFESTTRAITVAR` does not require stable `a < 0`. Distinct positive `ψ` recover the same 1. `trait / trait = 1` is `TRAITVARstd` and recovers the same number and remains a distinct named quantity. `θ` is `MANIFESTVAR` and is measurement error, not this correlation. Meredith (1993) remains unread (web search 2026-08-27T14:20Z: Springer/Cambridge Core paywalled; Unpaywall historically `is_oa: false`; Springer `content/pdf` is an HTML stub). Mislevy (1991, *Psychometrika, 56*, 177–196) remains unread on the same terms (DOI `10.1007/bf02294457`). Still not a Kalman filter, not a matrix `expm`, not ESEM estimation, not DSEM, and not ctsem estimation.
Expand Down
87 changes: 85 additions & 2 deletions crates/tepp_api/src/analysis_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
//! redacted failure code.

use crate::analysis_run::require_rfc3339_knowledge_cutoff;
use crate::scientific_acceptance::{
SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE, SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION,
ScientificAcceptanceArtifact,
};
use crate::wire::{
from_json, require_byte_limit, require_contract_version, require_nonempty, to_json,
};
Expand Down Expand Up @@ -86,7 +90,8 @@ impl AnalysisResultSummary {
/// The succeeded shape excludes source text, credentials, direct identity,
/// respondent/item records, and unrestricted model output. The failed shape
/// contains no measurement artifact.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[serde(deny_unknown_fields)]
pub struct AnalysisRunTerminalResult {
/// Semantic contract version.
Expand Down Expand Up @@ -119,6 +124,10 @@ pub struct AnalysisRunTerminalResult {
pub summary: Option<AnalysisResultSummary>,
/// Stable snake-case code for a failed run.
pub failure_code: Option<String>,
/// Scientific-acceptance artifact, present only on a succeeded
/// `scientific_acceptance_v1` terminal result.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scientific_acceptance: Option<ScientificAcceptanceArtifact>,
}

impl AnalysisRunTerminalResult {
Expand Down Expand Up @@ -153,6 +162,51 @@ impl AnalysisRunTerminalResult {
completed_at: completed_at.into(),
summary: Some(summary),
failure_code: None,
scientific_acceptance: None,
};
value.validate()?;
require_terminal_binding(request, accepted, &value)?;
Ok(value)
}

/// Construct a succeeded terminal result that carries scientific acceptance.
///
/// The request and accepted receipt remain metric-free. The artifact digest
/// becomes `result_sha256` and the schema is `tepp.scientific_acceptance.v1`.
///
/// # Errors
///
/// Returns a fail-closed error for invalid shape, digest, time, summary,
/// artifact, or request/receipt binding.
pub fn succeeded_scientific_acceptance(
request: &AnalysisRunRequest,
accepted: &AnalysisRunAccepted,
result_artifact_id: impl Into<String>,
completed_at: impl Into<String>,
summary: AnalysisResultSummary,
artifact: ScientificAcceptanceArtifact,
) -> Result<Self, ApiError> {
if request.output_profile != SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE {
return Err(ApiError::InvalidWirePayload);
}
let digest = artifact.sha256()?;
let value = Self {
contract_version: ANALYSIS_RESULT_CONTRACT_VERSION,
run_id: accepted.run_id.clone(),
run_state: AnalysisRunTerminalState::Succeeded,
idempotency_key: request.idempotency_key.clone(),
tenant_workspace_id: request.tenant_workspace_id.clone(),
snapshot_id: request.snapshot_id.clone(),
knowledge_cutoff: request.knowledge_cutoff.clone(),
model_contract_version: request.model_contract_version.clone(),
output_profile: request.output_profile.clone(),
result_artifact_id: Some(result_artifact_id.into()),
result_sha256: Some(digest),
result_schema_version: Some(SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION.into()),
completed_at: completed_at.into(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Completion can predate evidence cutoff

succeeded_scientific_acceptance accepts completed_at earlier than the knowledge cutoff. The result can include evidence unavailable when it supposedly completed.

Prompt for agents
AnalysisRunTerminalResult::succeeded_scientific_acceptance and terminal-result validation parse completed_at and knowledge_cutoff independently but do not enforce temporal ordering. Reject terminal results whose system completion instant precedes their knowledge cutoff, so the eligible evidence horizon cannot extend beyond completion. Apply the invariant in shared terminal validation so parsed JSON and both succeeded constructors behave consistently, and add a test with completed_at before knowledge_cutoff.
Devin Review

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

summary: Some(summary),
failure_code: None,
scientific_acceptance: Some(artifact),
};
value.validate()?;
require_terminal_binding(request, accepted, &value)?;
Expand Down Expand Up @@ -187,6 +241,7 @@ impl AnalysisRunTerminalResult {
completed_at: completed_at.into(),
summary: None,
failure_code: Some(failure_code.into()),
scientific_acceptance: None,
};
value.validate()?;
require_terminal_binding(request, accepted, &value)?;
Expand Down Expand Up @@ -270,14 +325,40 @@ impl AnalysisRunTerminalResult {
if self.failure_code.is_some() {
return Err(ApiError::InvalidWirePayload);
}
Ok(())
self.validate_scientific_acceptance(digest, schema)
}

fn validate_scientific_acceptance(&self, digest: &str, schema: &str) -> Result<(), ApiError> {
match (
self.output_profile.as_str() == SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE,
self.scientific_acceptance.as_ref(),
) {
(false, None) => Ok(()),
(false, Some(_)) | (true, None) => Err(ApiError::InvalidWirePayload),
(true, Some(artifact)) => {
artifact.validate()?;
if schema != SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION
|| artifact.schema_version != SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION
|| artifact.run_id != self.run_id
|| artifact.snapshot_id != self.snapshot_id
|| artifact.knowledge_cutoff != self.knowledge_cutoff
|| artifact.model != self.model_contract_version
|| artifact.output_profile != self.output_profile
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
|| artifact.sha256()?.as_str() != digest

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: Nested artifact limit remains enforced

Terminal validation hashes through the artifact’s bounded serializer. Construction, parsing, and serialization therefore retain the independent 16 KiB artifact limit.

Devin Review

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

{
return Err(ApiError::InvalidWirePayload);
}
Ok(())
}
}
}

fn validate_failed(&self) -> Result<(), ApiError> {
if self.result_artifact_id.is_some()
|| self.result_sha256.is_some()
|| self.result_schema_version.is_some()
|| self.summary.is_some()
|| self.scientific_acceptance.is_some()
{
return Err(ApiError::InvalidWirePayload);
}
Expand All @@ -289,6 +370,8 @@ impl AnalysisRunTerminalResult {
}
}

impl Eq for AnalysisRunTerminalResult {}

/// Return whether a terminal result exactly binds to its submitted request.
#[must_use]
pub fn terminal_result_matches_request(
Expand Down
8 changes: 7 additions & 1 deletion crates/tepp_api/src/analysis_run.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Versioned analysis-run request and accepted-run response contracts.

use crate::ApiError;
use crate::scientific_acceptance::refuse_metrics_on_receipt;
use crate::wire::{
from_json, require_byte_limit, require_contract_version, require_nonempty, to_json,
};
Expand Down Expand Up @@ -70,7 +71,8 @@ pub enum AnalysisRunStatusState {
}

/// Typed status/read response for an accepted analysis run.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[serde(deny_unknown_fields)]
pub struct AnalysisRunStatus {
/// Semantic contract version for this status payload family.
Expand Down Expand Up @@ -102,6 +104,7 @@ impl AnalysisRunRequest {
/// Returns wire, version, limit, or field-validation errors.
pub fn from_json_with_limit(payload: &str, maximum_bytes: usize) -> Result<Self, ApiError> {
require_byte_limit(payload, maximum_bytes)?;
refuse_metrics_on_receipt(payload)?;
let request: Self = from_json(payload)?;
request.validate()?;
Ok(request)
Expand Down Expand Up @@ -184,6 +187,7 @@ impl AnalysisRunAccepted {
/// Returns wire, version, limit, or field-validation errors.
pub fn from_json_with_limit(payload: &str, maximum_bytes: usize) -> Result<Self, ApiError> {
require_byte_limit(payload, maximum_bytes)?;
refuse_metrics_on_receipt(payload)?;
let accepted: Self = from_json(payload)?;
accepted.validate()?;
Ok(accepted)
Expand Down Expand Up @@ -337,6 +341,8 @@ impl AnalysisRunStatus {
}
}

impl Eq for AnalysisRunStatus {}

/// Compare two requests for idempotent-retry semantic equality.
#[must_use]
pub fn requests_are_idempotent_matches(
Expand Down
25 changes: 24 additions & 1 deletion crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
//! These pure wire contracts let TEPP operate standalone and as a modular CWL
//! component without sharing application tables. Domain estimation remains in
//! scientific crates; this crate only defines fail-closed interchange shapes.
//! Naruon and `LineageWeave` use the versioned analysis-run contract; `LineageWeave`
//! Analysis-run request and accepted receipts stay metric-free; only a succeeded
//! terminal result with profile `scientific_acceptance_v1` may carry
//! `tepp.scientific_acceptance.v1`. Naruon and `LineageWeave` use the versioned analysis-run contract; `LineageWeave`
//! may also request a cutoff-safe project-history projection from explicit
//! source evidence. Naruon owns the current purpose-bound export adapter.
//! Loopback listeners prove the HTTP boundary without claiming production TLS,
Expand All @@ -30,6 +32,7 @@ mod orchestration;
mod project_history;
mod project_journey;
mod provider_payload;
mod scientific_acceptance;
mod temporal_context;
mod wire;

Expand Down Expand Up @@ -95,6 +98,26 @@ pub use export::GraphMlExport;
pub use export::JsonLdExport;
/// Reproducibility manifest.
pub use export::ReproducibilityManifest;
/// Default maximum scientific-acceptance artifact size.
pub use scientific_acceptance::DEFAULT_SCIENTIFIC_ACCEPTANCE_BYTE_LIMIT;
/// Bound compute backend identity for scientific acceptance.
pub use scientific_acceptance::SCIENTIFIC_ACCEPTANCE_BACKEND;
/// CPU `f64` reference model identity for scientific acceptance.
pub use scientific_acceptance::SCIENTIFIC_ACCEPTANCE_MODEL;
/// Output profile that authorizes a scientific-acceptance terminal artifact.
pub use scientific_acceptance::SCIENTIFIC_ACCEPTANCE_OUTPUT_PROFILE;
/// Numeric precision bound into a scientific-acceptance artifact.
pub use scientific_acceptance::SCIENTIFIC_ACCEPTANCE_PRECISION;
/// Versioned scientific-acceptance artifact schema.
pub use scientific_acceptance::SCIENTIFIC_ACCEPTANCE_SCHEMA_VERSION;
/// Digest-bound scientific-acceptance artifact.
pub use scientific_acceptance::ScientificAcceptanceArtifact;
/// Nested recovery report carried by a scientific-acceptance artifact.
pub use scientific_acceptance::ScientificAcceptanceReport;
/// Detect scientific-metric keys on a receipt JSON object.
pub use scientific_acceptance::receipt_json_carries_scientific_metrics;
/// Refuse a request or accepted receipt that already carries metrics.
pub use scientific_acceptance::refuse_metrics_on_receipt;

/// Analytical export purpose.
pub use authorization::AnalyticalPurpose;
Expand Down
Loading
Loading