diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6b4abbc9..f0d2d0803 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ on: - ".coveragerc" - ".config/**" - "crates/**" + - "migrations/**" - "scripts/**" - "tests/quality/**" - ".github/workflows/ci.yml" @@ -24,6 +25,7 @@ on: - ".coveragerc" - ".config/**" - "crates/**" + - "migrations/**" - "scripts/**" - "tests/quality/**" - ".github/workflows/ci.yml" @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e2174e2d..0b96e6428 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` 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. diff --git a/crates/persistence_postgres/src/lib.rs b/crates/persistence_postgres/src/lib.rs index 22c2ab1c0..5a1e9a72d 100644 --- a/crates/persistence_postgres/src/lib.rs +++ b/crates/persistence_postgres/src/lib.rs @@ -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; diff --git a/crates/persistence_postgres/tests/live_postgres.rs b/crates/persistence_postgres/tests/live_postgres.rs new file mode 100644 index 000000000..9de49e1de --- /dev/null +++ b/crates/persistence_postgres/tests/live_postgres.rs @@ -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, + 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"); +} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index bc1e5a1b7..4c929e6d6 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -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 | 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 8384c3a93..984c9f9c9 100644 --- a/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md +++ b/docs/adr/0013-bitemporal-persistence-reproducibility-and-split-authority.md @@ -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). diff --git a/docs/research/task-8-live-sql-transport.md b/docs/research/task-8-live-sql-transport.md index 3e4d13da2..01ac137c4 100644 --- a/docs/research/task-8-live-sql-transport.md +++ b/docs/research/task-8-live-sql-transport.md @@ -9,9 +9,10 @@ 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 @@ -19,7 +20,10 @@ Jensen, C. S., & Snodgrass, R. T. (1999). Temporal data management. *IEEE Transa 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). diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 3076fa919..97c0e045d 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -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) diff --git a/tests/quality/test_ci_coverage_diagnostics.py b/tests/quality/test_ci_coverage_diagnostics.py index 8d7f305fd..eaa1b6b87 100644 --- a/tests/quality/test_ci_coverage_diagnostics.py +++ b/tests/quality/test_ci_coverage_diagnostics.py @@ -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()