From 7f0fa8f8efcb5e3cff1a48a5bbb200e595617d23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 16:49:38 +0900 Subject: [PATCH 1/9] feat(persistence): restack PR #37 onto main after membership 0006 Surgical re-application of pre-#36 tip onto current main. --- CHANGELOG.md | 1 + crates/persistence_postgres/src/error.rs | 7 ++ crates/persistence_postgres/src/lib.rs | 5 + .../src/live_repository.rs | 49 ++++++++ .../persistence_postgres/src/relation_sql.rs | 105 ++++++++++++++++++ .../tests/event_relation_sql_contract.rs | 63 +++++++++++ docs/ERD.md | 1 + docs/TRACEABILITY.md | 1 + ...nce-reproducibility-and-split-authority.md | 1 + .../event-relation-vocabulary-persistence.md | 35 ++++++ 10 files changed, 268 insertions(+) create mode 100644 crates/persistence_postgres/src/relation_sql.rs create mode 100644 crates/persistence_postgres/tests/event_relation_sql_contract.rs create mode 100644 docs/research/event-relation-vocabulary-persistence.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abfea7e7..2cea0ee20 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 +- `persistence_postgres` event-relation SQL contracts: closed ERD transition/provenance vocabulary bound to `transition_edge`, fail-closed unknown types and transition self-loops, live insert of `causes`/`references`. - `persistence_postgres` typed membership assignment (migration `0006`): `entity_record`, `project_record`, and `text_segment` plus exactly-one observed-unit and target constraints that replace the polymorphic `membership_target_id` stub, with SQL insert/lookup, fail-closed inverted-window and backslash-label refusal, and live proof that one document persists two entity memberships and one project membership. - Actions workflow fleet auditor (`scripts/actions_workflow_fleet.py`): paginated registry inventory bound to the exact default-branch SHA/tree, classification of present/orphan/disabled/GitHub-dynamic identities, and fail-closed orphan disable that confirms GitHub's official `disabled_manually` state. - `persistence_postgres` temporal interval ordering migration (`0005`): multi-word CHECK constraints on `document_record`, `event_instance`, and `membership_assignment` that reject inverted valid/system windows and non-positive document revisions while preserving open-ended NULL upper bounds and equal point bounds; catalog validation and live inverted-window proof. diff --git a/crates/persistence_postgres/src/error.rs b/crates/persistence_postgres/src/error.rs index 5b37f4bf1..744d4f348 100644 --- a/crates/persistence_postgres/src/error.rs +++ b/crates/persistence_postgres/src/error.rs @@ -26,6 +26,8 @@ pub enum PersistenceError { LiveAdapterNotConfigured, /// A membership assignment violated exactly-one, weight, window, or label contracts. InvalidMembershipAssignment, + /// An event relation violated the closed ERD transition vocabulary. + InvalidEventRelation, } impl fmt::Display for PersistenceError { @@ -41,6 +43,7 @@ impl fmt::Display for PersistenceError { Self::PoolOptionsInvalid => "pool options invalid", Self::LiveAdapterNotConfigured => "live adapter not configured", Self::InvalidMembershipAssignment => "invalid membership assignment", + Self::InvalidEventRelation => "invalid event relation", }; formatter.write_str(message) } @@ -140,6 +143,10 @@ mod tests { PersistenceError::InvalidMembershipAssignment.to_string(), "invalid membership assignment" ); + assert_eq!( + PersistenceError::InvalidEventRelation.to_string(), + "invalid event relation" + ); assert_eq!( MigrationContractError::SingleWordObjectName.to_string(), "single-word database object name" diff --git a/crates/persistence_postgres/src/lib.rs b/crates/persistence_postgres/src/lib.rs index 3faa22213..6e55756f4 100644 --- a/crates/persistence_postgres/src/lib.rs +++ b/crates/persistence_postgres/src/lib.rs @@ -24,6 +24,7 @@ mod live_pool; mod live_repository; mod manifest_sql; mod membership_sql; +mod relation_sql; mod migration; mod model_run_sql; mod naming; @@ -83,6 +84,10 @@ pub use membership_sql::MembershipAssignmentRecord; pub use membership_sql::insert_membership_assignment_sql; /// Render selection SQL for document-level membership assignments. pub use membership_sql::select_membership_assignments_for_document_sql; +pub use relation_sql::insert_event_relation_sql; +/// Render insert SQL for a validated event relation. +pub use relation_sql::EventRelationRecord; +/// Typed event-relation row bound to the ERD transition vocabulary. /// Embedded and ad-hoc migration catalogs. pub use migration::MigrationCatalog; /// Validate migration SQL against TEPP contracts. diff --git a/crates/persistence_postgres/src/live_repository.rs b/crates/persistence_postgres/src/live_repository.rs index fb62a3f21..8f037b9fe 100644 --- a/crates/persistence_postgres/src/live_repository.rs +++ b/crates/persistence_postgres/src/live_repository.rs @@ -12,6 +12,7 @@ use crate::membership_sql::{ MembershipAssignmentRecord, insert_membership_assignment_sql, select_membership_assignments_for_document_sql, }; +use crate::relation_sql::{EventRelationRecord, insert_event_relation_sql}; use crate::migration::{MigrationCatalog, validate_migration_catalog}; use crate::model_run_sql::{ CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord, @@ -244,6 +245,20 @@ impl LiveDocumentRepository { self.session.execute(&sql) } + /// Insert a typed event relation under the active tenant. + /// + /// # Errors + /// + /// Returns vocabulary/flag validation or transport failures. + pub fn insert_event_relation( + &mut self, + record: &EventRelationRecord, + ) -> Result<(), PersistenceError> { + let sql = insert_event_relation_sql(record)?; + self.session.execute(&sql) + } + + /// Look up a model run by primary key. /// /// # Errors @@ -295,6 +310,7 @@ mod tests { use crate::manifest_sql::ReproducibilityManifestRecord; use crate::migration::MigrationCatalog; use crate::model_run_sql::{CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord}; + use crate::relation_sql::EventRelationRecord; use crate::sql_session::RecordingSqlSession; use crate::{MigrationContractError, PersistenceError}; use temporal_core::{AvailableTime, EventTime, SystemTime}; @@ -411,6 +427,38 @@ mod tests { ); } + fn exercise_event_relation(repo: &mut LiveDocumentRepository) { + let (available, system) = ( + AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"), + SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"), + ); + let relation = EventRelationRecord { + event_relation_id: uuid::Uuid::nil(), + tenant_record_id: uuid::Uuid::nil(), + source_event_id: uuid::Uuid::from_u128(1), + target_event_id: uuid::Uuid::from_u128(2), + relation_type_code: "causes".into(), + transition_edge: true, + system_time: system, + available_time: available, + }; + repo.insert_event_relation(&relation) + .expect("relation insert"); + let mut bad = relation; + bad.transition_edge = false; + assert_eq!( + repo.insert_event_relation(&bad), + Err(PersistenceError::InvalidEventRelation) + ); + assert!( + repo.session() + .executed() + .iter() + .any(|sql| sql.contains("INSERT INTO event_relation")) + ); + } + + #[test] fn live_repository_applies_migrations_and_document_sql() { let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new()); @@ -464,6 +512,7 @@ mod tests { ); exercise_model_run_chain(&mut repo, &manifest); exercise_membership_assignment(&mut repo); + exercise_event_relation(&mut repo); let audit = AuditEvent { audit_event_id: uuid::Uuid::nil(), diff --git a/crates/persistence_postgres/src/relation_sql.rs b/crates/persistence_postgres/src/relation_sql.rs new file mode 100644 index 000000000..bd5710fb9 --- /dev/null +++ b/crates/persistence_postgres/src/relation_sql.rs @@ -0,0 +1,105 @@ +//! SQL contracts for typed event relations (ADR 0013 / ERD relation vocabulary). + +use crate::PersistenceError; +use temporal_core::{AvailableTime, SystemTime}; +use uuid::Uuid; + +const TRANSITION_TYPES: [&str; 8] = [ + "causes", + "enables", + "intervenes_on", + "leads_to", + "produces", + "transitions_to", + "input_to", + "process_to", +]; + +const PROVENANCE_TYPES: [&str; 8] = [ + "references", + "summarizes", + "revises", + "translates", + "retrospectively_reports", + "supports", + "contradicts", + "outcome_of", +]; + +/// One append-only event relation with ERD-bound transition classification. +/// +/// Maps to `event_relation`. `transition_edge` must match the closed +/// transition/provenance vocabulary; unknown types fail closed. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EventRelationRecord { + /// Primary key for this relation identity. + pub event_relation_id: Uuid, + /// Owning tenant boundary. + pub tenant_record_id: Uuid, + /// Source event identity. + pub source_event_id: Uuid, + /// Target event identity. + pub target_event_id: Uuid, + /// Closed ERD relation type code. + pub relation_type_code: String, + /// Whether this row is a forward state-transition edge. + pub transition_edge: bool, + /// System/record time when the relation was asserted. + pub system_time: SystemTime, + /// Availability time of the relation evidence. + pub available_time: AvailableTime, +} + +impl EventRelationRecord { + /// Fail-closed vocabulary, flag, and self-loop validation. + /// + /// # Errors + /// + /// Returns [`PersistenceError::InvalidEventRelation`] when the type is + /// unknown, the transition flag disagrees with the vocabulary, or a + /// transition is a self-loop. + pub fn validate(&self) -> Result<(), PersistenceError> { + let is_transition = TRANSITION_TYPES.contains(&self.relation_type_code.as_str()); + let is_provenance = PROVENANCE_TYPES.contains(&self.relation_type_code.as_str()); + if !is_transition && !is_provenance { + return Err(PersistenceError::InvalidEventRelation); + } + if is_transition != self.transition_edge { + return Err(PersistenceError::InvalidEventRelation); + } + if is_transition && self.source_event_id == self.target_event_id { + return Err(PersistenceError::InvalidEventRelation); + } + Ok(()) + } +} + +/// Render insert SQL for a validated event relation. +/// +/// # Errors +/// +/// Returns [`PersistenceError::InvalidEventRelation`] before any SQL is produced. +pub fn insert_event_relation_sql(record: &EventRelationRecord) -> Result { + record.validate()?; + let flag = if record.transition_edge { + "TRUE" + } else { + "FALSE" + }; + Ok(format!( + "INSERT INTO event_relation (\ + event_relation_id, tenant_record_id, source_event_id, target_event_id, \ + relation_type_code, transition_edge, system_time, available_time\ + ) VALUES (\ + '{relation_id}'::uuid, '{tenant_id}'::uuid, '{source}'::uuid, '{target}'::uuid, \ + '{kind}', {flag}, '{system}'::timestamptz, '{available}'::timestamptz\ + )", + relation_id = record.event_relation_id, + tenant_id = record.tenant_record_id, + source = record.source_event_id, + target = record.target_event_id, + kind = record.relation_type_code, + system = record.system_time.to_rfc3339(), + available = record.available_time.to_rfc3339(), + )) +} diff --git a/crates/persistence_postgres/tests/event_relation_sql_contract.rs b/crates/persistence_postgres/tests/event_relation_sql_contract.rs new file mode 100644 index 000000000..8e9460f49 --- /dev/null +++ b/crates/persistence_postgres/tests/event_relation_sql_contract.rs @@ -0,0 +1,63 @@ +//! Event-relation SQL must bind ERD transition vocabulary to `transition_edge`. + +use persistence_postgres::{EventRelationRecord, PersistenceError, insert_event_relation_sql}; +use temporal_core::{AvailableTime, SystemTime}; +use uuid::Uuid; + +fn clocks() -> (AvailableTime, SystemTime) { + ( + AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("available"), + SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("system"), + ) +} + +fn base_record(relation_type_code: &str, transition_edge: bool) -> EventRelationRecord { + let (available, system) = clocks(); + EventRelationRecord { + event_relation_id: Uuid::nil(), + tenant_record_id: Uuid::nil(), + source_event_id: Uuid::from_u128(1), + target_event_id: Uuid::from_u128(2), + relation_type_code: relation_type_code.into(), + transition_edge, + system_time: system, + available_time: available, + } +} + +#[test] +fn forward_transition_inserts_and_provenance_is_not_a_transition() { + let sql = insert_event_relation_sql(&base_record("causes", true)).expect("causes"); + assert!(sql.contains("INSERT INTO event_relation")); + assert!(sql.contains("transition_edge")); + assert!(sql.contains("TRUE")); + + let sql = insert_event_relation_sql(&base_record("references", false)).expect("references"); + assert!(sql.contains("FALSE")); +} + +#[test] +fn mismatched_transition_flag_and_unknown_type_fail_closed() { + assert_eq!( + insert_event_relation_sql(&base_record("causes", false)), + Err(PersistenceError::InvalidEventRelation) + ); + assert_eq!( + insert_event_relation_sql(&base_record("references", true)), + Err(PersistenceError::InvalidEventRelation) + ); + assert_eq!( + insert_event_relation_sql(&base_record("invented_link", true)), + Err(PersistenceError::InvalidEventRelation) + ); +} + +#[test] +fn transition_self_loop_fails_closed() { + let mut record = base_record("produces", true); + record.target_event_id = record.source_event_id; + assert_eq!( + insert_event_relation_sql(&record), + Err(PersistenceError::InvalidEventRelation) + ); +} diff --git a/docs/ERD.md b/docs/ERD.md index 20e9533f3..9cd02b182 100644 --- a/docs/ERD.md +++ b/docs/ERD.md @@ -4,6 +4,7 @@ **Last reviewed:** 2026-08-13 Protected main implements storage-independent domain objects plus `persistence_postgres` foundation tables (`0001`), tenant row-level security (`0002`), the model-run/artifact chain (`0003`), append-only immutability triggers (`0004`), and temporal interval ordering CHECKs (`0005`) as executable migration contracts with live CI. Migration `0006` (active PR) replaces the polymorphic `membership_target_id` stub with typed exactly-one membership foreign keys and is not implemented-main until exact-head checks, review, and protected-main integration complete. Broader planned ERD entities, concurrent-write acceptance, and backup/recovery gates remain accepted-target. +Protected main implements storage-independent domain objects plus `persistence_postgres` foundation tables (`0001`), tenant row-level security (`0002`), the model-run/artifact chain (`0003`), and append-only immutability triggers (`0004`) as executable migration contracts with live CI. Event-relation SQL insert contracts (active PR) bind the closed transition/provenance vocabulary to `transition_edge` without a new migration number. Broader planned ERD entities, exactly-one membership constraints, concurrent-write acceptance, and backup/recovery gates remain accepted-target until each lands with exact-head evidence. ## Current domain foundation diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 051062ea3..6fad26a1d 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -18,6 +18,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | 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` on active PR); remaining physical ERD/backup remaining | partial | +| 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`), event-relation vocabulary SQL (active PR); remaining physical ERD/backup remaining | 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 contract on protected main; `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 | diff --git a/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md b/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md index 8766e3f6c..0c440d568 100644 --- a/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md +++ b/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md @@ -2,6 +2,7 @@ **Decision status:** Accepted **Implementation maturity:** partial — migration contracts, cutoff eligibility, in-memory bitemporal adapters, live SQL session/migration port, document SQL contracts, `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` open/execute driver, exact-head live PostgreSQL CI, tenant RLS (`tepp_app_runtime` + session GUC), append-only reproducibility-manifest SQL insert/lookup, model-run / model-artifact / corpus-split-manifest chain (migration `0003`), append-only immutability triggers (migration `0004`), and temporal interval ordering CHECK constraints (migration `0005`) implemented-main; typed membership-assignment storage (migration `0006`) implemented on the active PR (not implemented-main); remaining physical ERD (relation transition vocabulary), concurrent write stress, and backup/restore remain accepted-target +**Implementation maturity:** partial — migration contracts, cutoff eligibility, in-memory bitemporal adapters, live SQL session/migration port, document SQL contracts, `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` open/execute driver, exact-head live PostgreSQL CI, tenant RLS (`tepp_app_runtime` + session GUC), append-only reproducibility-manifest SQL insert/lookup, model-run / model-artifact / corpus-split-manifest chain (migration `0003`), append-only immutability triggers (migration `0004`), and temporal interval ordering CHECK constraints (migration `0005`) implemented-main; event-relation vocabulary SQL insert contracts implemented on the active PR (not implemented-main); remaining physical ERD (membership exactly-one FKs, catalog CHECKs), concurrent write stress, and backup/restore remain accepted-target **Date:** 2026-08-12 **Supersedes:** None; complements ADR 0002 (temporal semantics), ADR 0008 (evidence identity), and ADR 0011 (service ownership). diff --git a/docs/research/event-relation-vocabulary-persistence.md b/docs/research/event-relation-vocabulary-persistence.md new file mode 100644 index 000000000..91a057c21 --- /dev/null +++ b/docs/research/event-relation-vocabulary-persistence.md @@ -0,0 +1,35 @@ +# Event-relation vocabulary persistence (doctoring) + +## Scope + +`event_relation` already exists on the foundation schema. This slice adds the +fail-closed SQL insert contract that binds `relation_type_code` to +`transition_edge` using the closed ERD vocabulary. Forward state-transition +types (`causes`, `enables`, `intervenes_on`, `leads_to`, `produces`, +`transitions_to`, `input_to`, `process_to`) must set `transition_edge=true`. +Provenance types (`references`, `summarizes`, `revises`, `translates`, +`retrospectively_reports`, `supports`, `contradicts`, `outcome_of`) must set +`transition_edge=false` and may point backward (Allen, 1983; Jensen & +Snodgrass, 1999). Unknown types and transition self-loops fail closed. + +A later physical CHECK can encode the same vocabulary in the catalog. This +slice does not add a new migration number so it can land independently of +stacked `0005`/`0006` PRs. + +## Authority + +Allen, J. F. (1983). Maintaining knowledge about temporal intervals. +*Communications of the ACM, 26*(11), 832–843. +https://doi.org/10.1145/182.358434 + +Jensen, C. S., & Snodgrass, R. T. (1999). Temporal data management. *IEEE +Transactions on Knowledge and Data Engineering, 11*(1), 36–44. +https://doi.org/10.1109/69.755613 + +## Verification + +- contract tests require matching flags, reject unknown types, and reject + transition self-loops; +- recording-session coverage for insert SQL; +- live PostgreSQL CI inserts one transition and one provenance row when + `TEPP_LIVE_POSTGRES=1`. From 2bd79db876f4a0a99becedd24049a88ad2150318 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 17:18:04 +0900 Subject: [PATCH 2/9] fix(persistence): format relation SQL exports and complete rustdocs Satisfy cargo fmt import ordering and the public docstring gate for the event-relation SQL surface on exact head. --- crates/persistence_postgres/src/lib.rs | 9 +++++---- crates/persistence_postgres/src/live_repository.rs | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/persistence_postgres/src/lib.rs b/crates/persistence_postgres/src/lib.rs index 6e55756f4..56766f702 100644 --- a/crates/persistence_postgres/src/lib.rs +++ b/crates/persistence_postgres/src/lib.rs @@ -24,10 +24,10 @@ mod live_pool; mod live_repository; mod manifest_sql; mod membership_sql; -mod relation_sql; mod migration; mod model_run_sql; mod naming; +mod relation_sql; mod sql_session; mod sqlx_gate; #[cfg(feature = "live-sqlx")] @@ -84,9 +84,6 @@ pub use membership_sql::MembershipAssignmentRecord; pub use membership_sql::insert_membership_assignment_sql; /// Render selection SQL for document-level membership assignments. pub use membership_sql::select_membership_assignments_for_document_sql; -pub use relation_sql::insert_event_relation_sql; -/// Render insert SQL for a validated event relation. -pub use relation_sql::EventRelationRecord; /// Typed event-relation row bound to the ERD transition vocabulary. /// Embedded and ad-hoc migration catalogs. pub use migration::MigrationCatalog; @@ -110,6 +107,10 @@ pub use model_run_sql::select_model_artifacts_by_run_sql; pub use model_run_sql::select_model_run_by_id_sql; /// Multi-word `snake_case` database object naming. pub use naming::is_multi_word_snake_case; +/// Typed event-relation row bound to the ERD transition vocabulary. +pub use relation_sql::EventRelationRecord; +/// Render insert SQL for a validated event relation. +pub use relation_sql::insert_event_relation_sql; /// Recording SQL transport for offline contract tests. pub use sql_session::RecordingSqlSession; /// Live SQL transport contract. diff --git a/crates/persistence_postgres/src/live_repository.rs b/crates/persistence_postgres/src/live_repository.rs index 8f037b9fe..71904a84e 100644 --- a/crates/persistence_postgres/src/live_repository.rs +++ b/crates/persistence_postgres/src/live_repository.rs @@ -12,13 +12,13 @@ use crate::membership_sql::{ MembershipAssignmentRecord, insert_membership_assignment_sql, select_membership_assignments_for_document_sql, }; -use crate::relation_sql::{EventRelationRecord, insert_event_relation_sql}; use crate::migration::{MigrationCatalog, validate_migration_catalog}; use crate::model_run_sql::{ CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord, insert_corpus_split_manifest_sql, insert_model_artifact_sql, insert_model_run_sql, select_model_artifacts_by_run_sql, select_model_run_by_id_sql, }; +use crate::relation_sql::{EventRelationRecord, insert_event_relation_sql}; use crate::sql_session::{SqlSession, apply_sql_batch}; use crate::{MigrationContractError, PersistenceError}; use temporal_core::{EventTime, SystemTime}; @@ -258,7 +258,6 @@ impl LiveDocumentRepository { self.session.execute(&sql) } - /// Look up a model run by primary key. /// /// # Errors @@ -458,7 +457,6 @@ mod tests { ); } - #[test] fn live_repository_applies_migrations_and_document_sql() { let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new()); From 0e0eee95910f99e2a8bd9ccbe1a4138fd00439b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 18:02:21 +0900 Subject: [PATCH 3/9] ci: re-trigger exact-head OpenCode after queue thrash From 3a9a0cbaeb05d797154f223071b1b6e8b6b952cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 18:14:05 +0900 Subject: [PATCH 4/9] =?UTF-8?q?ci:=20serialize=20queue=20=E2=80=94=20re-ru?= =?UTF-8?q?n=20exact-head=20after=20draft=20siblings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 92980496356e765411912616991aafb01b55b992 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 19:28:14 +0900 Subject: [PATCH 5/9] docs(research): separate TEPP relation vocabulary authority from temporal literature Address CodeRabbit review: cite ERD/ADR 0013 as normative for closed relation_type_code and transition_edge rules; keep Allen/Jensen as temporal semantics only; drop unverified live PostgreSQL relation-row claims. --- .../event-relation-vocabulary-persistence.md | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/research/event-relation-vocabulary-persistence.md b/docs/research/event-relation-vocabulary-persistence.md index 91a057c21..8a1b46dde 100644 --- a/docs/research/event-relation-vocabulary-persistence.md +++ b/docs/research/event-relation-vocabulary-persistence.md @@ -9,8 +9,8 @@ types (`causes`, `enables`, `intervenes_on`, `leads_to`, `produces`, `transitions_to`, `input_to`, `process_to`) must set `transition_edge=true`. Provenance types (`references`, `summarizes`, `revises`, `translates`, `retrospectively_reports`, `supports`, `contradicts`, `outcome_of`) must set -`transition_edge=false` and may point backward (Allen, 1983; Jensen & -Snodgrass, 1999). Unknown types and transition self-loops fail closed. +`transition_edge=false` and may point backward in event time. Unknown types and +transition self-loops fail closed. A later physical CHECK can encode the same vocabulary in the catalog. This slice does not add a new migration number so it can land independently of @@ -18,6 +18,25 @@ stacked `0005`/`0006` PRs. ## Authority +### Normative TEPP contract (vocabulary and transition flags) + +The closed `relation_type_code` list, `transition_edge` pairing, forward versus +provenance direction rules, and the prohibition on reverse state transitions are +TEPP product/measurement contracts. Normative sources: + +- `docs/ERD.md` — Typed event-relation contract (forward state-transition types, + evidence/provenance types, `transition_edge` validation, temporal-order + admission before transition-subgraph use). +- `docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md` — + persistence must preserve relation-aware provenance without collapsing + temporal/event semantics; relation components fail closed when inconsistent. + +### Supporting temporal literature (semantics only) + +Allen (1983) and Jensen and Snodgrass (1999) support interval ordering and +bitemporal data-management context. They do **not** define TEPP's closed +relation vocabulary or `transition_edge` mapping. + Allen, J. F. (1983). Maintaining knowledge about temporal intervals. *Communications of the ACM, 26*(11), 832–843. https://doi.org/10.1145/182.358434 @@ -28,8 +47,11 @@ https://doi.org/10.1109/69.755613 ## Verification -- contract tests require matching flags, reject unknown types, and reject - transition self-loops; -- recording-session coverage for insert SQL; -- live PostgreSQL CI inserts one transition and one provenance row when - `TEPP_LIVE_POSTGRES=1`. +- `crates/persistence_postgres/tests/event_relation_sql_contract.rs` requires + matching transition flags, rejects unknown types, and rejects transition + self-loops before SQL is rendered; +- recording-session / unit coverage for `insert_event_relation_sql` and + `EventRelationRecord::validate`; +- no live PostgreSQL insert of transition/provenance rows is claimed in this + slice (live CI remains the shared foundation gate; catalog CHECK and live + relation-row admission are deferred). From ea28010080ea15f8eb0b923bfcbb0d2410e3c9d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 21:29:05 +0900 Subject: [PATCH 6/9] ci: re-run CodeQL Analyze after cancelled job blocks merge From d00b58d7f654467f4d086bf94eba9486505a1d64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 22:34:15 +0900 Subject: [PATCH 7/9] ci: re-trigger OpenCode coverage-evidence for #37 after runner starvation From 16ff9818f484dd59a5973452498255d42f3e71a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 22:40:34 +0900 Subject: [PATCH 8/9] test(persistence): preserve provenance self-loop contract --- .../tests/event_relation_sql_contract.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/persistence_postgres/tests/event_relation_sql_contract.rs b/crates/persistence_postgres/tests/event_relation_sql_contract.rs index 8e9460f49..fcf970f89 100644 --- a/crates/persistence_postgres/tests/event_relation_sql_contract.rs +++ b/crates/persistence_postgres/tests/event_relation_sql_contract.rs @@ -52,6 +52,16 @@ fn mismatched_transition_flag_and_unknown_type_fail_closed() { ); } +#[test] +fn provenance_self_loop_is_allowed() { + let mut record = base_record("references", false); + record.target_event_id = record.source_event_id; + + let sql = insert_event_relation_sql(&record).expect("provenance self-loop must remain valid"); + assert!(sql.contains("references")); + assert!(sql.contains("FALSE")); +} + #[test] fn transition_self_loop_fails_closed() { let mut record = base_record("produces", true); From 5d50129cfb96b1a705b6fbefcf0c8e27f9172ef7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 00:54:17 +0900 Subject: [PATCH 9/9] ci: re-trigger OpenCode after attempt-2 bootstrap starvation