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 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 |
| `revision_order` | later document revisions must have later system time |
| `encrypted_mapping` | purpose-bound in-memory AES-256-GCM identity mappings; no plaintext persistence or KMS integration |
| `citation_edge` | citation, revision, translation, and retrospective edges are not state transitions |
| `psychometric_fit` | CPU `f64` ESEM loading recovery and event-time DSEM lag gates |
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

- `revision_order` system-time gate: a higher document revision number cannot carry earlier or equal system time; recovered order flags match known truth at a higher computed rate than accepting every pair (ADR 0002/0013).
- `encrypted_mapping` purpose-bound AES-256-GCM envelope: source identities are sealed with an operating-system-generated nonce and analytical/key identifiers as authenticated associated data, with a 1 MiB resource bound, so analytical, log, and model-artifact purposes cannot recover plaintext; recovered identities match known truth at a higher computed rate than collapsing every mapping to one name. Persistence and KMS wait for a later migration (ADR 0009).
- `citation_edge` provenance gate: citation, translation, revision, and retrospective-report edges may point to the past but cannot become input-process-outcome transitions; recovered kinds match known truth at a higher computed rate than collapsing every edge to citation (ADR 0002/0003).
- `psychometric_fit` CPU `f64` ESEM/DSEM fit: exploratory OLS recovers known cross-loadings from admitted log-ratio or logistic-normal coordinates with computed RMSE below a zero-loading collapse; reverse or zero event-time lagged paths fail closed; a good global fit cannot reclassify formative or network constructs as reflective (ADR 0005). No new migration number (`#45` still owns `0007`).
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/revision_order",
"crates/encrypted_mapping",
"crates/citation_edge",
"crates/psychometric_fit",
Expand Down Expand Up @@ -38,6 +39,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/revision_order",
"crates/encrypted_mapping",
"crates/citation_edge",
"crates/psychometric_fit",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ crates/corpus_split
crates/tepp_simulation
crates/validation_core
crates/tepp_api
crates/revision_order
crates/encrypted_mapping
crates/citation_edge
crates/psychometric_fit
Expand Down
17 changes: 17 additions & 0 deletions crates/revision_order/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "revision_order"
description = "Later document revisions cannot move backward in system time."
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
48 changes: 48 additions & 0 deletions crates/revision_order/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! Fail-closed revision-order errors.

use std::fmt;

/// A fail-closed revision-order error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum RevisionOrderError {
/// A later revision did not have a later system time.
SystemTimeDidNotIncrease,
/// A revision number or recovery slice was empty, zero, or mismatched.
InvalidRevisionPayload,
}

impl fmt::Display for RevisionOrderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::SystemTimeDidNotIncrease => {
"later document revisions must have later system time"
}
Self::InvalidRevisionPayload => "invalid revision-order payload",
};
formatter.write_str(message)
}
}

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

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

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
RevisionOrderError::SystemTimeDidNotIncrease,
"later document revisions must have later system time",
),
(
RevisionOrderError::InvalidRevisionPayload,
"invalid revision-order payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
21 changes: 21 additions & 0 deletions crates/revision_order/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)]
//! Later document revisions cannot move backward in system time.
//!
//! A higher revision number is a later assertion about the same document
//! identity. Its system time must strictly increase (ADR 0002/0013).

mod error;
mod revision;

/// Fail-closed revision-order errors.
pub use error::RevisionOrderError;
/// One document revision with a positive revision number and system time.
pub use revision::DocumentRevision;
/// Fraction of recovered order flags that match known truth.
pub use revision::order_recovery_rate;
/// Refuse a later revision whose system time did not increase.
pub use revision::refuse_nonincreasing_system_time;
/// Return whether a later revision has a later system time.
pub use revision::revisions_are_increasing;
138 changes: 138 additions & 0 deletions crates/revision_order/src/revision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! Document revisions stamped with system time.

use crate::RevisionOrderError;

/// One document revision with a positive revision number and system time.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DocumentRevision {
revision_number: u32,
system_time_seconds: i64,
}

impl DocumentRevision {
/// Construct a revision whose number is at least one.
///
/// # Errors
///
/// Returns [`RevisionOrderError::InvalidRevisionPayload`] when
/// `revision_number` is zero.
pub const fn new(
revision_number: u32,
system_time_seconds: i64,
) -> Result<Self, RevisionOrderError> {
if revision_number == 0 {
return Err(RevisionOrderError::InvalidRevisionPayload);
}
Ok(Self {
revision_number,
system_time_seconds,
})
}

/// Positive revision number.
#[must_use]
pub const fn revision_number(self) -> u32 {
self.revision_number
}

/// System/record time in seconds.
#[must_use]
pub const fn system_time_seconds(self) -> i64 {
self.system_time_seconds
}
}

/// Return whether `later` has a greater revision number and later system time.
///
/// # Errors
///
/// Returns [`RevisionOrderError::InvalidRevisionPayload`] when `later` is not
/// a strictly greater revision number than `earlier`.
pub fn revisions_are_increasing(
earlier: DocumentRevision,
later: DocumentRevision,
) -> Result<bool, RevisionOrderError> {
if later.revision_number <= earlier.revision_number {
return Err(RevisionOrderError::InvalidRevisionPayload);
}
Ok(later.system_time_seconds > earlier.system_time_seconds)
}
Comment on lines +51 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: System-time gate is consistent with bitemporal revision semantics

The gate refuses a later revision unless its system_time_seconds strictly increases (revision.rs, 55-58). This is transaction/system-time ordering only and does not constrain event/valid time, so it remains consistent with AGENTS.md #5 (revision edges may point to the past in event time). The strict > comparison also correctly rejects equal system times, matching the PR's stated 'earlier or equal' refusal. No bug; noting because the distinction between system time and event time is the crux of correctness here.

Open in Devin Review

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

Comment on lines +45 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Doc comment on revisions_are_increasing understates behavior

The doc for revisions_are_increasing at revision.rs says it returns whether later has "a greater revision number and later system time," but the function only returns the system-time comparison (revision.rs); the revision-number condition is enforced by returning an error, not folded into the boolean. This is a documentation/behavior wording mismatch, not a correctness bug, since callers get an Err when revision numbers are non-increasing rather than a false.

Open in Devin Review

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


/// Refuse a later revision whose system time did not increase.
///
/// # Errors
///
/// Returns revision-construction errors, or
/// [`RevisionOrderError::SystemTimeDidNotIncrease`] when the system times
/// are not strictly increasing.
pub fn refuse_nonincreasing_system_time(
earlier: DocumentRevision,
later: DocumentRevision,
) -> Result<(), RevisionOrderError> {
if revisions_are_increasing(earlier, later)? {
return Ok(());
}
Err(RevisionOrderError::SystemTimeDidNotIncrease)
}
Comment on lines +68 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: refuse_nonincreasing_system_time returns InvalidRevisionPayload for equal/backward revision numbers

refuse_nonincreasing_system_time propagates InvalidRevisionPayload (via revisions_are_increasing) when the later revision number is not strictly greater, rather than SystemTimeDidNotIncrease. This is documented ("Returns revision-construction errors, or SystemTimeDidNotIncrease...") and tested, so it is intentional, but callers should be aware that a same-numbered revision pair is rejected as an invalid payload, not as a time-order violation.

Open in Devin Review

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

Comment on lines +68 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Error-propagation branch in refuse_nonincreasing_system_time may be untested

In refuse_nonincreasing_system_time (revision.rs), the ? operator on revisions_are_increasing(...) has an implicit error-propagation branch that fires only when the two revision numbers are equal or reversed. Every test call (both the unit test at revision.rs and the integration tests at order_contract.rs) passes strictly increasing revision numbers, so that inner Err path is never exercised through this function. Given the repo's 100% branch-coverage gate (AGENTS.md #8), this is worth confirming against the nightly branch-coverage run; the author claims 12/12 branches, but LLVM branch coverage of the ? desugaring is worth double-checking. Not reported as a bug because it is a coverage/CI concern that will be caught by the pinned coverage gate, not a runtime defect.

Open in Devin Review

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


/// Fraction of recovered order flags that match known truth.
///
/// # Errors
///
/// Returns [`RevisionOrderError::InvalidRevisionPayload`] when either slice
/// is empty or the lengths differ.
pub fn order_recovery_rate(truth: &[bool], decided: &[bool]) -> Result<f64, RevisionOrderError> {
if truth.is_empty() || truth.len() != decided.len() {
return Err(RevisionOrderError::InvalidRevisionPayload);
}
let mut matches = 0_u32;
for (truth_flag, decided_flag) in truth.iter().zip(decided) {
if truth_flag == decided_flag {
matches += 1;
}
}
Ok(f64::from(matches) / truth.len() as f64)
}

#[cfg(test)]
mod tests {
use super::{
DocumentRevision, order_recovery_rate, refuse_nonincreasing_system_time,
revisions_are_increasing,
};
use crate::RevisionOrderError;

#[test]
fn local_branches_cover_order_and_payloads() {
let first = DocumentRevision::new(1, 10).expect("first");
let second = DocumentRevision::new(2, 20).expect("second");
assert_eq!(first.revision_number(), 1);
assert_eq!(first.system_time_seconds(), 10);
assert!(revisions_are_increasing(first, second).expect("increasing"));
refuse_nonincreasing_system_time(first, second).expect("ok");
let same_time = DocumentRevision::new(3, 20).expect("same");
assert!(!revisions_are_increasing(second, same_time).expect("flat"));
assert_eq!(
refuse_nonincreasing_system_time(second, same_time),
Err(RevisionOrderError::SystemTimeDidNotIncrease)
);
assert_eq!(
revisions_are_increasing(second, first),
Err(RevisionOrderError::InvalidRevisionPayload)
);
assert_eq!(
DocumentRevision::new(0, 1),
Err(RevisionOrderError::InvalidRevisionPayload)
);
let matched = order_recovery_rate(&[true], &[true]).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
assert_eq!(
order_recovery_rate(&[], &[]),
Err(RevisionOrderError::InvalidRevisionPayload)
);
assert_eq!(
order_recovery_rate(&[true], &[]),
Err(RevisionOrderError::InvalidRevisionPayload)
);
}
}
7 changes: 7 additions & 0 deletions crates/revision_order/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `revision_order` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "revision_order");
}
77 changes: 77 additions & 0 deletions crates/revision_order/tests/order_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! Later revisions cannot carry earlier or equal system time.

use revision_order::{
DocumentRevision, RevisionOrderError, order_recovery_rate, refuse_nonincreasing_system_time,
revisions_are_increasing,
};

fn revision(number: u32, system: i64) -> DocumentRevision {
DocumentRevision::new(number, system).expect("revision")
}

#[test]
fn later_revisions_cannot_move_backward_in_system_time() {
let first = revision(1, 10);
let second = revision(2, 20);
let backward = revision(3, 15);
assert!(revisions_are_increasing(first, second).expect("increasing"));
refuse_nonincreasing_system_time(first, second).expect("ok");
assert!(!revisions_are_increasing(second, backward).expect("backward"));
assert_eq!(
refuse_nonincreasing_system_time(second, backward),
Err(RevisionOrderError::SystemTimeDidNotIncrease)
);
assert_eq!(
refuse_nonincreasing_system_time(second, revision(4, 20)),
Err(RevisionOrderError::SystemTimeDidNotIncrease)
);
}

#[test]
fn recovered_order_flags_match_known_truth_better_than_accepting_all() {
let pairs = [
(revision(1, 10), revision(2, 20)),
(revision(2, 20), revision(3, 15)),
(revision(3, 30), revision(4, 40)),
];
let truth = [true, false, true];
let recovered = [
revisions_are_increasing(pairs[0].0, pairs[0].1).expect("p0"),
revisions_are_increasing(pairs[1].0, pairs[1].1).expect("p1"),
revisions_are_increasing(pairs[2].0, pairs[2].1).expect("p2"),
];
let collapsed = [true, true, true];
let recovered_rate = order_recovery_rate(&truth, &recovered).expect("recovered");
let collapsed_rate = order_recovery_rate(&truth, &collapsed).expect("collapsed");
let expected = {
let mut matches = 0_u32;
for (truth_flag, decided_flag) in truth.iter().zip(recovered.iter()) {
if truth_flag == decided_flag {
matches += 1;
}
}
f64::from(matches) / f64::from(u32::try_from(truth.len()).expect("len"))
};
assert!((recovered_rate - expected).abs() < f64::EPSILON);
assert!(recovered_rate > collapsed_rate);
}

#[test]
fn empty_or_invalid_revision_payloads_fail_closed() {
assert_eq!(
DocumentRevision::new(0, 10),
Err(RevisionOrderError::InvalidRevisionPayload)
);
assert_eq!(
order_recovery_rate(&[], &[]),
Err(RevisionOrderError::InvalidRevisionPayload)
);
assert_eq!(
order_recovery_rate(&[true], &[]),
Err(RevisionOrderError::InvalidRevisionPayload)
);
assert_eq!(
order_recovery_rate(&[true, false], &[true]),
Err(RevisionOrderError::InvalidRevisionPayload)
);
}
2 changes: 1 addition & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| time-varying cross-classified multiple membership | PRD; ADR 0003 | `membership_core` network on protected main; multilevel estimators remaining | partial |
| leakage-safe availability/cutoff snapshots | PRD; ADR 0002/0013 | `corpus_split` on protected main | implemented-main |
| recovery metrics (RMSE, bias, coverage, graph, temporal order, Monte Carlo SE gates) | PRD; Test Strategy; ADR 0007/0014 | `validation_core` on protected main (PR #19); SE-aware Monte Carlo gates included | implemented-main |
| PostgreSQL bitemporal/lineage persistence | ADR 0013; Architecture/ERD | `persistence_postgres` migration contracts, in-memory adapters, live SQL session/document SQL port, tenant RLS (`0002` + session GUC/role helpers), `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` driver, exact-head live PostgreSQL CI with isolation proof, append-only immutability triggers (`0004`), temporal interval ordering CHECKs (`0005`), typed membership assignment (`0006` implemented-main), event-relation/mention/instance SQL (#37–#39 implemented-main), source-artifact SQL (#40 implemented-main), audit-event SQL (#41 implemented-main), concurrent document-write stress (#43 implemented-main), and backup/restore integrity revalidation (#44 implemented-main); remaining physical ERD constraints | partial |
| PostgreSQL bitemporal/lineage persistence | ADR 0013; Architecture/ERD | `persistence_postgres` on protected main as before; `revision_order` later-revision system-time gate on the active PR; remaining physical ERD constraints | partial |
| known-truth temporal/event simulation manifests | PRD; TRD; Test Strategy | `tepp_simulation` on protected main; recovery metrics in `validation_core` | implemented-main |
| versioned service/API contracts and exports | PRD; API contract; ADR 0011/0013 | `tepp_api` analysis-run/export/JSON-LD/GraphML contracts on protected main (PR #21); HTTP service remaining accepted-target | partial |
| immutable split/run/reproducibility manifests | ADR 0013; ERD | `tepp_api` reproducibility manifest and corpus-split leakage-audit wire (`CorpusSplitManifest` v1) on this PR; `persistence_postgres` append-only SQL insert/lookup for `reproducibility_manifest`, `corpus_split_manifest`, `model_run`, and `model_artifact` (migration `0003`); full physical ERD constraints remaining | partial |
Expand Down
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio
| [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | partial | `tepp_api` router/ablation/orchestrator binding implemented-main; live NIM execution and production ablation evidence remain accepted-target. |
| [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. |
| [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. |
| [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; `0007` retention/deletion/legal-hold implemented-main; document revision system-time order in `revision_order` is active on this PR; remaining physical ERD/backup accepted-target. |
| [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, `0006` membership, `0007` retention/deletion/legal-hold, and backup/restore integrity revalidation implemented-main; remaining physical ERD/DR-runbook depth accepted-target. |
| [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | active-PR | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates; `topic_lineage` implements the active/dormant/reactivated identity slice on the active PR. |
| [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, recovery identity, retention/deletion/legal-hold, backup/restore integrity, and concurrent writes; optional `live-sqlx` `PgPool`, live PG CI, tenant RLS, and `0006` membership implemented-main; remaining physical ERD/backup evidence accepted-target. |
Expand Down
Loading
Loading