diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ffe514db6..8793de3be 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 | +| `irregular_time` | event-time lags; equal system spacing is not event spacing | 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f83a9137..958f65d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `irregular_time` event-time lags: consecutive lags are taken from event/valid time, equal system-time spacing cannot stand in for irregular event spacing, and recovered event lags match known truth with lower computed RMSE than an equal-spacing assumption. - `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. - `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011). - `persistence_postgres` audit-event SQL contracts: append-only insert that refuses empty, oversized, or hostile `action_code` values before SQL is rendered. diff --git a/Cargo.lock b/Cargo.lock index 372a55f43..eb498bfc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -595,6 +595,10 @@ dependencies = [ "libc", ] +[[package]] +name = "irregular_time" +version = "0.1.0" + [[package]] name = "itoa" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 925659406..47c60eb0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/irregular_time", ] default-members = [ "crates/evidence_core", @@ -23,6 +24,7 @@ default-members = [ "crates/tepp_simulation", "crates/validation_core", "crates/tepp_api", + "crates/irregular_time", ] [workspace.package] diff --git a/README.md b/README.md index ae74015d3..50ead6307 100644 --- a/README.md +++ b/README.md @@ -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. @@ -22,6 +22,7 @@ crates/corpus_split crates/tepp_simulation crates/validation_core crates/tepp_api +crates/irregular_time ``` ## Local verification diff --git a/crates/irregular_time/Cargo.toml b/crates/irregular_time/Cargo.toml new file mode 100644 index 000000000..84383929b --- /dev/null +++ b/crates/irregular_time/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "irregular_time" +description = "Event-time lags that refuse equal system-time spacing." +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 diff --git a/crates/irregular_time/src/error.rs b/crates/irregular_time/src/error.rs new file mode 100644 index 000000000..cd0619414 --- /dev/null +++ b/crates/irregular_time/src/error.rs @@ -0,0 +1,55 @@ +//! Fail-closed irregular-time errors. + +use std::fmt; + +/// A fail-closed irregular-time error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum IrregularTimeError { + /// Equal system-time spacing was treated as event-time spacing. + SystemSpacingIsNotEventSpacing, + /// Event time did not strictly increase. + NonIncreasingEventTime, + /// Observation or lag slices were empty or length-mismatched. + InvalidObservationPayload, +} + +impl fmt::Display for IrregularTimeError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::SystemSpacingIsNotEventSpacing => { + "equal system spacing is not event-time spacing" + } + Self::NonIncreasingEventTime => "event time is not strictly increasing", + Self::InvalidObservationPayload => "invalid irregular-time payload", + }; + formatter.write_str(message) + } +} + +impl std::error::Error for IrregularTimeError {} + +#[cfg(test)] +mod tests { + use super::IrregularTimeError; + + #[test] + fn error_messages_are_stable() { + for (error, message) in [ + ( + IrregularTimeError::SystemSpacingIsNotEventSpacing, + "equal system spacing is not event-time spacing", + ), + ( + IrregularTimeError::NonIncreasingEventTime, + "event time is not strictly increasing", + ), + ( + IrregularTimeError::InvalidObservationPayload, + "invalid irregular-time payload", + ), + ] { + assert_eq!(error.to_string(), message); + } + } +} diff --git a/crates/irregular_time/src/lib.rs b/crates/irregular_time/src/lib.rs new file mode 100644 index 000000000..bec5b6962 --- /dev/null +++ b/crates/irregular_time/src/lib.rs @@ -0,0 +1,22 @@ +#![forbid(unsafe_code)] +#![deny(missing_docs)] +#![allow(clippy::cast_precision_loss)] +//! Event-time lags that refuse equal system-time spacing. +//! +//! DSEM and other longitudinal estimators must space observations on event +//! time. Equal system-time sampling cannot stand in for irregular event lags +//! (ADR 0002/0005). + +mod error; +mod observation; + +/// Fail-closed irregular-time errors. +pub use error::IrregularTimeError; +/// Dual-clock observation. +pub use observation::ClockedObservation; +/// Consecutive event-time lags. +pub use observation::event_lag_seconds; +/// RMSE of recovered lags against known truth. +pub use observation::lag_root_mean_square_error; +/// Refuse to treat equal system spacing as event spacing. +pub use observation::refuse_equal_system_spacing_as_event_spacing; diff --git a/crates/irregular_time/src/observation.rs b/crates/irregular_time/src/observation.rs new file mode 100644 index 000000000..e196518e8 --- /dev/null +++ b/crates/irregular_time/src/observation.rs @@ -0,0 +1,152 @@ +//! Dual-clock observations and event-time lags. + +use crate::IrregularTimeError; + +/// One observation stamped with event time and system time in whole seconds. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ClockedObservation { + event_time_seconds: i64, + system_time_seconds: i64, +} + +impl ClockedObservation { + /// Construct an observation with distinct TEPP clocks. + /// + /// Sequence checks happen in [`event_lag_seconds`]. + #[must_use] + pub const fn new(event_time_seconds: i64, system_time_seconds: i64) -> Self { + Self { + event_time_seconds, + system_time_seconds, + } + } + + /// Event/valid time in seconds. + #[must_use] + pub const fn event_time_seconds(self) -> i64 { + self.event_time_seconds + } + + /// System/record time in seconds. + #[must_use] + pub const fn system_time_seconds(self) -> i64 { + self.system_time_seconds + } +} + +/// Consecutive event-time lags in seconds. +/// +/// # Errors +/// +/// Returns [`IrregularTimeError::InvalidObservationPayload`] when fewer than +/// two observations are supplied, or +/// [`IrregularTimeError::NonIncreasingEventTime`] when event time does not +/// strictly increase. +pub fn event_lag_seconds( + observations: &[ClockedObservation], +) -> Result, IrregularTimeError> { + if observations.len() < 2 { + return Err(IrregularTimeError::InvalidObservationPayload); + } + let mut lags = Vec::with_capacity(observations.len() - 1); + for window in observations.windows(2) { + let delta = window[1].event_time_seconds - window[0].event_time_seconds; + if delta <= 0 { + return Err(IrregularTimeError::NonIncreasingEventTime); + } + lags.push(delta); + } + Ok(lags) +} + +/// Refuse to treat equal system-time spacing as event-time spacing. +/// +/// # Errors +/// +/// Returns lag-construction errors, or +/// [`IrregularTimeError::SystemSpacingIsNotEventSpacing`] when system-time +/// deltas are constant while event-time deltas are not. +pub fn refuse_equal_system_spacing_as_event_spacing( + observations: &[ClockedObservation], +) -> Result<(), IrregularTimeError> { + let event_lags = event_lag_seconds(observations)?; + let mut system_lags = Vec::with_capacity(event_lags.len()); + for window in observations.windows(2) { + system_lags.push(window[1].system_time_seconds - window[0].system_time_seconds); + } + let system_constant = system_lags.windows(2).all(|pair| pair[0] == pair[1]); + let event_varies = event_lags.windows(2).any(|pair| pair[0] != pair[1]); + if system_constant && event_varies { + return Err(IrregularTimeError::SystemSpacingIsNotEventSpacing); + } + Ok(()) +} + +/// RMSE of recovered lags against known-truth event lags. +/// +/// # Errors +/// +/// Returns [`IrregularTimeError::InvalidObservationPayload`] when either slice +/// is empty or the lengths differ. +pub fn lag_root_mean_square_error( + truth: &[i64], + decided: &[i64], +) -> Result { + if truth.is_empty() || truth.len() != decided.len() { + return Err(IrregularTimeError::InvalidObservationPayload); + } + let mut sum_squares = 0.0_f64; + for (truth_lag, decided_lag) in truth.iter().zip(decided) { + let residual = *decided_lag as f64 - *truth_lag as f64; + sum_squares += residual * residual; + } + Ok((sum_squares / truth.len() as f64).sqrt()) +} + +#[cfg(test)] +mod tests { + use super::{ + ClockedObservation, event_lag_seconds, lag_root_mean_square_error, + refuse_equal_system_spacing_as_event_spacing, + }; + use crate::IrregularTimeError; + + #[test] + fn matching_clocks_and_empty_rmse_cover_local_branches() { + let regular = [ + ClockedObservation::new(0, 0), + ClockedObservation::new(2, 2), + ClockedObservation::new(4, 4), + ]; + refuse_equal_system_spacing_as_event_spacing(®ular).expect("matching clocks"); + assert_eq!(event_lag_seconds(®ular).expect("lags"), vec![2, 2]); + assert_eq!(regular[0].event_time_seconds(), 0); + assert_eq!(regular[0].system_time_seconds(), 0); + assert_eq!( + lag_root_mean_square_error(&[], &[]), + Err(IrregularTimeError::InvalidObservationPayload) + ); + assert_eq!( + lag_root_mean_square_error(&[2, 2], &[]), + Err(IrregularTimeError::InvalidObservationPayload) + ); + let matched = lag_root_mean_square_error(&[2, 2], &[2, 2]).expect("rmse"); + assert!(matched.abs() < f64::EPSILON); + let irregular_system = [ + ClockedObservation::new(0, 0), + ClockedObservation::new(10, 1), + ClockedObservation::new(13, 5), + ]; + refuse_equal_system_spacing_as_event_spacing(&irregular_system) + .expect("non-constant system lags are not equal system spacing"); + let equal_system_irregular_event = [ + ClockedObservation::new(0, 0), + ClockedObservation::new(10, 1), + ClockedObservation::new(13, 2), + ]; + assert_eq!( + refuse_equal_system_spacing_as_event_spacing(&equal_system_irregular_event), + Err(IrregularTimeError::SystemSpacingIsNotEventSpacing) + ); + } +} diff --git a/crates/irregular_time/tests/crate_contract.rs b/crates/irregular_time/tests/crate_contract.rs new file mode 100644 index 000000000..306a39242 --- /dev/null +++ b/crates/irregular_time/tests/crate_contract.rs @@ -0,0 +1,7 @@ +//! Integration contract for the `irregular_time` package identity. + +#[test] +fn package_identity_is_stable() { + let observed = std::hint::black_box(env!("CARGO_PKG_NAME")); + assert_eq!(observed, "irregular_time"); +} diff --git a/crates/irregular_time/tests/event_spacing_contract.rs b/crates/irregular_time/tests/event_spacing_contract.rs new file mode 100644 index 000000000..37c2a8044 --- /dev/null +++ b/crates/irregular_time/tests/event_spacing_contract.rs @@ -0,0 +1,87 @@ +//! Event-time lags cannot be replaced by equal system-time spacing. + +use irregular_time::{ + ClockedObservation, IrregularTimeError, event_lag_seconds, lag_root_mean_square_error, + refuse_equal_system_spacing_as_event_spacing, +}; + +fn observation(event: i64, system: i64) -> ClockedObservation { + ClockedObservation::new(event, system) +} + +#[test] +fn equal_system_spacing_cannot_replace_irregular_event_lags() { + let observations = [observation(0, 0), observation(10, 1), observation(13, 2)]; + assert_eq!( + refuse_equal_system_spacing_as_event_spacing(&observations), + Err(IrregularTimeError::SystemSpacingIsNotEventSpacing) + ); + let lags = event_lag_seconds(&observations).expect("event lags"); + assert_eq!(lags, vec![10, 3]); +} + +#[test] +fn event_lags_recover_known_truth_better_than_equal_system_spacing() { + let truth_lags = [10_i64, 3]; + let observations = [observation(0, 0), observation(10, 1), observation(13, 2)]; + let event_lags = event_lag_seconds(&observations).expect("event"); + let assumed_equal = [1_i64, 1]; + let event_rmse = lag_root_mean_square_error(&truth_lags, &event_lags).expect("event rmse"); + let assumed_rmse = lag_root_mean_square_error(&truth_lags, &assumed_equal).expect("assumed"); + let expected = { + let mut sum_squares = 0.0_f64; + for (truth, decided) in truth_lags.iter().zip(event_lags.iter()) { + let residual = f64::from(i32::try_from(*decided).expect("decided")) + - f64::from(i32::try_from(*truth).expect("truth")); + sum_squares += residual * residual; + } + (sum_squares / f64::from(u32::try_from(truth_lags.len()).expect("len"))).sqrt() + }; + assert!((event_rmse - expected).abs() < f64::EPSILON); + assert!(event_rmse < assumed_rmse); +} + +#[test] +fn empty_or_non_increasing_event_clocks_fail_closed() { + assert_eq!( + event_lag_seconds(&[]), + Err(IrregularTimeError::InvalidObservationPayload) + ); + assert_eq!( + event_lag_seconds(&[observation(5, 0), observation(4, 1)]), + Err(IrregularTimeError::NonIncreasingEventTime) + ); +} + +#[test] +fn matching_event_and_system_spacing_is_not_refused() { + let observations = [observation(0, 0), observation(2, 2), observation(4, 4)]; + refuse_equal_system_spacing_as_event_spacing(&observations).expect("matching clocks"); +} + +#[test] +fn non_constant_system_lags_are_not_treated_as_equal_system_spacing() { + let observations = [observation(0, 0), observation(10, 1), observation(13, 5)]; + refuse_equal_system_spacing_as_event_spacing(&observations) + .expect("varying system lags are not equal system spacing"); + assert_eq!( + event_lag_seconds(&observations).expect("event lags"), + vec![10, 3] + ); +} + +#[test] +fn lag_rmse_rejects_empty_or_nonempty_mismatched_lengths() { + assert_eq!( + lag_root_mean_square_error(&[], &[]), + Err(IrregularTimeError::InvalidObservationPayload) + ); + assert_eq!( + lag_root_mean_square_error(&[10, 3], &[]), + Err(IrregularTimeError::InvalidObservationPayload) + ); + assert_eq!( + lag_root_mean_square_error(&[10, 3], &[10]), + Err(IrregularTimeError::InvalidObservationPayload) + ); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index afada87ae..3053426a1 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -28,7 +28,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | report template/section/copied/style/modality method effects | ADR 0004/0012; PRD/TRD | simulation truth factors implemented; estimator-side method model remains future | partial | | candidate K statistical/Pareto gates + blinded LLM review | ADR 0012; research | future `model_selection` | accepted-target | | compositional topic correlation / stable clustering | ADR 0005/0012; research | future `network_analysis` | accepted-target | -| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | future `psychometric_core` | accepted-target | +| posterior ESEM / longitudinal invariance / DSEM | ADR 0005 | `irregular_time` event-lag spacing on the active PR; remaining ESEM/DSEM fit remains accepted-target | active-PR | | CPU bounded multithreading + GPU/VRAM streaming/parity | ADR 0001/0006 | future `compute_backend` | accepted-target | | TDT detection/tracking vs CHRONOS schema/prediction/temporal consistency | ADR 0016; PRD/research | future `event_intelligence` | accepted-target | | evidence-bounded LLM interpretation | ADR 0010/0012; PRD | future `interpretation_gateway` | accepted-target | diff --git a/docs/adr/0005-posterior-esem-dsem.md b/docs/adr/0005-posterior-esem-dsem.md index 09e5b0ce5..0f68e296b 100644 --- a/docs/adr/0005-posterior-esem-dsem.md +++ b/docs/adr/0005-posterior-esem-dsem.md @@ -1,7 +1,7 @@ # ADR 0005 — Posterior-aware ESEM/DSEM and structural interpretation **Decision status:** Accepted -**Implementation maturity:** accepted-target +**Implementation maturity:** active-PR — `irregular_time` spaces lags on event time and refuses equal system-time spacing; remaining ESEM/DSEM fit remains accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0012 governs upstream topic measurement/network coordinates; this ADR governs higher-order psychometric structure and longitudinal interpretation. diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b315..d614628ca 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,7 +10,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0002](0002-six-clock-temporal-semantics.md) | Six-clock temporal semantics and fail-closed historical leakage prevention | Accepted | active-PR | Unmerged PR #8 is the canonical Task 3 replacement implementing typed clocks/intervals against the current protected-main lineage; conflicted PR #5 is superseded lineage. Later graph/split enforcement remains target work. | | [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. | | [0004](0004-shared-multilingual-latent-space.md) | One shared multilingual latent space with explicit invariance status | Accepted | accepted-target | ADR 0012 owns the full topic-estimator/backend/global-topic contract. | -| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | accepted-target | Downstream psychometric authority; upstream topic/network model is clarified by ADR 0012. | +| [0005](0005-posterior-esem-dsem.md) | Posterior-aware ESEM/DSEM and valid compositional coordinates | Accepted | active-PR | Event-time lag spacing in `irregular_time` on the active PR; remaining ESEM/DSEM fit remains accepted-target. | | [0006](0006-vram-gpu-nvidia-orchestration.md) | VRAM-adaptive GPU compute and model-credential boundary | Accepted | accepted-target | LLM orchestration policy superseded by ADR 0010; autonomous development authority governed by ADR 0015. | | [0007](0007-rust-workspace-quality-gates.md) | Explicit Rust workspace, pinned toolchains, and exact quality gates | Accepted | implemented-main | ADR 0014 governs scientific/product claim promotion beyond repository-quality tooling. | | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | diff --git a/docs/research/irregular-event-time-spacing.md b/docs/research/irregular-event-time-spacing.md new file mode 100644 index 000000000..69a2fec4c --- /dev/null +++ b/docs/research/irregular-event-time-spacing.md @@ -0,0 +1,29 @@ +# Irregular event-time spacing (doctoring) + +## Scope + +`irregular_time` computes consecutive lags from event/valid time. Equal +system-time sampling cannot stand in for irregular event lags. Recovery is the +computed RMSE of recovered lags against known-truth event lags. + +This slice does not fit DSEM, claim a unique lag kernel, or collapse the six +TEPP clocks. + +## Authority + +### Normative TEPP contract + +- `docs/adr/0002-six-clock-temporal-semantics.md` — event/valid time is + distinct from system/record time. +- `docs/adr/0005-posterior-esem-dsem.md` — longitudinal models must handle + irregular time rather than assume equally spaced system samples. + +### Supporting literature + +Asparouhov, Hamaker, and Muthén (2018) formulate DSEM for intensive +longitudinal data whose observation times need not be equally spaced. They do +**not** authorize substituting system-time cadence for event-time lags. + +Asparouhov, T., Hamaker, E. L., & Muthén, B. (2018). Dynamic structural +equation models. *Structural Equation Modeling: A Multidisciplinary Journal, +25*(3), 359–388. https://doi.org/10.1080/10705511.2017.1406803 diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b4b144684..7221edf4c 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -8,6 +8,8 @@ American Educational Research Association, American Psychological Association, & Asparouhov, T., Hamaker, E. L., & Muthén, B. (2018). Dynamic structural equation models. *Structural Equation Modeling: A Multidisciplinary Journal, 25*(3), 359–388. https://doi.org/10.1080/10705511.2017.1406803 +`irregular_time` uses this source for irregular observation times: lags are event-time differences, not equal system-time steps. + Asparouhov, T., & Muthén, B. (2009). Exploratory structural equation modeling. *Structural Equation Modeling: A Multidisciplinary Journal, 16*(3), 397–438. https://doi.org/10.1080/10705510903008204 Marsh, H. W., Morin, A. J. S., Parker, P. D., & Kaur, G. (2014). Exploratory structural equation modeling: An integration of the best features of exploratory and confirmatory factor analysis. *Annual Review of Clinical Psychology, 10*, 85–110. https://doi.org/10.1146/annurev-clinpsy-032813-153700 diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index e367a798f..6620d0d6e 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -23,6 +23,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Truth corpora / manifests | `tepp_simulation` | implemented-main | — | deterministic generator tests | Task 10 / PR #18 | | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | naruon HTTP interchange | unknown-field/version/limit + naruon HTTPS interchange tests | Task 12 / PR #21; live HTTP service remaining | +| Irregular event-time lags | `irregular_time` | active-PR | this PR | event-lag RMSE vs equal system spacing | ADR 0002/0005 | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | diff --git a/scripts/check_workspace_contract.py b/scripts/check_workspace_contract.py index c7b1ecf58..7557f9b27 100644 --- a/scripts/check_workspace_contract.py +++ b/scripts/check_workspace_contract.py @@ -23,6 +23,7 @@ "tepp_simulation", "validation_core", "tepp_api", + "irregular_time", ) REQUIRED_CI_SNIPPETS: tuple[str, ...] = (