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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- ".coveragerc"
- ".config/**"
- "crates/**"
- "migrations/**"
- "scripts/**"
- "tests/quality/**"
- ".github/workflows/ci.yml"
Expand All @@ -24,6 +25,7 @@ on:
- ".coveragerc"
- ".config/**"
- "crates/**"
- "migrations/**"
- "scripts/**"
- "tests/quality/**"
- ".github/workflows/ci.yml"
Expand Down Expand Up @@ -249,3 +251,34 @@ jobs:
path: coverage-branches.json
if-no-files-found: error
retention-days: 1

live-postgres:
name: Live PostgreSQL integration
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16.9-alpine
env:
POSTGRES_USER: tepp
POSTGRES_PASSWORD: tepp_ci
POSTGRES_DB: tepp
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U tepp -d tepp"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
TEPP_LIVE_POSTGRES: "1"
DATABASE_URL: postgres://tepp:tepp_ci@localhost:5432/tepp
steps:
- name: Checkout exact head
uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87
with:
persist-credentials: false
- name: Install pinned Rust toolchain
run: rustup toolchain install 1.97.1 --profile minimal
- name: Run live-sqlx PostgreSQL integration
run: cargo test -p persistence_postgres --features live-sqlx --test live_postgres -- --nocapture
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

- `persistence_postgres` live PostgreSQL CI: `live-postgres` job with Postgres 16 service, `TEPP_LIVE_POSTGRES=1` gate, and integration coverage for pool open, foundation migrations, document insert/revise/as-of, and audit SQL.
- Repository release evidence tooling: `scripts/release_evidence.py` generates CycloneDX 1.5 SBOM, exact-head provenance, and SHA-256 checksums from `Cargo.lock`/`Cargo.toml`, with fail-closed validation and CI generation on every quality gate.
- `persistence_postgres` `live-sqlx` feature: real `SQLx`/`PgPool` open/execute behind validated `DATABASE_URL` and `LiveSqlxPoolOptions`, with offline/live executor backends and CI coverage exclusion for the transport module.
- `persistence_postgres` live pool open gate: validated `LiveSqlxPoolOptions`, fail-closed `open_live_sqlx_pool` / `LiveSqlxPool` (`SqlSession`) with offline test backend; optional `live-sqlx` attaches real `SQLx`/`PgPool` after `DATABASE_URL` validation.
Expand Down
2 changes: 1 addition & 1 deletion crates/persistence_postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! in-memory bitemporal adapters, live SQL session/migration ports, document
//! SQL contracts, a fail-closed `DATABASE_URL` gate, and a fail-closed live
//! pool open path with validated sizing options (ADR 0013). In-process
//! transports keep CI deterministic; optional `live-sqlx` feature compiles a real `PgPool` driver behind validated URL/options.
//! transports keep default CI deterministic; optional `live-sqlx` feature compiles a real `PgPool` driver behind validated URL/options, with a gated live `PostgreSQL` CI job (`TEPP_LIVE_POSTGRES=1`).

mod cutoff;
mod document_sql;
Expand Down
146 changes: 146 additions & 0 deletions crates/persistence_postgres/tests/live_postgres.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Live `PostgreSQL` integration for the optional `live-sqlx` driver.
//!
//! Default and offline CI stay free of a database process. Exact-head live
//! evidence is produced only when `TEPP_LIVE_POSTGRES=1` and a validated
//! `DATABASE_URL` point at a reachable server (see the `live-postgres` CI job).

#![cfg(feature = "live-sqlx")]

use persistence_postgres::{
AuditEvent, DocumentRecord, LiveDocumentRepository, LiveSqlxPoolOptions, MigrationCatalog,
SqlSession, apply_sql_batch, open_live_sqlx_pool, require_live_sqlx_config,
};
use temporal_core::{AvailableTime, EventTime, SystemTime};
use uuid::Uuid;

const LIVE_GATE_ENV: &str = "TEPP_LIVE_POSTGRES";

fn live_postgres_requested() -> bool {
match std::env::var(LIVE_GATE_ENV) {
Ok(value) => value == "1",
Err(_) => false,
}
}

fn sample_times() -> (AvailableTime, EventTime, SystemTime) {
(
AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("available"),
EventTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("valid"),
SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("system"),
)
}

fn seed_tenant_and_artifact(
repo: &mut LiveDocumentRepository<persistence_postgres::LiveSqlxPool>,
tenant_record_id: Uuid,
source_artifact_id: Uuid,
content_digest: &str,
) {
let (available, _valid, system) = sample_times();
let tenant_sql = format!(
"INSERT INTO tenant_record (tenant_record_id, tenant_status_code, system_time) \
VALUES ('{tenant_record_id}'::uuid, 'active', '{system}'::timestamptz)",
system = system.to_rfc3339(),
);
repo.session_mut()
.execute(&tenant_sql)
.expect("insert tenant_record");

let artifact_sql = format!(
"INSERT INTO source_artifact (\
source_artifact_id, tenant_record_id, content_sha256, source_size_bytes, \
media_type_code, protected_object_ref, system_time, available_time\
) VALUES (\
'{source_artifact_id}'::uuid, '{tenant_record_id}'::uuid, '{content_digest}', 4, \
'text/plain', NULL, '{system}'::timestamptz, '{available}'::timestamptz\
)",
system = system.to_rfc3339(),
available = available.to_rfc3339(),
);
repo.session_mut()
.execute(&artifact_sql)
.expect("insert source_artifact");
}

#[test]
fn live_postgres_applies_migrations_and_document_sql() {
if !live_postgres_requested() {
// Offline default CI and local unit lanes stay database-free.
return;
}

let config = require_live_sqlx_config().expect(
"DATABASE_URL must be set and valid when TEPP_LIVE_POSTGRES=1 (live Postgres CI gate)",
);
let options = LiveSqlxPoolOptions::new(2, 5_000).expect("pool options");
let pool = open_live_sqlx_pool(&config, options)
.expect("live-sqlx pool must open against the CI PostgreSQL service");
assert!(pool.is_live());

let mut repo = LiveDocumentRepository::new(pool);
repo.session_mut()
.execute("SELECT 1")
.expect("SELECT 1 through live transport");

let catalog = MigrationCatalog::from_embedded().expect("embedded foundation catalog");
// Re-run safe: down is IF EXISTS, then apply the authoritative up contract.
apply_sql_batch(repo.session_mut(), catalog.down_sql())
.expect("foundation down migration must apply (IF EXISTS)");
let applied = repo
.apply_migrations(&catalog)
.expect("foundation migrations must apply on live PostgreSQL");
assert!(applied >= 1);

let tenant_record_id = Uuid::now_v7();
let document_record_id = Uuid::now_v7();
// document_sql contracts bind source_artifact_id to the document identity.
let source_artifact_id = document_record_id;
let content_digest = "ab".repeat(32);
seed_tenant_and_artifact(
&mut repo,
tenant_record_id,
source_artifact_id,
&content_digest,
);

let (available, valid, system) = sample_times();
let record = DocumentRecord {
document_record_id,
tenant_record_id,
content_digest: content_digest.clone(),
available_time: available,
valid_from: valid,
valid_to: None,
system_from: system,
system_to: None,
revision_number: 1,
};
repo.insert(&record).expect("insert document_record");

let mut revised = record.clone();
revised.revision_number = 2;
revised.system_from = SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("later system");
revised.content_digest = "cd".repeat(32);
repo.revise(&revised).expect("revise document_record");

repo.submit_as_known_at(
document_record_id,
&SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("known_at"),
)
.expect("as-known-at select");
repo.submit_as_valid_at(
document_record_id,
&EventTime::parse_rfc3339("2026-01-15T00:00:00Z").expect("valid_at"),
&SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("known_at"),
)
.expect("as-valid-at select");

let audit = AuditEvent {
audit_event_id: Uuid::now_v7(),
tenant_record_id,
action_code: "live_postgres_ci".into(),
subject_record_id: document_record_id,
recorded_system_time: SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("audit"),
};
repo.append_audit(&audit).expect("append audit_event");
}
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, `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` driver; full physical ERD/RLS/live CI PG remaining | partial |
| PostgreSQL bitemporal/lineage persistence | ADR 0013; Architecture/ERD | `persistence_postgres` migration contracts, in-memory adapters, live SQL session/document SQL port, `DATABASE_URL` SQLx gate, optional `live-sqlx` `PgPool` driver, exact-head live PostgreSQL CI; full physical ERD/RLS 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/model-run artifact chain remaining | partial |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR 0013 — Bitemporal persistence, reproducibility manifests, and split authority

**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, and optional `live-sqlx` `PgPool` open/execute driver implemented; full physical ERD, RLS, concurrent write stress, backup/restore, and live-Postgres CI 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, and exact-head live PostgreSQL CI integration implemented; full physical ERD, RLS, 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).

Expand Down
10 changes: 7 additions & 3 deletions docs/research/task-8-live-sql-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ Extends Task 8 / ADR 0013 with:
3. parameterized document/audit SQL rendering for bitemporal tables;
4. `LiveDocumentRepository` over any `SqlSession`;
5. fail-closed `DATABASE_URL` configuration gate for `SQLx` pool wiring;
6. optional `live-sqlx` feature compiling a real `SQLx`/`PgPool` open/execute driver behind validated URL and pool options.
6. optional `live-sqlx` feature compiling a real `SQLx`/`PgPool` open/execute driver behind validated URL and pool options;
7. exact-head live PostgreSQL CI (`live-postgres` job) that opens the pool, applies foundation migrations, and exercises document insert/revise/as-of/audit SQL when `TEPP_LIVE_POSTGRES=1`.

A live PostgreSQL process is not required in default CI. Offline/`RecordingSqlSession` backends keep deterministic tests; `live-sqlx` fails closed without a reachable server. Full physical ERD, RLS, concurrent write stress, backup/restore, and live-Postgres CI remain follow-ons.
Offline/`RecordingSqlSession` backends keep deterministic default CI free of a database process; `live-sqlx` fails closed without a reachable server. Full physical ERD, RLS, concurrent write stress, and backup/restore remain follow-ons.

## Authority

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

ISO/IEC. (2011). *ISO/IEC 9075-2:2011 Information technology — Database languages — SQL — Part 2: Foundation (SQL/Foundation)*. International Organization for Standardization.

PostgreSQL Global Development Group. (2024). *PostgreSQL 16.9 documentation*. https://www.postgresql.org/docs/16/

## Verification

- unit tests for URL validation, empty batches, statement splitting, recording sessions, migration apply, insert/revise/audit SQL, and digest fail-closed paths;
- workspace line/branch coverage must remain complete.
- live integration (`tests/live_postgres.rs`) gated by `TEPP_LIVE_POSTGRES=1` plus validated `DATABASE_URL`, required in the `live-postgres` CI service job;
- workspace line/branch coverage must remain complete (`sqlx_live.rs` remains ignored for authored LLVM coverage until broader live success-path instrumentation lands).
5 changes: 3 additions & 2 deletions docs/validation/temporal-event-foundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ This report tracks exact-head scientific and engineering evidence required befor
| Event mention/instance | `event_core` | partial | — | unit + fail-closed promotion | Task 5 / PR #13 |
| Multiple membership | `membership_core` | partial | — | unit + ESS weights | Task 7 / PR #12 + #25 |
| Forward transition DAG | `relation_graph` | implemented-main | — | unit + cycle rejection | Task 6 / PR #14 |
| Bitemporal persistence + live SQL port | `persistence_postgres` | partial | | migration contracts + recording transport + optional PgPool | Task 8 / PR #16 + #23 + #26 + #27 |
| Bitemporal persistence + live SQL port | `persistence_postgres` | partial | active-PR (PR #29 live PG CI) | migration contracts + recording transport + optional PgPool + live CI service | Task 8 / PR #16 + #23 + #26 + #27 + #29 |
| Leakage-safe splits | `corpus_split` | implemented-main | — | cutoff + co-partition tests | Task 9 / PR #17 |
| 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 | — | unknown-field/version/limit tests | Task 12 / PR #21; HTTP service remaining |
| 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 | active-PR (PR #28) | generate+validate in CI | Task 13 partial |
| Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 |


## Scientific acceptance checklist (foundation)

Expand Down
16 changes: 16 additions & 0 deletions tests/quality/test_ci_coverage_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ def test_line_gate_uses_lcov_authored_lines_and_keeps_region_evidence(self) -> N
self.assertIn("UNCOVERED_REGION", workflow)
self.assertIn("UNCOVERED_FUNCTION", workflow)

def test_live_postgres_job_is_gated_and_service_backed(self) -> None:
"""Live SQLx evidence requires a Postgres service and explicit env gate."""

workflow = CI_WORKFLOW.read_text(encoding="utf-8")

self.assertIn("live-postgres:", workflow)
self.assertIn("name: Live PostgreSQL integration", workflow)
self.assertIn("image: postgres:16.9-alpine", workflow)
self.assertIn('TEPP_LIVE_POSTGRES: "1"', workflow)
self.assertIn("DATABASE_URL: postgres://tepp:tepp_ci@localhost:5432/tepp", workflow)
self.assertIn(
"cargo test -p persistence_postgres --features live-sqlx --test live_postgres",
workflow,
)
self.assertIn("migrations/**", workflow)


if __name__ == "__main__": # pragma: no cover
unittest.main()
Loading