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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +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.
- **TDT/CHRONOS durable result contract**: canonical typed JSON and deterministic GraphML now export the complete bounded Allen reasoner scope, historical knowledge cutoff, observed/derived status, and conservative accepted-assertion support; canonical payload digest and `tdt_chronos_interval_consistency_v2` 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
6 changes: 6 additions & 0 deletions crates/event_core/src/interval_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ impl IntervalConsistencyNetwork {
.map_err(|error| map_reasoner_error(&error))
}

/// Number of event-interval variables currently in this network.
#[must_use]
pub fn variable_count(&self) -> usize {
self.reasoner.variable_count()
}

/// Assert a nonempty qualitative relation set for an ordered pair.
///
/// Use this when temporal extents are unknown and only Allen vocabulary is
Expand Down
53 changes: 36 additions & 17 deletions crates/event_core/src/interval_consistency_artifact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::fmt::Write as _;
use temporal_core::{AllenRelation, RelationSet, TemporalVariableId};
use temporal_core::{AllenRelation, KnowledgeCutoff, 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";
pub const INTERVAL_CONSISTENCY_ARTIFACT_TYPE: &str = "tdt_chronos_interval_consistency_v2";
const SCHEMA_VERSION: &str = "tepp.tdt_chronos_interval_consistency.v2";
const MAX_RELATIONS: usize = 100_000;
const MAX_JSON_BYTES: usize = 4 * 1024 * 1024;

Expand Down Expand Up @@ -41,6 +41,10 @@ pub struct IntervalConsistencyArtifact {
pub snapshot_id: String,
/// Lowercase SHA-256 of the exact admitted input bytes.
pub input_digest_sha256: String,
/// Historical evidence-availability cutoff applied to the admitted input.
pub knowledge_cutoff: String,
/// Complete stable inventory of event identities in the reasoner network.
pub event_ids: Vec<String>,
/// Non-causal observed and closure-derived temporal relations.
pub relations: Vec<IntervalConsistencyArtifactRelation>,
}
Expand All @@ -57,17 +61,23 @@ impl IntervalConsistencyArtifact {
run_id: impl Into<String>,
snapshot_id: impl Into<String>,
input_digest_sha256: impl Into<String>,
knowledge_cutoff: 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))
let mut variable_ids = BTreeSet::new();
if variables.len() != network.variable_count()
|| variables.len() < 2
|| variables.iter().any(|(identity, variable)| {
identity.trim().is_empty()
|| !identities.insert(identity)
|| !variable_ids.insert(variable)
})
{
return Err(EventError::InvalidWirePayload);
}
Comment thread
seonghobae marked this conversation as resolved.
let event_ids = identities.into_iter().cloned().collect();
let mut ordered_variables = variables.iter().collect::<Vec<_>>();
ordered_variables.sort_by(|left, right| left.0.cmp(&right.0));
let mut relations = Vec::new();
Expand All @@ -84,7 +94,7 @@ impl IntervalConsistencyArtifact {
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(),
observed: derived.is_observed() | inverse.is_observed(),
Comment thread
seonghobae marked this conversation as resolved.
support_assertion_ordinals: derived
.support()
.iter()
Expand All @@ -102,6 +112,8 @@ impl IntervalConsistencyArtifact {
run_id: run_id.into(),
snapshot_id: snapshot_id.into(),
input_digest_sha256: input_digest_sha256.into(),
knowledge_cutoff: knowledge_cutoff.into(),
event_ids,
relations,
};
artifact.validate()?;
Expand Down Expand Up @@ -157,25 +169,21 @@ impl IntervalConsistencyArtifact {
/// 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=\"",
"<?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=\"knowledge_cutoff\" for=\"graph\" attr.name=\"knowledge_cutoff\" 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>",
"<data key=\"schema\">{}</data><data key=\"snapshot\">{}</data><data key=\"input_digest\">{}</data><data key=\"knowledge_cutoff\">{}</data>",
xml_escape(&self.schema_version),
xml_escape(&self.snapshot_id),
self.input_digest_sha256
self.input_digest_sha256,
xml_escape(&self.knowledge_cutoff)
)
.expect("writing to String cannot fail");
for node in nodes {
for node in &self.event_ids {
output.push_str("<node id=\"");
output.push_str(&xml_escape(node));
output.push_str("\"/>\n");
Expand All @@ -192,6 +200,9 @@ impl IntervalConsistencyArtifact {
|| self.run_id.trim().is_empty()
|| self.snapshot_id.trim().is_empty()
|| !valid_digest(&self.input_digest_sha256)
|| KnowledgeCutoff::parse_rfc3339(&self.knowledge_cutoff).is_err()
Comment thread
seonghobae marked this conversation as resolved.
|| self.event_ids.len() < 2
|| !strictly_increasing(&self.event_ids)
|| self.relations.is_empty()
|| self.relations.len() > MAX_RELATIONS
{
Expand All @@ -209,6 +220,14 @@ impl IntervalConsistencyArtifact {
|| !strictly_increasing(&relation.allen_relations)
|| !strictly_increasing(&relation.support_assertion_ordinals)
|| previous.is_some_and(|old| old >= key)
|| self
.event_ids
.binary_search(&relation.left_event_id)
.is_err()
|| self
.event_ids
.binary_search(&relation.right_event_id)
.is_err()
{
return Err(EventError::InvalidWirePayload);
}
Expand Down
109 changes: 107 additions & 2 deletions crates/event_core/tests/interval_consistency_artifact_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use event_core::{
use temporal_core::{AllenRelation, RelationSet};

const DIGEST: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const CUTOFF: &str = "2026-08-28T00:00:00Z";

fn artifact() -> IntervalConsistencyArtifact {
let mut network = IntervalConsistencyNetwork::with_limits(3, 3, 128).expect("limits");
Expand All @@ -24,6 +25,7 @@ fn artifact() -> IntervalConsistencyArtifact {
"run<&1",
"snapshot-1",
DIGEST,
CUTOFF,
&network,
&[
("event-1<&".to_owned(), first),
Expand All @@ -39,8 +41,10 @@ fn derived_branch_is_durable_typed_and_provenance_bearing() {
let artifact = artifact();
assert_eq!(
INTERVAL_CONSISTENCY_ARTIFACT_TYPE,
"tdt_chronos_interval_consistency_v1"
"tdt_chronos_interval_consistency_v2"
);
assert_eq!(artifact.knowledge_cutoff, CUTOFF);
assert_eq!(artifact.event_ids, ["event-1<&", "event-2", "event-3"]);
assert_eq!(artifact.relations.len(), 3);
let derived = artifact
.relations
Expand All @@ -62,6 +66,7 @@ fn derived_branch_is_durable_typed_and_provenance_bearing() {
let graphml = artifact.to_graphml().expect("graphml");
assert!(graphml.contains("allen_relations"));
assert!(graphml.contains(&format!("<data key=\"input_digest\">{DIGEST}</data>")));
assert!(graphml.contains(&format!("<data key=\"knowledge_cutoff\">{CUTOFF}</data>")));
assert!(graphml.contains("<data key=\"observed\">false</data>"));
assert!(graphml.contains("<data key=\"support\">0,1</data>"));
assert!(graphml.contains("event-1&lt;&amp;"));
Expand All @@ -79,6 +84,7 @@ fn reverse_oriented_assertion_is_observed_in_stable_export_order() {
"run-reverse",
"snapshot-reverse",
DIGEST,
CUTOFF,
&network,
&[
("event-left".to_owned(), left),
Expand All @@ -94,6 +100,75 @@ fn reverse_oriented_assertion_is_observed_in_stable_export_order() {
);
}

#[test]
fn complete_variable_inventory_preserves_isolated_events() {
let mut singleton = IntervalConsistencyNetwork::with_limits(1, 1, 1).expect("limits");
let only = singleton.add_variable().expect("only");
assert_eq!(
IntervalConsistencyArtifact::from_network(
"run",
"snapshot",
DIGEST,
CUTOFF,
&singleton,
&[("event-only".into(), only)]
),
Err(EventError::InvalidWirePayload)
);
let mut network = IntervalConsistencyNetwork::with_limits(3, 1, 8).expect("limits");
let first = network.add_variable().expect("first");
let second = network.add_variable().expect("second");
let isolated = network.add_variable().expect("isolated");
network
.assert_qualitative_relations(first, second, RelationSet::singleton(AllenRelation::Before))
.expect("assertion");
assert_eq!(
IntervalConsistencyArtifact::from_network(
"run",
"snapshot",
DIGEST,
CUTOFF,
&network,
&[("event-1".into(), first), ("event-2".into(), second)]
),
Err(EventError::InvalidWirePayload)
);
assert_eq!(
IntervalConsistencyArtifact::from_network(
"run",
"snapshot",
DIGEST,
CUTOFF,
&network,
&[
("event-1".into(), first),
("event-2".into(), first),
("event-3".into(), isolated),
]
),
Err(EventError::InvalidWirePayload)
);
let artifact = IntervalConsistencyArtifact::from_network(
"run",
"snapshot",
DIGEST,
CUTOFF,
&network,
&[
("event-1".into(), first),
("event-2".into(), second),
("event-isolated".into(), isolated),
],
)
.expect("complete artifact");
assert!(
artifact
.to_graphml()
.expect("graphml")
.contains("event-isolated")
);
}

#[test]
fn caller_variable_order_does_not_change_canonical_artifact() {
let expected = artifact();
Expand All @@ -112,6 +187,7 @@ fn caller_variable_order_does_not_change_canonical_artifact() {
"run<&1",
"snapshot-1",
DIGEST,
CUTOFF,
&network,
&[
("event-3".to_owned(), third),
Expand Down Expand Up @@ -139,6 +215,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"run",
"snapshot",
"not-a-digest",
CUTOFF,
&network,
&[("same".to_owned(), first), ("same".to_owned(), second)]
),
Expand All @@ -149,6 +226,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"run",
"snapshot",
DIGEST,
CUTOFF,
&network,
&[("event-1".to_owned(), first)]
),
Expand All @@ -159,6 +237,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"run",
"snapshot",
DIGEST,
CUTOFF,
&network,
&[(String::new(), first), ("event-2".to_owned(), second)]
),
Expand All @@ -182,6 +261,12 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
invalid_binding.to_json(),
Err(EventError::InvalidWirePayload)
);
let mut invalid_cutoff = valid.clone();
invalid_cutoff.knowledge_cutoff = "not-a-time".to_owned();
assert_eq!(
invalid_cutoff.to_json(),
Err(EventError::InvalidWirePayload)
);
let mut invalid_relation = valid.clone();
invalid_relation.relations[0]
.support_assertion_ordinals
Expand All @@ -206,6 +291,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"x".repeat(4 * 1024 * 1024),
"snapshot",
DIGEST,
CUTOFF,
&network,
&[
("event-1".to_owned(), first),
Expand All @@ -219,7 +305,9 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
Err(EventError::InvalidWirePayload)
);

let unconstrained = IntervalConsistencyNetwork::with_limits(2, 1, 8).expect("limits");
let mut unconstrained = IntervalConsistencyNetwork::with_limits(2, 1, 8).expect("limits");
unconstrained.add_variable().expect("local left");
unconstrained.add_variable().expect("local right");
let mut foreign = IntervalConsistencyNetwork::with_limits(2, 1, 8).expect("foreign");
let foreign_left = foreign.add_variable().expect("foreign left");
let foreign_right = foreign.add_variable().expect("foreign right");
Expand All @@ -228,6 +316,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"run",
"snapshot",
DIGEST,
CUTOFF,
&unconstrained,
&[
("event-1".to_owned(), foreign_left),
Expand All @@ -245,6 +334,7 @@ fn artifact_rejects_unbound_or_noncanonical_payloads() {
"run",
"snapshot",
DIGEST,
CUTOFF,
&quiet,
&[
("event-1".to_owned(), quiet_left),
Expand All @@ -269,6 +359,21 @@ fn invalid_artifact_variants(
invalid.snapshot_id.clear();
variants.push(invalid);
let mut invalid = valid.clone();
invalid.event_ids.clear();
variants.push(invalid);
let mut invalid = valid.clone();
invalid.event_ids.swap(0, 1);
variants.push(invalid);
let mut invalid = valid.clone();
invalid.event_ids.remove(0);
variants.push(invalid);
let mut invalid = valid.clone();
let missing_right = invalid.relations[0].right_event_id.clone();
invalid
.event_ids
.retain(|identity| identity != &missing_right);
variants.push(invalid);
let mut invalid = valid.clone();
invalid.relations = vec![valid.relations[0].clone(); 100_001];
variants.push(invalid);
let mut invalid = valid.clone();
Expand Down
1 change: 1 addition & 0 deletions crates/persistence_postgres/src/model_run_sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ mod tests {
"run-1",
"snapshot-1",
"ab".repeat(32),
"2026-08-28T00:00:00Z",
&network,
&[("event-1".into(), left), ("event-2".into(), right)],
)
Expand Down
6 changes: 6 additions & 0 deletions crates/temporal_core/src/reasoner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ impl TemporalReasoner {
}
}

/// Number of variables currently owned by this reasoner.
#[must_use]
pub fn variable_count(&self) -> usize {
self.cells.len()
}

/// Add one interval variable.
///
/// # Errors
Expand Down
Loading