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
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ boundaries above remain the target modular MSA architecture.
| `tepp_simulation` | known-truth temporal/event data generation |
| `validation_core` | RMSE, bias, coverage, graph, and Monte Carlo metrics |
| `tepp_api` | versioned DTO, schema, and export contracts |
| `translation_edge` | translation, copy, and revision edges are not state transitions |

No crate exposes placeholder production behavior in Task 1. This prevents an
empty façade from becoming a de facto public API before its invariants and tests
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `translation_edge` identity gate: translation, same-language copy, and revision edges cannot become state transitions; a shared primary language tag cannot be classified as a translation; recovered kinds match known truth at a higher computed rate than collapsing every kind to translation (ADR 0002/0003).
- `persistence_postgres` retention/deletion/legal-hold (migration `0007`): policy rows, legal holds that block completed deletion, evidence tombstones without raw-source restore, analysis exclusion only for `logical_revocation`/`identity_tombstone` (not `cache_export_removal`), and deletion requests bound to the cited retention policy's tenant/class/purpose.
- `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013).
- `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number.
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/translation_edge",
]
default-members = [
"crates/evidence_core",
Expand All @@ -23,6 +24,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/translation_edge",
]

[workspace.package]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ implemented in Rust.
## Current implementation state

This branch establishes the Task 1 Rust workspace and quality-gate foundation.
The ten bounded crates compile independently but intentionally expose no
The eleven bounded crates compile independently but intentionally expose no
placeholder production APIs. Domain behavior begins in Task 2 with immutable
evidence identifiers and source records.

Expand All @@ -22,6 +22,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/translation_edge
```

## Local verification
Expand Down
17 changes: 17 additions & 0 deletions crates/translation_edge/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "translation_edge"
description = "Translation, same-language copy, and revision edges cannot become state transitions."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
readme.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false

[lints]
workspace = true
62 changes: 62 additions & 0 deletions crates/translation_edge/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Fail-closed translation-edge errors.

use std::fmt;

/// A fail-closed translation-edge error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TranslationEdgeError {
/// A translation, copy, or revision edge was treated as a state transition.
TranslationIsNotTransition,
/// A same-language pair was classified as a translation.
SameLanguageIsNotTranslation,
/// A language tag was empty or lacked a primary subtag.
InvalidLanguageTag,
/// A kind slice was empty or length-mismatched.
InvalidEdgePayload,
}

impl fmt::Display for TranslationEdgeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::TranslationIsNotTransition => {
"translation, same-language copy, and revision edges are not state transitions"
}
Self::SameLanguageIsNotTranslation => "same primary language is not a translation",
Self::InvalidLanguageTag => "invalid language tag",
Self::InvalidEdgePayload => "invalid translation-edge payload",
};
formatter.write_str(message)
}
}

impl std::error::Error for TranslationEdgeError {}

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

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
TranslationEdgeError::TranslationIsNotTransition,
"translation, same-language copy, and revision edges are not state transitions",
),
(
TranslationEdgeError::SameLanguageIsNotTranslation,
"same primary language is not a translation",
),
(
TranslationEdgeError::InvalidLanguageTag,
"invalid language tag",
),
(
TranslationEdgeError::InvalidEdgePayload,
"invalid translation-edge payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
181 changes: 181 additions & 0 deletions crates/translation_edge/src/kind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
//! Translation-related provenance kinds that may point to the past.

use crate::TranslationEdgeError;

/// Closed vocabulary of translation-related edges that are not state transitions.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TranslationKind {
/// A translation of an earlier document into a different language.
Translation,
/// A same-language template or copied variant.
SameLanguageCopy,
/// A same-language revision of an earlier document.
Revision,
}

impl TranslationKind {
/// Return the stable wire kind name.
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Self::Translation => "translates",
Self::SameLanguageCopy => "template_copy_of",
Self::Revision => "revises",
}
}

/// Parse a stable wire kind name.
///
/// # Errors
///
/// Returns [`TranslationEdgeError::InvalidEdgePayload`] for unrecognized
/// names, including transition names such as `causes`.
pub fn from_wire_name(name: &str) -> Result<Self, TranslationEdgeError> {
match name {
"translates" => Ok(Self::Translation),
"template_copy_of" => Ok(Self::SameLanguageCopy),
"revises" => Ok(Self::Revision),
_ => Err(TranslationEdgeError::InvalidEdgePayload),
}
}

/// Return whether this kind is a forward state-transition edge.
///
/// Translation-related provenance kinds are never transitions.
#[must_use]
pub const fn is_transition_edge(self) -> bool {
match self {
Self::Translation | Self::SameLanguageCopy | Self::Revision => false,
}
}
}

/// Refuse to treat a translation-related edge as a forward state transition.
///
/// # Errors
///
/// Always returns [`TranslationEdgeError::TranslationIsNotTransition`].
pub fn refuse_translation_as_transition(
_kind: TranslationKind,
) -> Result<(), TranslationEdgeError> {
Err(TranslationEdgeError::TranslationIsNotTransition)
}

/// Refuse to classify a same primary-language pair as a translation.
///
/// Primary subtags are compared case-insensitively. `en` and `en-US` share
/// a primary language and therefore cannot be a translation.
///
/// # Errors
///
/// Returns [`TranslationEdgeError::InvalidLanguageTag`] when either tag is
/// empty or lacks a primary subtag, and
/// [`TranslationEdgeError::SameLanguageIsNotTranslation`] when the primary
/// subtags match.
pub fn refuse_same_language_as_translation(
source_language: &str,
target_language: &str,
) -> Result<(), TranslationEdgeError> {
let source = primary_language_subtag(source_language)?;
let target = primary_language_subtag(target_language)?;
if source.eq_ignore_ascii_case(target) {
return Err(TranslationEdgeError::SameLanguageIsNotTranslation);
}
Ok(())
}

/// Fraction of recovered provenance kinds that match known truth.
///
/// # Errors
///
/// Returns [`TranslationEdgeError::InvalidEdgePayload`] when either slice is
/// empty or the lengths differ.
pub fn edge_kind_recovery_rate(
truth: &[TranslationKind],
decided: &[TranslationKind],
) -> Result<f64, TranslationEdgeError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(TranslationEdgeError::InvalidEdgePayload);
}
let mut matches = 0_u32;
for (truth_kind, decided_kind) in truth.iter().zip(decided) {
if truth_kind == decided_kind {
matches += 1;
}
}
Ok(f64::from(matches) / truth.len() as f64)
}

fn primary_language_subtag(tag: &str) -> Result<&str, TranslationEdgeError> {
let trimmed = tag.trim();
if trimmed.is_empty() {
return Err(TranslationEdgeError::InvalidLanguageTag);
}
match trimmed.split_once('-') {
Some(("", _)) => Err(TranslationEdgeError::InvalidLanguageTag),
Some((primary, _)) => Ok(primary),
None => Ok(trimmed),
}
}

#[cfg(test)]
mod tests {
use super::{
TranslationKind, edge_kind_recovery_rate, primary_language_subtag,
refuse_same_language_as_translation, refuse_translation_as_transition,
};
use crate::TranslationEdgeError;

#[test]
fn local_branches_cover_kinds_languages_and_payloads() {
for kind in [
TranslationKind::Translation,
TranslationKind::SameLanguageCopy,
TranslationKind::Revision,
] {
assert!(!kind.is_transition_edge());
assert_eq!(
TranslationKind::from_wire_name(kind.wire_name()).expect("round-trip"),
kind
);
assert_eq!(
refuse_translation_as_transition(kind),
Err(TranslationEdgeError::TranslationIsNotTransition)
);
}
assert_eq!(
TranslationKind::from_wire_name("causes"),
Err(TranslationEdgeError::InvalidEdgePayload)
);
assert_eq!(
refuse_same_language_as_translation("en-US", "EN"),
Err(TranslationEdgeError::SameLanguageIsNotTranslation)
);
refuse_same_language_as_translation("en", "fr").expect("cross-language");
assert_eq!(
primary_language_subtag(""),
Err(TranslationEdgeError::InvalidLanguageTag)
);
assert_eq!(
primary_language_subtag("-"),
Err(TranslationEdgeError::InvalidLanguageTag)
);
let truth = [TranslationKind::Translation, TranslationKind::Revision];
let matched = edge_kind_recovery_rate(&truth, &truth).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
let partial = edge_kind_recovery_rate(
&truth,
&[TranslationKind::Translation, TranslationKind::Translation],
)
.expect("partial");
assert!((partial - 0.5).abs() < f64::EPSILON);
assert_eq!(
edge_kind_recovery_rate(&[], &[]),
Err(TranslationEdgeError::InvalidEdgePayload)
);
assert_eq!(
edge_kind_recovery_rate(&truth, &[]),
Err(TranslationEdgeError::InvalidEdgePayload)
);
}
}
21 changes: 21 additions & 0 deletions crates/translation_edge/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Translation, same-language copy, and revision are not state transitions.
//!
//! Provenance may point to earlier event time. A same primary language tag
//! cannot be classified as a translation (ADR 0002/0003).

mod error;
mod kind;

/// Fail-closed translation-edge errors.
pub use error::TranslationEdgeError;
/// Closed vocabulary of translation-related provenance that is not a transition.
pub use kind::TranslationKind;
/// Fraction of recovered provenance kinds that match known truth.
pub use kind::edge_kind_recovery_rate;
/// Refuse to treat a same-language pair as a translation.
pub use kind::refuse_same_language_as_translation;
/// Refuse to treat a translation-related edge as a forward state transition.
pub use kind::refuse_translation_as_transition;
7 changes: 7 additions & 0 deletions crates/translation_edge/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `translation_edge` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "translation_edge");
}
Loading
Loading