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 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` 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.
- `persistence_postgres` append-only immutability migration (`0004`): `reject_append_only_mutation`, statement-level `BEFORE UPDATE OR DELETE OR TRUNCATE` triggers on identity/manifest tables, `REVOKE UPDATE`/`DELETE`/`TRUNCATE` from `tepp_app_runtime`, executable DDL/rollback contracts, and live representative mutation proof.
Expand Down
7 changes: 7 additions & 0 deletions crates/persistence_postgres/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum PersistenceError {
PoolOptionsInvalid,
/// Live `SQLx` wiring was requested without a configured transport URL.
LiveAdapterNotConfigured,
/// A membership assignment violated exactly-one, weight, window, or label contracts.
InvalidMembershipAssignment,
}

impl fmt::Display for PersistenceError {
Expand All @@ -38,6 +40,7 @@ impl fmt::Display for PersistenceError {
Self::DatabaseUrlInvalid => "database url invalid",
Self::PoolOptionsInvalid => "pool options invalid",
Self::LiveAdapterNotConfigured => "live adapter not configured",
Self::InvalidMembershipAssignment => "invalid membership assignment",
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -133,6 +136,10 @@ mod tests {
PersistenceError::LiveAdapterNotConfigured.to_string(),
"live adapter not configured"
);
assert_eq!(
PersistenceError::InvalidMembershipAssignment.to_string(),
"invalid membership assignment"
);
assert_eq!(
MigrationContractError::SingleWordObjectName.to_string(),
"single-word database object name"
Expand Down
11 changes: 10 additions & 1 deletion crates/persistence_postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
//! CI job (`TEPP_LIVE_POSTGRES=1`). Append-only reproducibility-manifest SQL
//! contracts bind evidence digests, code commit, dependency lock, and knowledge
//! cutoff for run provenance (ADR 0013). Model-run and model-artifact SQL
//! contracts chain immutable run identities to those manifests.
//! contracts chain immutable run identities to those manifests. Typed
//! membership-assignment SQL (migration `0006`) replaces the polymorphic 0001 stub so documents
//! can belong to multiple entities and projects without atomistic collapse.

mod cutoff;
mod document_sql;
Expand All @@ -21,6 +23,7 @@ mod error;
mod live_pool;
mod live_repository;
mod manifest_sql;
mod membership_sql;
mod migration;
mod model_run_sql;
mod naming;
Expand Down Expand Up @@ -74,6 +77,12 @@ pub use manifest_sql::insert_reproducibility_manifest_sql;
pub use manifest_sql::select_reproducibility_manifest_by_digests_sql;
/// Render selection SQL by primary key.
pub use manifest_sql::select_reproducibility_manifest_by_id_sql;
/// Typed membership assignment row.
pub use membership_sql::MembershipAssignmentRecord;
/// Render insert SQL for a typed membership assignment.
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;
/// Embedded and ad-hoc migration catalogs.
pub use migration::MigrationCatalog;
/// Validate migration SQL against TEPP contracts.
Expand Down
69 changes: 69 additions & 0 deletions crates/persistence_postgres/src/live_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use crate::manifest_sql::{
ReproducibilityManifestRecord, insert_reproducibility_manifest_sql,
select_reproducibility_manifest_by_digests_sql, select_reproducibility_manifest_by_id_sql,
};
use crate::membership_sql::{
MembershipAssignmentRecord, insert_membership_assignment_sql,
select_membership_assignments_for_document_sql,
};
use crate::migration::{MigrationCatalog, validate_migration_catalog};
use crate::model_run_sql::{
CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord,
Expand Down Expand Up @@ -214,6 +218,32 @@ impl<S: SqlSession> LiveDocumentRepository<S> {
self.session.execute(&sql)
}

/// Insert a typed membership assignment under the active tenant.
///
/// # Errors
///
/// Returns exactly-one/weight validation or transport failures.
pub fn insert_membership_assignment(
&mut self,
record: &MembershipAssignmentRecord,
) -> Result<(), PersistenceError> {
let sql = insert_membership_assignment_sql(record)?;
self.session.execute(&sql)
}

/// Look up document-level membership assignments by document identity.
///
/// # Errors
///
/// Returns transport failures.
pub fn submit_membership_assignments_for_document(
&mut self,
document_record_id: Uuid,
) -> Result<(), PersistenceError> {
let sql = select_membership_assignments_for_document_sql(document_record_id);
self.session.execute(&sql)
}

/// Look up a model run by primary key.
///
/// # Errors
Expand Down Expand Up @@ -343,6 +373,44 @@ mod tests {
);
}

fn sample_membership() -> crate::MembershipAssignmentRecord {
crate::MembershipAssignmentRecord {
membership_assignment_id: uuid::Uuid::nil(),
tenant_record_id: uuid::Uuid::nil(),
document_record_id: Some(uuid::Uuid::nil()),
text_segment_id: None,
target_entity_id: Some(uuid::Uuid::nil()),
target_project_id: None,
membership_type_code: "author".into(),
membership_weight: 1.0,
valid_from: EventTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("v"),
valid_to: None,
valid_time_precision_code: "second".into(),
system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"),
available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"),
}
}

fn exercise_membership_assignment(repo: &mut LiveDocumentRepository<RecordingSqlSession>) {
let membership = sample_membership();
repo.insert_membership_assignment(&membership)
.expect("membership insert");
repo.submit_membership_assignments_for_document(uuid::Uuid::nil())
.expect("membership lookup");
assert!(
repo.session()
.executed()
.iter()
.any(|sql| sql.contains("INSERT INTO membership_assignment"))
);
let mut invalid = membership;
invalid.target_entity_id = None;
assert_eq!(
repo.insert_membership_assignment(&invalid),
Err(PersistenceError::InvalidMembershipAssignment)
);
}

#[test]
fn live_repository_applies_migrations_and_document_sql() {
let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new());
Expand Down Expand Up @@ -395,6 +463,7 @@ mod tests {
.any(|sql| sql.contains("INSERT INTO reproducibility_manifest"))
);
exercise_model_run_chain(&mut repo, &manifest);
exercise_membership_assignment(&mut repo);

let audit = AuditEvent {
audit_event_id: uuid::Uuid::nil(),
Expand Down
Loading
Loading