Skip to content
Draft
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]

- **Posterior topic-context analysis-run profile**: cutoff-safe `topic_context_posterior_v1` binds `TopicContextPosteriorArtifact` to an authoritative source/artifact digest manifest, rejects missing or post-cutoff document availability and unapproved producer contracts, derives the validated coordinate count, and refuses importance, collapsed draws, and invented birth/split/merge (`analysis_engine`). Not a Bayesian sampler and not implemented-main.

- `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
13 changes: 9 additions & 4 deletions crates/analysis_engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
//! through [`tepp_api`]. It deliberately does not claim latent-variable or topic
//! estimation authority; those estimators remain separate scientific crates.
//! estimation authority; it invokes estimators through their scientific crate
//! contracts and preserves their artifact meaning.
//! contracts and preserves their artifact meaning. Posterior topic-context
//! artifacts are validated through [`TopicContextPosteriorArtifact`] and do
//! not claim topic importance.

mod case_deletion_refit;
mod lineage_criterion;
Expand Down Expand Up @@ -48,9 +50,12 @@ pub use lineage_criterion::{
};
/// Bounded posterior topic-context producer contract and record types.
pub use topic_context_posterior::{
TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT, TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION,
TopicActivityInterval, TopicContextMembership, TopicContextPosteriorArtifact,
TopicDocumentRelation, TopicLineageEvent, TopicPostPlausibleValue,
TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT, TOPIC_CONTEXT_POSTERIOR_MODEL_CONTRACT_VERSION,
TOPIC_CONTEXT_POSTERIOR_OUTPUT_PROFILE, TOPIC_CONTEXT_POSTERIOR_PRODUCER_CONTRACT_VERSION,
TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION, TopicActivityInterval, TopicContextMembership,
TopicContextPosteriorArtifact, TopicContextPosteriorExecution,
TopicContextPosteriorSnapshotManifest, TopicDocumentRelation, TopicLineageEvent,
TopicPostPlausibleValue, execute_topic_context_posterior_run,
};
/// Topic-lineage artifact and execution contracts from this engine.
pub use topic_lineage_artifact::{
Expand Down
128 changes: 126 additions & 2 deletions crates/analysis_engine/src/topic_context_posterior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,23 @@ use sha2::{Digest, Sha256};
use temporal_core::KnowledgeCutoff;
use uuid::Uuid;

use crate::{AnalysisEngineError, format_digest, valid_identifier};
use tepp_api::{
AnalysisResultSummary, AnalysisRunAccepted, AnalysisRunRequest, AnalysisRunTerminalResult,
};

use crate::{AnalysisEngineError, format_digest, require_receipt_identity, valid_identifier};

/// Exact posterior artifact schema.
pub const TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION: &str = "tepp.topic_context_posterior.v1";
/// Maximum canonical JSON size.
pub const TOPIC_CONTEXT_POSTERIOR_BYTE_LIMIT: usize = 16 * 1024 * 1024;
/// Model contract required by the topic-context posterior analysis-run path.
pub const TOPIC_CONTEXT_POSTERIOR_MODEL_CONTRACT_VERSION: &str = "topic_context_posterior_v1";
/// Producer model contract accepted by the analysis-run profile.
pub const TOPIC_CONTEXT_POSTERIOR_PRODUCER_CONTRACT_VERSION: &str = "trsl-tm-v1";
/// Analysis-run output profile required for a topic-context posterior artifact.
pub const TOPIC_CONTEXT_POSTERIOR_OUTPUT_PROFILE: &str = "topic_context_posterior_v1";
const TOPIC_CONTEXT_POSTERIOR_INFERENCE_STATUS: &str = "posterior_topic_coordinates_not_importance";
const ENTRY_LIMIT: usize = 1_000_000;
const DIMENSIONS: [&str; 4] = ["business_unit", "process_unit", "team", "person"];
type PosteriorDraws = BTreeMap<Uuid, BTreeSet<u64>>;
Expand Down Expand Up @@ -150,6 +161,21 @@ pub struct TopicContextPosteriorArtifact {
pub inference_status: String,
}

/// Authoritative snapshot and cutoff-eligibility manifest for one artifact.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TopicContextPosteriorSnapshotManifest {
/// Immutable source snapshot identity.
pub snapshot_id: String,
/// Canonical digest of the resolved source snapshot bytes.
pub source_snapshot_sha256: String,
/// Historical cutoff applied while resolving eligibility.
pub knowledge_cutoff: String,
/// Canonical digest of the exact artifact admitted from this snapshot.
pub artifact_sha256: String,
/// Availability instant for every document represented by the artifact.
pub document_available_at: BTreeMap<String, String>,
}
Comment on lines +164 to +177

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Manifest lacks a wire contract

TopicContextPosteriorSnapshotManifest has no versioned serialization, parser, or byte limit. Define its standalone exchange contract before another service supplies this authoritative input.

Devin Review

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


fn digest(value: &str) -> bool {
value.len() == 64
&& value
Expand Down Expand Up @@ -217,7 +243,7 @@ impl TopicContextPosteriorArtifact {
],
entry_limit,
)
&& self.inference_status == "posterior_topic_coordinates_not_importance"
&& self.inference_status == TOPIC_CONTEXT_POSTERIOR_INFERENCE_STATUS
}

/// Parse and validate one bounded posterior artifact.
Expand Down Expand Up @@ -636,6 +662,104 @@ impl TopicContextPosteriorArtifact {
}
}

/// One completed topic-context posterior artifact and its terminal result.
#[derive(Clone, Debug, PartialEq)]
pub struct TopicContextPosteriorExecution {
/// Digest-bound producer posterior artifact.
pub artifact: TopicContextPosteriorArtifact,
/// Terminal result carrying the artifact identity, digest, and schema.
pub terminal_result: AnalysisRunTerminalResult,
}

/// Execute posterior topic-context validation as one analysis-run profile.
///
/// The executor validates an already-constructed
/// [`TopicContextPosteriorArtifact`] through its producer contract and does
/// not reimplement TRSL-TM fitting, collapse missing draws, infer topic
/// importance, or invent birth/split/merge events. Lineage events remain
/// producer-supplied. This is not a Bayesian sampler and not GPU execution.
///
/// # Errors
///
/// Returns a request/receipt/snapshot/cutoff/profile error or a producer
/// contract refusal.
pub fn execute_topic_context_posterior_run(
request: &AnalysisRunRequest,
accepted: &AnalysisRunAccepted,
manifest: &TopicContextPosteriorSnapshotManifest,
artifact: &TopicContextPosteriorArtifact,
completed_at: impl Into<String>,
Comment on lines +686 to +691

@devin-ai-integration devin-ai-integration Bot Sep 1, 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.

🔴 Late supporting evidence passes cutoff

When late membership, relation, or lineage evidence supports an eligible document, document_available_at checks only the document's availability. The historical run then accepts evidence unavailable at its cutoff.

Devin Review

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

) -> Result<TopicContextPosteriorExecution, AnalysisEngineError> {
request.to_json()?;
accepted.to_json()?;
require_receipt_identity(request, accepted)?;
if request.snapshot_id != manifest.snapshot_id || artifact.snapshot_id != manifest.snapshot_id {
return Err(AnalysisEngineError::SnapshotMismatch);
}
let knowledge_cutoff =
canonical_time(&manifest.knowledge_cutoff).ok_or(AnalysisEngineError::InvalidEvidence)?;
if request.knowledge_cutoff != manifest.knowledge_cutoff
|| artifact.knowledge_cutoff != manifest.knowledge_cutoff
|| artifact.source_snapshot_sha256 != manifest.source_snapshot_sha256
|| request.model_contract_version != TOPIC_CONTEXT_POSTERIOR_MODEL_CONTRACT_VERSION
|| request.output_profile != TOPIC_CONTEXT_POSTERIOR_OUTPUT_PROFILE
Comment on lines +704 to +705

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind the producer model contract into the run

When an otherwise valid artifact declares any nonempty artifact.model_contract_version (for example a different or unapproved producer), this condition checks only the request's fixed profile value and still succeeds. The emitted terminal result then reports topic_context_posterior_v1 as its model contract while omitting the artifact's actual producer contract, making incompatible model output indistinguishable in run metadata; validate the artifact contract against an approved/request-bound producer version.

AGENTS.md reference: AGENTS.md:L26-L26

Useful? React with 👍 / 👎.

|| artifact.model_contract_version != TOPIC_CONTEXT_POSTERIOR_PRODUCER_CONTRACT_VERSION
|| artifact.run_id != accepted.run_id
|| artifact.inference_status != TOPIC_CONTEXT_POSTERIOR_INFERENCE_STATUS
|| !digest(&manifest.source_snapshot_sha256)
|| !digest(&manifest.artifact_sha256)
{
return Err(AnalysisEngineError::InvalidEvidence);
}

let digest = artifact.sha256()?;
if digest != manifest.artifact_sha256 {
return Err(AnalysisEngineError::InvalidEvidence);
}
let mut document_ids = BTreeSet::new();
for value in &artifact.plausible_values {
document_ids.insert(value.document_id.clone());
}
if document_ids.len() != manifest.document_available_at.len()
|| document_ids.iter().any(|document_id| {
manifest
.document_available_at
.get(document_id)
.and_then(|available_at| canonical_time(available_at))
.is_none_or(|available_at| available_at > knowledge_cutoff)
})
{
return Err(AnalysisEngineError::InvalidEvidence);
}
// Artifact validation bounds the canonical payload to 16 MiB, so both
// counts are far below the public summary limit and fit in u64.
let document_count = document_ids.len() as u64;
let statistic_count = artifact
.plausible_values
.iter()
.map(|value| value.logistic_normal_coordinates.len() as u64)
.sum();
let summary = AnalysisResultSummary {
analysis_family: "topic_context_posterior".into(),
evidence_count: document_count,
statistic_count,
validation_status: TOPIC_CONTEXT_POSTERIOR_INFERENCE_STATUS.into(),
};
let terminal_result = AnalysisRunTerminalResult::succeeded(
request,
accepted,
format!("topic_context_posterior_artifact_{}", &digest[..16]),
digest,
TOPIC_CONTEXT_POSTERIOR_SCHEMA_VERSION,
completed_at,
summary,
)?;
Ok(TopicContextPosteriorExecution {
artifact: artifact.clone(),
terminal_result,
})
}

#[cfg(test)]
mod tests {
use super::AnalysisEngineError;
Expand Down
Loading
Loading