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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- **TDT/CHRONOS durable result contract**: canonical typed JSON and deterministic GraphML now export the actual bounded Allen reasoner result, observed/derived status, and conservative accepted-assertion support; canonical payload digest and `tdt_chronos_interval_consistency_v1` type bind the immutable bytes into ADR 0013's append-only `model_artifact` chain.
- **Analysis engine**: deterministic end-to-end analysis-run execution with cutoff-safe eligibility, immutable evidence binding, and reproducibility manifests (`analysis_engine` crate).
- **Restore Driver p.16 `MANIFESTVARstd`**: `recover_standardised_manifest_variance` maps `θ / θ = 1` with strictly positive `MANIFESTVAR`, refusing unstandardised manifest-variance, `MANIFESTTRAITVARstd`, and Equation 5 `Var(y)` substitutions (`psychometric_core`).
- **Posterior network estimator**: cross-draw Pearson correlations in ILR space, jackknife SE and CI, Benjamini–Hochberg FDR edge admission, nonparametric bootstrap stability, greedy modularity consensus clustering (`network_analysis` crate).
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/event_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ publish = false
evidence_core = { path = "../evidence_core", version = "0.2.0" }
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
temporal_core = { path = "../temporal_core", version = "0.2.0" }
uuid.workspace = true

Expand Down
21 changes: 18 additions & 3 deletions crates/event_core/src/interval_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

use crate::{EventError, EventInstanceId};
use temporal_core::{
AllenRelation, ClosureReport, ConstraintId, EventTime, RelationSet, TemporalInterval,
TemporalReasoner, TemporalReasonerError, TemporalReasonerLimits, TemporalVariableId,
classify_interval_relation,
AllenRelation, ClosureReport, ConstraintId, DerivedRelation, EventTime, RelationSet,
TemporalInterval, TemporalReasoner, TemporalReasonerError, TemporalReasonerLimits,
TemporalVariableId, classify_interval_relation,
};

/// Summary of one successful bounded interval-consistency closure.
Expand Down Expand Up @@ -166,6 +166,21 @@ impl IntervalConsistencyNetwork {
.map(|derived| derived.relations())
.map_err(|error| map_reasoner_error(&error))
}

/// Return the current relation with observation and support provenance.
///
/// # Errors
///
/// Returns an unknown-variable error for identifiers outside this network.
pub fn derived_relation(
&self,
left: TemporalVariableId,
right: TemporalVariableId,
) -> Result<DerivedRelation, EventError> {
self.reasoner
.relation(left, right)
.map_err(|error| map_reasoner_error(&error))
}
}

/// Explicit refusal to treat bounded path consistency as unrestricted SAT.
Expand Down
263 changes: 263 additions & 0 deletions crates/event_core/src/interval_consistency_artifact.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
//! Durable, digest-bound export of bounded interval-consistency results.

use crate::{EventError, IntervalConsistencyNetwork};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::fmt::Write as _;
use temporal_core::{AllenRelation, RelationSet, TemporalVariableId};

/// Model-artifact type used by the ADR-0013 persistence chain.
pub const INTERVAL_CONSISTENCY_ARTIFACT_TYPE: &str = "tdt_chronos_interval_consistency_v1";
const SCHEMA_VERSION: &str = "tepp.tdt_chronos_interval_consistency.v1";
const MAX_RELATIONS: usize = 100_000;
const MAX_JSON_BYTES: usize = 4 * 1024 * 1024;

/// One observed or closure-derived ordered interval relation.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IntervalConsistencyArtifactRelation {
/// Opaque source event identity for the left interval.
pub left_event_id: String,
/// Opaque source event identity for the right interval.
pub right_event_id: String,
/// Remaining Allen relations in stable reasoner order.
pub allen_relations: Vec<AllenRelation>,
/// Whether either orientation of this interval pair has a direct accepted assertion.
pub observed: bool,
/// Accepted-assertion ordinals conservatively supporting this result.
pub support_assertion_ordinals: Vec<usize>,
}

/// Versioned bounded reasoner result suitable for immutable artifact storage.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IntervalConsistencyArtifact {
/// Exact typed schema identity.
pub schema_version: String,
/// Opaque analysis-run identity.
pub run_id: String,
/// Immutable source snapshot identity.
pub snapshot_id: String,
/// Lowercase SHA-256 of the exact admitted input bytes.
pub input_digest_sha256: String,
/// Non-causal observed and closure-derived temporal relations.
pub relations: Vec<IntervalConsistencyArtifactRelation>,
}

impl IntervalConsistencyArtifact {
/// Project a closed network into a canonical artifact.
///
/// Universal unconstrained pairs and identity pairs are omitted.
///
/// # Errors
///
/// Returns a fail-closed wire or reasoner error for invalid bindings.
pub fn from_network(
run_id: impl Into<String>,
snapshot_id: impl Into<String>,
input_digest_sha256: impl Into<String>,
network: &IntervalConsistencyNetwork,
variables: &[(String, TemporalVariableId)],
) -> Result<Self, EventError> {
let mut identities = BTreeSet::new();
if variables.len() < 2
|| variables
.iter()
.any(|(identity, _)| identity.trim().is_empty() || !identities.insert(identity))
{
return Err(EventError::InvalidWirePayload);
}
Comment thread
seonghobae marked this conversation as resolved.
let mut ordered_variables = variables.iter().collect::<Vec<_>>();
ordered_variables.sort_by(|left, right| left.0.cmp(&right.0));
let mut relations = Vec::new();
for (left_index, (left_identity, left)) in ordered_variables.iter().enumerate() {
for (right_identity, right) in ordered_variables.iter().skip(left_index + 1) {
let derived = network.derived_relation(*left, *right)?;
if derived.relations() == RelationSet::all() {
continue;
}
let inverse = network.derived_relation(*right, *left)?;
relations.push(IntervalConsistencyArtifactRelation {
left_event_id: left_identity.clone(),
right_event_id: right_identity.clone(),
allen_relations: derived.relations().iter().collect(),
// Observation is orientation-independent even though the
// export retains the stable variable ordering.
observed: derived.is_observed() || inverse.is_observed(),
support_assertion_ordinals: derived
.support()
.iter()
.map(|identifier| identifier.assertion_ordinal())
.collect(),
});
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
}
}
relations.sort_by(|left, right| {
(&left.left_event_id, &left.right_event_id)
.cmp(&(&right.left_event_id, &right.right_event_id))
});
let artifact = Self {
schema_version: SCHEMA_VERSION.to_owned(),
run_id: run_id.into(),
snapshot_id: snapshot_id.into(),
input_digest_sha256: input_digest_sha256.into(),
relations,
};
artifact.validate()?;
let _canonical_json = artifact.to_json()?;
Ok(artifact)
Comment thread
seonghobae marked this conversation as resolved.
}

/// Parse and validate canonical JSON.
///
/// # Errors
///
/// Returns [`EventError::InvalidWirePayload`] for malformed input.
pub fn from_json(payload: &str) -> Result<Self, EventError> {
if payload.len() > MAX_JSON_BYTES {
return Err(EventError::InvalidWirePayload);
}
let artifact: Self =
serde_json::from_str(payload).map_err(|_| EventError::InvalidWirePayload)?;
artifact.validate()?;
if artifact.to_json()? != payload {
return Err(EventError::InvalidWirePayload);
}
Ok(artifact)
}
Comment thread
seonghobae marked this conversation as resolved.

/// Serialize canonical validated JSON.
///
/// # Errors
///
/// Returns a wire error when fields or size are invalid.
pub fn to_json(&self) -> Result<String, EventError> {
self.validate()?;
let payload = serde_json::to_string(self).map_err(|_| EventError::InvalidWirePayload)?;
if payload.len() > MAX_JSON_BYTES {
return Err(EventError::InvalidWirePayload);
}
Ok(payload)
}

/// Return the lowercase SHA-256 of canonical JSON bytes.
///
/// # Errors
///
/// Returns a wire error when the artifact is invalid.
pub fn sha256(&self) -> Result<String, EventError> {
Ok(format!("{:x}", Sha256::digest(self.to_json()?.as_bytes())))
}

/// Render typed `GraphML` with observation and support provenance.
///
/// # Errors
///
/// Returns a wire error when the artifact is invalid.
pub fn to_graphml(&self) -> Result<String, EventError> {
self.validate()?;
let mut nodes = BTreeSet::new();
for relation in &self.relations {
nodes.insert(&relation.left_event_id);
nodes.insert(&relation.right_event_id);
}
let mut output = String::from(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\">\n<key id=\"schema\" for=\"graph\" attr.name=\"schema_version\" attr.type=\"string\"/>\n<key id=\"snapshot\" for=\"graph\" attr.name=\"snapshot_id\" attr.type=\"string\"/>\n<key id=\"input_digest\" for=\"graph\" attr.name=\"input_digest_sha256\" attr.type=\"string\"/>\n<key id=\"relations\" for=\"edge\" attr.name=\"allen_relations\" attr.type=\"string\"/>\n<key id=\"observed\" for=\"edge\" attr.name=\"observed\" attr.type=\"boolean\"/>\n<key id=\"support\" for=\"edge\" attr.name=\"support_assertion_ordinals\" attr.type=\"string\"/>\n<graph id=\"",
);
output.push_str(&xml_escape(&self.run_id));
output.push_str("\" edgedefault=\"directed\">\n");
writeln!(
output,
"<data key=\"schema\">{}</data><data key=\"snapshot\">{}</data><data key=\"input_digest\">{}</data>",
xml_escape(&self.schema_version),
xml_escape(&self.snapshot_id),
self.input_digest_sha256
)
.expect("writing to String cannot fail");
for node in nodes {
output.push_str("<node id=\"");
output.push_str(&xml_escape(node));
output.push_str("\"/>\n");
}
for (index, relation) in self.relations.iter().enumerate() {
append_edge(&mut output, index, relation);
}
output.push_str("</graph>\n</graphml>\n");
Ok(output)
}

fn validate(&self) -> Result<(), EventError> {
if self.schema_version != SCHEMA_VERSION
|| self.run_id.trim().is_empty()
|| self.snapshot_id.trim().is_empty()
|| !valid_digest(&self.input_digest_sha256)
|| self.relations.is_empty()
|| self.relations.len() > MAX_RELATIONS
{
return Err(EventError::InvalidWirePayload);
}
let mut previous = None;
for relation in &self.relations {
let key = (&relation.left_event_id, &relation.right_event_id);
if relation.left_event_id.trim().is_empty()
|| relation.right_event_id.trim().is_empty()
|| relation.left_event_id == relation.right_event_id
|| relation.allen_relations.is_empty()
|| relation.allen_relations.len() == AllenRelation::ALL.len()
|| relation.support_assertion_ordinals.is_empty()
|| !strictly_increasing(&relation.allen_relations)
|| !strictly_increasing(&relation.support_assertion_ordinals)
|| previous.is_some_and(|old| old >= key)
{
return Err(EventError::InvalidWirePayload);
}
previous = Some(key);
}
Ok(())
}
Comment thread
seonghobae marked this conversation as resolved.
}

fn append_edge(output: &mut String, index: usize, relation: &IntervalConsistencyArtifactRelation) {
let kinds = relation
.allen_relations
.iter()
.map(|value| serde_json::to_string(value).expect("Allen relation serialization"))
.map(|value| value.trim_matches('"').to_owned())
.collect::<Vec<_>>()
.join(",");
let support = relation
.support_assertion_ordinals
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",");
writeln!(
output,
"<edge id=\"e{index}\" source=\"{}\" target=\"{}\"><data key=\"relations\">{}</data><data key=\"observed\">{}</data><data key=\"support\">{support}</data></edge>",
xml_escape(&relation.left_event_id),
xml_escape(&relation.right_event_id),
xml_escape(&kinds),
relation.observed
)
.expect("writing to String cannot fail");
}

fn strictly_increasing<T: Ord>(values: &[T]) -> bool {
values.windows(2).all(|pair| pair[0] < pair[1])
}

fn valid_digest(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}

fn xml_escape(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
6 changes: 6 additions & 0 deletions crates/event_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod identifier;
mod instance;
mod intelligence;
mod interval_consistency;
mod interval_consistency_artifact;
mod link;
mod mention;
mod prediction;
Expand Down Expand Up @@ -113,6 +114,11 @@ pub use interval_consistency::IntervalConsistencyReport;
pub use interval_consistency::refuse_interval_consistency_as_unrestricted_satisfiability;
/// Explicit refusal to promote an interval contradiction into an instance.
pub use interval_consistency::refuse_interval_contradiction_as_instance;
/// Durable JSON and `GraphML` projection of one bounded consistency result.
pub use interval_consistency_artifact::{
INTERVAL_CONSISTENCY_ARTIFACT_TYPE, IntervalConsistencyArtifact,
IntervalConsistencyArtifactRelation,
};
/// TDT same-event versus distinct-event link label.
pub use link::EventLinkLabel;
/// Undirected TDT link hypothesis between two mentions.
Expand Down
Loading