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 |
| `available_clock` | availability time cannot be replaced by event or system time |
| `document_clocks` | document rows must carry assertion time and document time |
| `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 |
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

- `available_clock` identity gate: event time and system time cannot stand in for availability time; recovered availability stamps match known truth at a higher computed rate than treating every stamp as system time (ADR 0002).
- `document_clocks` six-clock gate: a document analytical row cannot omit assertion time or document time, and event/system time cannot stand in for those clocks; recovered completeness flags match known truth at a higher computed rate than treating every row as complete (ADR 0002/0013).
- `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).
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/available_clock",
"crates/document_clocks",
"crates/revision_order",
"crates/encrypted_mapping",
Expand Down Expand Up @@ -40,6 +41,7 @@ default-members = [
"crates/tepp_simulation",
"crates/validation_core",
"crates/tepp_api",
"crates/available_clock",
"crates/document_clocks",
"crates/revision_order",
"crates/encrypted_mapping",
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/available_clock
crates/document_clocks
crates/revision_order
crates/encrypted_mapping
Expand Down
17 changes: 17 additions & 0 deletions crates/available_clock/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "available_clock"
description = "Availability time cannot be replaced by event or 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
99 changes: 99 additions & 0 deletions crates/available_clock/src/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Clock-family identity for availability stamps.

use crate::AvailableClockError;

/// Closed vocabulary of clocks that must not be confused with availability.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClockFamily {
/// Event/valid time.
EventTime,
/// System/record time.
SystemTime,
/// Availability time.
AvailableTime,
}

/// Return whether a stamp is on the availability clock.
///
/// # Errors
///
/// This function is infallible for the closed vocabulary and exists to keep
/// the public comparison surface explicit.
#[allow(clippy::unnecessary_wraps)]
pub fn stamp_is_available(family: ClockFamily) -> Result<bool, AvailableClockError> {
Ok(matches!(family, ClockFamily::AvailableTime))
}

/// Refuse to treat event time as availability time.
///
/// # Errors
///
/// Always returns [`AvailableClockError::EventTimeIsNotAvailableTime`].
pub fn refuse_event_time_as_available() -> Result<(), AvailableClockError> {
Err(AvailableClockError::EventTimeIsNotAvailableTime)
}

/// Refuse to treat system time as availability time.
///
/// # Errors
///
/// Always returns [`AvailableClockError::SystemTimeIsNotAvailableTime`].
pub fn refuse_system_time_as_available() -> Result<(), AvailableClockError> {
Err(AvailableClockError::SystemTimeIsNotAvailableTime)
}

/// Fraction of recovered availability flags that match known truth.
///
/// # Errors
///
/// Returns [`AvailableClockError::InvalidAvailabilityPayload`] when either
/// slice is empty or the lengths differ.
pub fn eligibility_recovery_rate(
truth: &[bool],
decided: &[bool],
) -> Result<f64, AvailableClockError> {
if truth.is_empty() || truth.len() != decided.len() {

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 says "either slice is empty" but only truth is checked

The rustdoc for eligibility_recovery_rate (clock.rs) states the error is returned when "either slice is empty or the lengths differ", but the guard at clock.rs only checks truth.is_empty(). This is not a behavioral bug: if truth is non-empty while decided is empty, the truth.len() != decided.len() check catches it, and if both are empty the truth.is_empty() check catches it. So all documented failure cases are still handled; only the wording is slightly imprecise.

Open in Devin Review

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

return Err(AvailableClockError::InvalidAvailabilityPayload);
}
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)
Comment on lines +55 to +64

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: Branch coverage of the || short-circuit relies on integration tests

eligibility_recovery_rate at clock.rs has a compound || condition with distinct branches. The in-crate unit test local_branches_cover_families_and_payloads exercises the empty case (&[]) and the length-mismatch case (&[true], &[]), plus a success path, which does cover both operands of the short-circuit. The if truth_flag == decided_flag non-match branch is only exercised via the integration test available_clock_contract.rs:48 (collapsed system-time flags of false vs truth true). Since cargo-llvm-cov aggregates all test binaries, the claimed 6/6 branch coverage is plausible, but reviewers should confirm coverage holds if the integration test is ever removed or run in isolation.

Open in Devin Review

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

}

#[cfg(test)]
mod tests {
use super::{
ClockFamily, eligibility_recovery_rate, refuse_event_time_as_available,
refuse_system_time_as_available, stamp_is_available,
};
use crate::AvailableClockError;

#[test]
fn local_branches_cover_families_and_payloads() {
assert!(stamp_is_available(ClockFamily::AvailableTime).expect("available"));
assert!(!stamp_is_available(ClockFamily::EventTime).expect("event"));
assert!(!stamp_is_available(ClockFamily::SystemTime).expect("system"));
assert_eq!(
refuse_event_time_as_available(),
Err(AvailableClockError::EventTimeIsNotAvailableTime)
);
assert_eq!(
refuse_system_time_as_available(),
Err(AvailableClockError::SystemTimeIsNotAvailableTime)
);
let matched = eligibility_recovery_rate(&[true], &[true]).expect("rate");
assert!((matched - 1.0).abs() < f64::EPSILON);
assert_eq!(
eligibility_recovery_rate(&[], &[]),
Err(AvailableClockError::InvalidAvailabilityPayload)
);
assert_eq!(
eligibility_recovery_rate(&[true], &[]),
Err(AvailableClockError::InvalidAvailabilityPayload)
);
}
}
53 changes: 53 additions & 0 deletions crates/available_clock/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//! Fail-closed available-clock errors.

use std::fmt;

/// A fail-closed available-clock error.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum AvailableClockError {
/// Event time was treated as availability time.
EventTimeIsNotAvailableTime,
/// System time was treated as availability time.
SystemTimeIsNotAvailableTime,
/// A recovery slice was empty or length-mismatched.
InvalidAvailabilityPayload,
}

impl fmt::Display for AvailableClockError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::EventTimeIsNotAvailableTime => "event time is not availability time",
Self::SystemTimeIsNotAvailableTime => "system time is not availability time",
Self::InvalidAvailabilityPayload => "invalid available-clock payload",
};
formatter.write_str(message)
}
}

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

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

#[test]
fn error_messages_are_stable() {
for (error, message) in [
(
AvailableClockError::EventTimeIsNotAvailableTime,
"event time is not availability time",
),
(
AvailableClockError::SystemTimeIsNotAvailableTime,
"system time is not availability time",
),
(
AvailableClockError::InvalidAvailabilityPayload,
"invalid available-clock payload",
),
] {
assert_eq!(error.to_string(), message);
}
}
}
23 changes: 23 additions & 0 deletions crates/available_clock/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::cast_precision_loss)]
//! Availability time cannot be replaced by event or system time.
//!
//! Historical eligibility uses availability versus knowledge cutoff. Event
//! time and system time are not substitutes (ADR 0002).

mod clock;
mod error;

/// Closed vocabulary of clocks that must not be confused with availability.
pub use clock::ClockFamily;
/// Fraction of recovered availability flags that match known truth.
pub use clock::eligibility_recovery_rate;
/// Refuse to treat event time as availability time.
pub use clock::refuse_event_time_as_available;
/// Refuse to treat system time as availability time.
pub use clock::refuse_system_time_as_available;
/// Return whether a stamp is on the availability clock.
pub use clock::stamp_is_available;
/// Fail-closed available-clock errors.
pub use error::AvailableClockError;
76 changes: 76 additions & 0 deletions crates/available_clock/tests/available_clock_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Event and system time cannot stand in for availability.

use available_clock::{
AvailableClockError, ClockFamily, eligibility_recovery_rate, refuse_event_time_as_available,
refuse_system_time_as_available, stamp_is_available,
};

#[test]
fn event_and_system_time_cannot_stand_in_for_availability() {
assert_eq!(
refuse_event_time_as_available(),
Err(AvailableClockError::EventTimeIsNotAvailableTime)
);
assert_eq!(
refuse_system_time_as_available(),
Err(AvailableClockError::SystemTimeIsNotAvailableTime)
);
assert!(stamp_is_available(ClockFamily::AvailableTime).expect("available"));
assert!(!stamp_is_available(ClockFamily::EventTime).expect("event"));
assert!(!stamp_is_available(ClockFamily::SystemTime).expect("system"));
}

#[test]
fn recovered_availability_stamps_match_known_truth_better_than_system_stand_in() {
let truth = [
ClockFamily::AvailableTime,
ClockFamily::AvailableTime,
ClockFamily::AvailableTime,
];
let recovered = truth;
let collapsed = [
ClockFamily::SystemTime,
ClockFamily::SystemTime,
ClockFamily::SystemTime,
];
let recovered_flags = [
stamp_is_available(recovered[0]).expect("r0"),
stamp_is_available(recovered[1]).expect("r1"),
stamp_is_available(recovered[2]).expect("r2"),
];
let collapsed_flags = [
stamp_is_available(collapsed[0]).expect("c0"),
stamp_is_available(collapsed[1]).expect("c1"),
stamp_is_available(collapsed[2]).expect("c2"),
];
let truth_flags = [true, true, true];
let recovered_rate = eligibility_recovery_rate(&truth_flags, &recovered_flags).expect("ok");
let collapsed_rate = eligibility_recovery_rate(&truth_flags, &collapsed_flags).expect("bad");
let expected = {
let mut matches = 0_u32;
for (truth_flag, decided_flag) in truth_flags.iter().zip(recovered_flags.iter()) {
if truth_flag == decided_flag {
matches += 1;
}
}
f64::from(matches) / f64::from(u32::try_from(truth_flags.len()).expect("len"))
};
assert!((recovered_rate - expected).abs() < f64::EPSILON);
assert!(recovered_rate > collapsed_rate);
}
Comment on lines +24 to +60

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: Comparison test uses all-true truth which trivially favors availability recovery

The contract test recovered_availability_stamps_match_known_truth_better_than_system_stand_in (available_clock_contract.rs) constructs truth flags that are all true and recovered flags equal to truth, so recovered_rate is 1.0 while the system stand-in collapses to all-false (rate 0.0). The recovered_rate > collapsed_rate assertion therefore holds trivially and would not distinguish a correct recovery from any implementation that maps AvailableTime to true. This is a weak scientific claim rather than a code defect, but worth noting given AGENTS.md's emphasis on realistic recovery evidence.

Open in Devin Review

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


#[test]
fn empty_or_mismatched_eligibility_payloads_fail_closed() {
assert_eq!(
eligibility_recovery_rate(&[], &[]),
Err(AvailableClockError::InvalidAvailabilityPayload)
);
assert_eq!(
eligibility_recovery_rate(&[true], &[]),
Err(AvailableClockError::InvalidAvailabilityPayload)
);
assert_eq!(
eligibility_recovery_rate(&[true, false], &[true]),
Err(AvailableClockError::InvalidAvailabilityPayload)
);
}
7 changes: 7 additions & 0 deletions crates/available_clock/tests/crate_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//! Integration contract for the `available_clock` package identity.

#[test]
fn package_identity_is_stable() {
let observed = std::hint::black_box(env!("CARGO_PKG_NAME"));
assert_eq!(observed, "available_clock");
}
2 changes: 1 addition & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| immutable source evidence and exact spans | PRD; Architecture; ADR 0008 | `evidence_core`, Task 2 tests/doctoring; `persistence_postgres` source-artifact SQL insert/lookup plus idempotent retry (#40 implemented-main); typed `text_segment` byte-span SQL (active PR) | implemented-main |
| Rust numerical authority / CPU `f64` reference | ADR 0001 | current workspace foundation; future estimators | partial |
| Rust workspace/quality foundation | ADR 0007 | workspace/CI/repository contract | implemented-main |
| six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; PR #5 historical only; `document_clocks` refuse omitted assertion/document time on the active PR | active-PR |
| six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; `available_clock` availability-vs-event/system identity on the active PR | active-PR |
| Allen relation algebra/bounded closure | ADR 0002; temporal research | PR #9 `temporal_core` path-consistency on protected main | implemented-main |
| forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main; `citation_edge` provenance-vs-transition gate on the active PR | active-PR |
| event ontology/evidence mentions | PRD; ADR 0003 | `event_core` mention/instance separation on protected main; `persistence_postgres` mention SQL implemented-main refuses mention-as-instance; event-instance SQL (#39 implemented-main) refuses inverted windows; full intelligence stack remaining | partial |
Expand Down
1 change: 1 addition & 0 deletions docs/adr/0002-six-clock-temporal-semantics.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ADR 0002 — Six-clock temporal semantics and leakage prevention

**Decision status:** Accepted
**Implementation maturity:** active-PR — availability-clock identity in `available_clock` on the active PR; remaining graph/split enforcement stays accepted-target
**Implementation maturity:** active-PR — typed clocks/intervals are implemented-main; `document_clocks` refuses omitted assertion time and document time on this PR; downstream transition/split enforcement remains accepted-target
**Implementation maturity:** active-PR — provenance-vs-transition gate in `citation_edge` on the active PR; remaining graph/split enforcement stays accepted-target
**Implementation maturity:** partial — typed six-clock values and uncertain intervals are implemented-main on protected `main` (merged PR #8 / `temporal_core`); Allen interval algebra and bounded path-consistency are implemented-main on protected `main` (merged PR #9 / `temporal_core`). Superseded PRs #5 and #6 are historical lineage only and are not current-product claims. Downstream estimator, event-intelligence, and remaining persistence-policy uses of these primitives follow their owning ADRs and [`docs/TRACEABILITY.md`](../TRACEABILITY.md).
Expand Down
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio
| ADR | Decision | Decision status | Implementation maturity | Clarification / supersession |
|---|---|---|---|---|
| [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 | active-PR | Availability-clock identity in `available_clock` on the active PR; remaining graph/split enforcement stays accepted-target. |
| [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Typed clocks/intervals are implemented-main; `document_clocks` refuses omitted assertion/document time on the active PR. Later graph/split enforcement remains target work. |
| [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Provenance-vs-transition gate in `citation_edge` on the active PR; remaining graph/split enforcement stays 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 are active-PR (PR #12); full multilevel estimators, graph ontology, and persistence remain accepted-target. ADR 0016 owns event-intelligence tasks. |
Expand Down
28 changes: 28 additions & 0 deletions docs/research/available-clock-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Availability-clock identity (doctoring)

## Scope

`available_clock` keeps availability time distinct from event time and
system time. Recovery is the computed share of availability stamps that
match known truth.

This slice does not persist clocks, replace `temporal_core`, or recreate
`document_clocks`.

## Authority

### Normative TEPP contract

- `docs/adr/0002-six-clock-temporal-semantics.md` — availability time is
the time evidence became usable; it is not event time or system time.
- Historical analyses may not treat record time as the moment evidence
was available.

### Supporting literature

Snodgrass (2000) separates valid time from transaction time. Availability
is a third TEPP clock: when the analyst could use the evidence. Neither
valid time nor transaction time is a substitute.

Snodgrass, R. T. (2000). *Developing time-oriented database applications
in SQL*. Morgan Kaufmann.
2 changes: 2 additions & 0 deletions docs/research/standards-and-literature.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Snodgrass, R. T. (2000). *Developing time-oriented database applications in SQL*

Jensen, C. S., & Snodgrass, R. T. (1996). Semantics of time-varying information. *Information Systems, 21*(4), 311–352. https://doi.org/10.1016/0306-4379(96)00017-8 Valid time versus transaction/system time informs `document_clocks`; assertion time and document time remain additional TEPP clocks and cannot be omitted or replaced by event or system time.

Snodgrass, R. T. (2000). *Developing time-oriented database applications in SQL*. Morgan Kaufmann. Valid vs transaction time informs `available_clock`; availability is a third TEPP clock.

## Privacy lifecycle, retention, and legal hold

European Union. (2016). *Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 on the protection of natural persons with regard to the processing of personal data and on the free movement of such data (General Data Protection Regulation)*. Official Journal of the European Union, L 119, 1–88. https://eur-lex.europa.eu/eli/reg/2016/679/oj
Expand Down
Loading
Loading