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
5 changes: 5 additions & 0 deletions CHANGELOG.d/lineage-criterion-anchor-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Added

- Publish the strict `tepp.lineage_criterion_anchor.v1` artifact contract so
LineageWeave can activate fast-mlsirm channel weights only after an exact,
TEPP-authored independent criterion-validity result.
15 changes: 15 additions & 0 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod corpus_split_manifest;
mod envelope;
mod error;
mod export;
mod lineage_criterion_anchor;
mod lineageweave_http;
mod live_http;
mod naruon_http;
Expand Down Expand Up @@ -94,6 +95,20 @@ pub use authorization::ExportAuthorizationRequest;
pub use authorization::authorize_export;
/// Fail closed when an export decision is denied.
pub use authorization::require_export_allowed;
/// TEPP-owned criterion-validity outcome for one Event Lineage weight run.
pub use lineage_criterion_anchor::CriterionValidityStatus;
/// Default maximum lineage criterion-anchor artifact size.
pub use lineage_criterion_anchor::DEFAULT_LINEAGE_CRITERION_ANCHOR_BYTE_LIMIT;
/// Semantic version of the lineage criterion-anchor artifact.
pub use lineage_criterion_anchor::LINEAGE_CRITERION_ANCHOR_CONTRACT_VERSION;
/// Analysis-run model contract that requests a lineage criterion anchor.
pub use lineage_criterion_anchor::LINEAGE_CRITERION_MODEL_CONTRACT;
/// Analysis-run output profile that requests a lineage criterion anchor.
pub use lineage_criterion_anchor::LINEAGE_CRITERION_OUTPUT_PROFILE;
/// Terminal result-schema identity for a lineage criterion anchor.
pub use lineage_criterion_anchor::LINEAGE_CRITERION_RESULT_SCHEMA;
/// Versioned TEPP criterion-validity artifact for one Event Lineage weight run.
pub use lineage_criterion_anchor::LineageCriterionAnchor;
/// Published `LineageWeave` modular-consumer identity.
pub use lineageweave_http::LINEAGEWEAVE_CONSUMER_CODE;
/// Published Naruon modular-consumer identity.
Expand Down
233 changes: 233 additions & 0 deletions crates/tepp_api/src/lineage_criterion_anchor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
//! Versioned TEPP criterion-validity artifact for Event Lineage channel weights.
//!
//! TEPP is the authority that evaluates an independently supplied lineage
//! criterion. This DTO only transports TEPP's decision and exact provenance;
//! it does not let a consumer calculate or reinterpret validity locally.

use serde::{Deserialize, Serialize};
use temporal_core::KnowledgeCutoff;

use crate::ApiError;
use crate::wire::{
from_json, require_byte_limit, require_contract_version, require_nonempty, to_json_with_limit,
};

/// Semantic contract version for lineage criterion anchors.
pub const LINEAGE_CRITERION_ANCHOR_CONTRACT_VERSION: u16 = 1;

/// Analysis-run model contract requested by a `LineageWeave` consumer.
pub const LINEAGE_CRITERION_MODEL_CONTRACT: &str = "tepp-lineage-criterion-v1";

/// Analysis-run output profile that produces this artifact.
pub const LINEAGE_CRITERION_OUTPUT_PROFILE: &str = "lineage_pair_criterion_anchor";

/// Result-schema identity carried by the terminal analysis result.
pub const LINEAGE_CRITERION_RESULT_SCHEMA: &str = "tepp.lineage_criterion_anchor.v1";

/// Default maximum serialized anchor artifact size.
pub const DEFAULT_LINEAGE_CRITERION_ANCHOR_BYTE_LIMIT: usize = 16 * 1024;

/// TEPP-owned criterion-validity outcome.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CriterionValidityStatus {
/// The proposed channel-weight run passed TEPP's registered criterion validation.
Accepted,
/// The proposed channel-weight run failed TEPP's registered criterion validation.
Rejected,
}

/// Exact, identity-bound TEPP criterion-validity result for one weight run.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct LineageCriterionAnchor {
/// Semantic contract version, always one for this shape.
pub contract_version: u16,
/// Artifact kind, always `lineage_pair_criterion`.
pub anchor_kind_code: String,
/// Opaque consumer-owned fast-mlsirm estimation-run identity.
pub estimation_run_id: String,
/// Immutable source snapshot SHA-256 shared with the proposed weights.
pub source_snapshot_sha256: String,
/// Exact RFC 3339 knowledge cutoff shared with the proposed weights.
pub knowledge_cutoff: String,
/// TEPP-owned criterion-validity outcome.
pub criterion_validity_status: CriterionValidityStatus,
/// Number of pair outcomes that entered the independent validation.
pub validated_pair_count: u64,
}

impl LineageCriterionAnchor {
/// Parse and validate an anchor with the default payload limit.
///
/// # Errors
///
/// Returns a fail-closed wire, version, identity, digest, time, or count error.
pub fn from_json(payload: &str) -> Result<Self, ApiError> {
Self::from_json_with_limit(payload, DEFAULT_LINEAGE_CRITERION_ANCHOR_BYTE_LIMIT)
}

/// Parse and validate an anchor with a caller-supplied payload limit.
///
/// # Errors
///
/// Returns a fail-closed wire, version, identity, digest, time, or count error.
pub fn from_json_with_limit(payload: &str, maximum_bytes: usize) -> Result<Self, ApiError> {
require_byte_limit(payload, maximum_bytes)?;
let value: Self = from_json(payload)?;
value.validate()?;
Ok(value)
}
Comment thread
seonghobae marked this conversation as resolved.

/// Serialize this validated anchor artifact.
///
/// # Errors
///
/// Returns a fail-closed validation or serialization error.
pub fn to_json(&self) -> Result<String, ApiError> {
self.validate()?;
to_json_with_limit(self, DEFAULT_LINEAGE_CRITERION_ANCHOR_BYTE_LIMIT)
}

fn validate(&self) -> Result<(), ApiError> {
require_contract_version(
self.contract_version,
LINEAGE_CRITERION_ANCHOR_CONTRACT_VERSION,
)?;
if self.anchor_kind_code != "lineage_pair_criterion"
|| self.validated_pair_count == 0
|| !is_canonical_sha256(&self.source_snapshot_sha256)
{
return Err(ApiError::InvalidWirePayload);
}
require_nonempty(&self.estimation_run_id)?;
let estimation_run_id = uuid::Uuid::parse_str(&self.estimation_run_id)
.map_err(|_| ApiError::InvalidWirePayload)?;
if estimation_run_id.hyphenated().to_string() != self.estimation_run_id {
return Err(ApiError::InvalidWirePayload);
}
KnowledgeCutoff::parse_rfc3339(&self.knowledge_cutoff)
.map_err(|_| ApiError::InvalidWirePayload)?;
Comment thread
seonghobae marked this conversation as resolved.
Ok(())
}
Comment thread
seonghobae marked this conversation as resolved.
}

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

#[cfg(test)]
mod tests {
use super::*;

fn anchor() -> LineageCriterionAnchor {
LineageCriterionAnchor {
contract_version: LINEAGE_CRITERION_ANCHOR_CONTRACT_VERSION,
anchor_kind_code: "lineage_pair_criterion".into(),
estimation_run_id: "018f47e7-7b5b-7cc0-98c6-15fdf9e3d9b1".into(),
source_snapshot_sha256: "a".repeat(64),
knowledge_cutoff: "2026-08-25T00:00:00Z".into(),
criterion_validity_status: CriterionValidityStatus::Accepted,
validated_pair_count: 600,
}
}

#[test]
fn accepted_and_rejected_results_round_trip_without_local_reinterpretation() {
for status in [
CriterionValidityStatus::Accepted,
CriterionValidityStatus::Rejected,
] {
let mut value = anchor();
value.criterion_validity_status = status;
let json = value.to_json().expect("serialize");
assert_eq!(
LineageCriterionAnchor::from_json(&json).expect("parse"),
value
);
}
let mut digit_digest = anchor();
digit_digest.source_snapshot_sha256 = "0".repeat(64);
assert!(digit_digest.to_json().is_ok());
}

#[test]
fn malformed_or_unbound_artifacts_fail_closed() {
for mutate in 0..9 {
let mut value = anchor();
match mutate {
0 => value.contract_version = 2,
1 => value.anchor_kind_code = "internal_structure".into(),
2 => value.estimation_run_id = "not-a-uuid".into(),
3 => value.source_snapshot_sha256 = "A".repeat(64),
4 => value.source_snapshot_sha256 = "a".repeat(63),
5 => value.estimation_run_id.clear(),
6 => value.knowledge_cutoff = "not-a-time".into(),
7 => value.estimation_run_id = "018f47e77b5b7cc098c615fdf9e3d9b1".into(),
_ => value.validated_pair_count = 0,
}
assert!(value.to_json().is_err());
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

#[test]
fn unknown_fields_are_rejected() {
let json = anchor().to_json().expect("serialize");
let payload = json.strip_suffix('}').expect("object");
assert!(LineageCriterionAnchor::from_json(&format!("{payload},\"theta\":0.8}}")).is_err());

let mut semantically_invalid = anchor();
semantically_invalid.validated_pair_count = 0;
let json = serde_json::to_string(&semantically_invalid).expect("wire shape");
assert!(LineageCriterionAnchor::from_json(&json).is_err());
}

#[test]
fn caller_payload_limit_is_enforced_before_parsing() {
let json = anchor().to_json().expect("serialize");
assert!(LineageCriterionAnchor::from_json_with_limit(&json, 1).is_err());
}

#[test]
fn published_schema_keeps_the_executable_identity_constraints() {
let schema: serde_json::Value = serde_json::from_str(include_str!(
"../../../schemas/lineage_criterion_anchor_v1.json"
))
.expect("published schema");
let properties = schema["properties"].as_object().expect("properties");
assert_eq!(schema["additionalProperties"], false);
assert_eq!(
schema["required"],
serde_json::json!([
"contract_version",
"anchor_kind_code",
"estimation_run_id",
"source_snapshot_sha256",
"knowledge_cutoff",
"criterion_validity_status",
"validated_pair_count"
])
);
assert_eq!(properties["contract_version"]["const"], 1);
assert_eq!(
properties["anchor_kind_code"]["const"],
"lineage_pair_criterion"
);
assert_eq!(
properties["criterion_validity_status"]["enum"],
serde_json::json!(["accepted", "rejected"])
);
assert_eq!(properties["validated_pair_count"]["minimum"], 1);
assert_eq!(
properties["estimation_run_id"]["pattern"],
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
);
assert_eq!(
properties["source_snapshot_sha256"]["pattern"],
"^[0-9a-f]{64}$"
);
}
}
10 changes: 10 additions & 0 deletions docs/API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ Wire payloads:
- reconstruct through domain validation rather than deserialize directly into private state;
- use stable machine-readable error codes plus content-redacting messages.

The TEPP-owned Event Lineage criterion artifact is
`tepp.lineage_criterion_anchor.v1` (`schemas/lineage_criterion_anchor_v1.json`).
A LineageWeave analysis run requests model contract
`tepp-lineage-criterion-v1` and output profile
`lineage_pair_criterion_anchor`. The artifact carries TEPP's accepted or
rejected criterion-validity outcome bound to one fast-mlsirm estimation run,
snapshot, cutoff, and validated pair count. The contract does not authorize a
consumer to self-assert validity; no weight vector activates until TEPP's
registered implementation returns the digest-bound artifact.

## 4. Target HTTP resource model

When the service layer is introduced, use resources such as:
Expand Down
73 changes: 73 additions & 0 deletions docs/adr/0023-lineage-criterion-anchor-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ADR 0023 — TEPP-owned Event Lineage criterion anchor

**Decision status:** Accepted
**Implementation maturity:** active-PR — the transport contract is implemented on PR #237; the registered TEPP analysis remains accepted-target
**Date:** 2026-08-25
**Supersedes:** None; complements ADR 0014's scientific promotion boundary.

## Context

LineageWeave uses fast-mlsirm to estimate relative Event Lineage channel
information. Internal response structure is not independent criterion
validity, and a consumer-authored validity flag would not make it independent.

## Decision

TEPP owns the `tepp.lineage_criterion_anchor.v1` result artifact and the
`tepp-lineage-criterion-v1` / `lineage_pair_criterion_anchor` analysis-run
request identity. The artifact binds TEPP's accepted or rejected criterion
decision to one opaque estimation run, immutable snapshot SHA-256, knowledge
cutoff, and positive validated-pair count.
The estimation-run identity uses the canonical lowercase, hyphenated UUID
form in both the executable DTO and the published JSON Schema.

The wire contract does not define an arbitrary correlation threshold, invent a
theta, or allow LineageWeave to reinterpret a rejection. The registered TEPP
analysis implementation and its scientific validation evidence own the
criterion design and acceptance procedure. Until that implementation emits a
digest-bound artifact through the terminal-result contract, consumers must
treat channel weighting as unavailable.

This separation follows the Standards' requirement that an intended score
interpretation and use be stated and supported by appropriate validity
evidence; internal model fit is not silently promoted to evidence for the
Event Lineage use.

## Alternatives considered

1. Let the consumer author a validity flag — rejected because the evidence
would not be independent of the proposed weights.
2. Treat an accepted transport receipt as validity evidence — rejected because
transport acceptance does not establish the intended score interpretation.
3. Publish a TEPP-owned, identity-bound outcome — accepted because it keeps
criterion authority and provenance at the measurement boundary.

## Consequences

- An accepted transport receipt is never a criterion anchor.
- Unknown fields and malformed provenance fail closed.
- Both accepted and rejected outcomes are preserved; only TEPP can author the
outcome, and a consumer may activate weights only for an exact accepted
artifact.
- The executable estimator remains a separately gated delivery slice.

## Verification

The Rust contract tests cover accepted and rejected round trips, canonical UUID
identity, malformed provenance, unknown fields, and payload limits. The JSON
Schema is checked against the same canonical UUID examples by the API schema
test suite.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

## Rollback and supersession

Rollback stops publishing the result profile while preserving previously issued
versioned artifacts. Supersession requires a new ADR that preserves independent
criterion authority, exact run/snapshot/cutoff binding, and fail-closed consumer
activation.

## Reference

American Educational Research Association, American Psychological
Association, & National Council on Measurement in Education. (2014).
*Standards for educational and psychological testing*. American Educational
Research Association. https://www.testingstandards.net/open-access-files.html
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio
| [0020](0020-span-grounded-semantic-units.md) | Span-grounded semantic units; language tags are not identity | Accepted | active-PR | First ADR 0004 production slice; concept alignment, invariance, and topic estimation are not claimed. |
| [0021](0021-lineageweave-project-history-boundary.md) | LineageWeave project-history service boundary | Accepted | active-PR | Credential-free bounded project-history API preserves LineageWeave authorization ownership. |
| [0022](0022-deterministic-analysis-run-execution.md) | Deterministic cutoff-safe analysis-run execution | Accepted | active-PR | Closes the first executable product path from accepted run to digest-bound terminal result without claiming estimator authority. |
| [0023](0023-lineage-criterion-anchor-contract.md) | TEPP-owned Event Lineage criterion anchor | Accepted | active-PR | PR #237 publishes the strict accepted/rejected artifact and identities; estimator execution remains fail-closed future work. |
| [0001](0001-rust-first-modular-msa.md) | Rust-first numerical core and CPU `f64` reference | Accepted | partial | ADR 0011 owns cross-service/MSA authority; 0001 retains numerical/backend authority. |
| [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | partial | Typed clocks/intervals are implemented-main via `temporal_core`; input-process-outcome event-time order is `outcome_order` on the active PR. Remaining clock-identity and split enforcement stay accepted-target. |
| [0003](0003-relational-event-multiple-membership.md) | Relational event ontology and time-varying cross-classified multiple membership | Accepted | partial | Weighted time-varying membership network/roles and the forward-transition graph are implemented-main; IPO event-time order is `outcome_order` on the active PR; full multilevel estimators and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. |
Expand Down
16 changes: 16 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Product and Technical Gap Baseline

## 2026-08-25 Event Lineage anchor contract slice

- Exact base: protected `main` `cf0e0ad74d23c5d2e0e33d389bb0bb4d37067c31`.
- This branch publishes TEPP's strict request identity and
`tepp.lineage_criterion_anchor.v1` accepted/rejected artifact contract.
- The buyer-visible integrity gain is fail-closed: LineageWeave cannot promote
fast-mlsirm's internal response structure into calibrated Event Lineage
weights without an exact TEPP-authored criterion result.
- Remaining product gap: the registered TEPP criterion estimator and terminal
artifact delivery are not implemented by this contract slice. Until they
exist and pass scientific recovery/validity gates, production activation
remains unavailable; the consumer must not invent a substitute.
- Acceptance evidence for this slice: complete `tepp_api` tests, warning-free
clippy, strict unknown-field/provenance rejection, schema and ADR/API
traceability, followed by exact-head protected checks and independent review.

**Status:** Live delivery baseline
**Product:** Temporal Event Psychometrics Platform (TEPP)
**Snapshot:** 2026-08-25T04:24:53Z
Expand Down
Loading
Loading