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` event-mention SQL contracts: mention identity cannot equal the instance it supports; confidence must be finite and in `(0, 1]`.
- `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.
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 @@ -28,6 +28,8 @@ pub enum PersistenceError {
InvalidMembershipAssignment,
/// An event relation violated the closed ERD transition vocabulary.
InvalidEventRelation,
/// An event mention reused an instance identity or had an invalid confidence.
InvalidEventMention,
}

impl fmt::Display for PersistenceError {
Expand All @@ -44,6 +46,7 @@ impl fmt::Display for PersistenceError {
Self::LiveAdapterNotConfigured => "live adapter not configured",
Self::InvalidMembershipAssignment => "invalid membership assignment",
Self::InvalidEventRelation => "invalid event relation",
Self::InvalidEventMention => "invalid event mention",
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -147,6 +150,10 @@ mod tests {
PersistenceError::InvalidEventRelation.to_string(),
"invalid event relation"
);
assert_eq!(
PersistenceError::InvalidEventMention.to_string(),
"invalid event mention"
);
assert_eq!(
MigrationContractError::SingleWordObjectName.to_string(),
"single-word database object name"
Expand Down
6 changes: 5 additions & 1 deletion crates/persistence_postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ mod live_pool;
mod live_repository;
mod manifest_sql;
mod membership_sql;
mod mention_sql;
mod migration;
mod model_run_sql;
mod naming;
Expand Down Expand Up @@ -84,7 +85,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;
/// Typed event-relation row bound to the ERD transition vocabulary.
/// Event-mention row that cannot collapse into an instance identity.
pub use mention_sql::EventMentionRecord;
/// Render insert SQL for a validated event mention.
pub use mention_sql::insert_event_mention_sql;
/// Embedded and ad-hoc migration catalogs.
pub use migration::MigrationCatalog;
/// Validate migration SQL against TEPP contracts.
Expand Down
41 changes: 41 additions & 0 deletions crates/persistence_postgres/src/live_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::membership_sql::{
MembershipAssignmentRecord, insert_membership_assignment_sql,
select_membership_assignments_for_document_sql,
};
use crate::mention_sql::{EventMentionRecord, insert_event_mention_sql};
use crate::migration::{MigrationCatalog, validate_migration_catalog};
use crate::model_run_sql::{
CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord,
Expand Down Expand Up @@ -258,6 +259,20 @@ impl<S: SqlSession> LiveDocumentRepository<S> {
self.session.execute(&sql)
}

/// Insert an event mention that is not an instance identity.
///
/// # Errors
///
/// Returns mention/instance or confidence validation failures, or transport
/// failures.
pub fn insert_event_mention(
&mut self,
record: &EventMentionRecord,
) -> Result<(), PersistenceError> {
let sql = insert_event_mention_sql(record)?;
self.session.execute(&sql)
}

/// Look up a model run by primary key.
///
/// # Errors
Expand Down Expand Up @@ -307,6 +322,7 @@ mod tests {
use super::{LiveDocumentRepository, LiveMigrationError};
use crate::document_store::{AuditEvent, DocumentRecord};
use crate::manifest_sql::ReproducibilityManifestRecord;
use crate::mention_sql::EventMentionRecord;
use crate::migration::MigrationCatalog;
use crate::model_run_sql::{CorpusSplitManifestRecord, ModelArtifactRecord, ModelRunRecord};
use crate::relation_sql::EventRelationRecord;
Expand Down Expand Up @@ -457,6 +473,30 @@ mod tests {
);
}

fn exercise_event_mention(repo: &mut LiveDocumentRepository<RecordingSqlSession>) {
let mention = EventMentionRecord {
event_mention_id: uuid::Uuid::from_u128(2),
event_instance_id: uuid::Uuid::from_u128(1),
tenant_record_id: uuid::Uuid::nil(),
confidence_score: 0.75,
system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"),
available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"),
};
repo.insert_event_mention(&mention).expect("mention insert");
let mut collapsed = mention.clone();
collapsed.event_mention_id = collapsed.event_instance_id;
assert_eq!(
repo.insert_event_mention(&collapsed),
Err(PersistenceError::InvalidEventMention)
);
assert!(
repo.session()
.executed()
.iter()
.any(|sql| sql.contains("INSERT INTO event_mention"))
);
}

#[test]
fn live_repository_applies_migrations_and_document_sql() {
let mut repo = LiveDocumentRepository::new(RecordingSqlSession::new());
Expand Down Expand Up @@ -511,6 +551,7 @@ mod tests {
exercise_model_run_chain(&mut repo, &manifest);
exercise_membership_assignment(&mut repo);
exercise_event_relation(&mut repo);
exercise_event_mention(&mut repo);

let audit = AuditEvent {
audit_event_id: uuid::Uuid::nil(),
Expand Down
70 changes: 70 additions & 0 deletions crates/persistence_postgres/src/mention_sql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! SQL contracts for event mentions distinct from event instances.

use crate::PersistenceError;
use temporal_core::{AvailableTime, SystemTime};
use uuid::Uuid;

/// One append-only event mention that cannot be treated as an instance.
///
/// Maps to `event_mention`. `event_mention_id` must differ from
/// `event_instance_id`; confidence must be finite and in `(0, 1]`.
#[derive(Clone, Debug, PartialEq)]
pub struct EventMentionRecord {
/// Mention identity (never interchangeable with the instance).
pub event_mention_id: Uuid,
/// Promoted instance this mention supports.
pub event_instance_id: Uuid,
/// Owning tenant boundary.
pub tenant_record_id: Uuid,
/// Mention confidence in `(0, 1]`.
pub confidence_score: f64,
/// System/record time when the mention was asserted.
pub system_time: SystemTime,
/// Availability time of the mention evidence.
pub available_time: AvailableTime,
}

impl EventMentionRecord {
/// Fail-closed mention/instance separation and confidence validation.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidEventMention`] when the mention is
/// the instance identity or the confidence is not in `(0, 1]`.
pub fn validate(&self) -> Result<(), PersistenceError> {
if self.event_mention_id == self.event_instance_id {
return Err(PersistenceError::InvalidEventMention);
}
if !self.confidence_score.is_finite()
|| self.confidence_score <= 0.0
|| self.confidence_score > 1.0
{
return Err(PersistenceError::InvalidEventMention);
}
Ok(())
}
}

/// Render insert SQL for a validated event mention.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidEventMention`] before any SQL is produced.
pub fn insert_event_mention_sql(record: &EventMentionRecord) -> Result<String, PersistenceError> {
record.validate()?;
Ok(format!(
"INSERT INTO event_mention (\
event_mention_id, event_instance_id, tenant_record_id, \
confidence_score, system_time, available_time\
) VALUES (\
'{mention}'::uuid, '{instance}'::uuid, '{tenant}'::uuid, \
{confidence}, '{system}'::timestamptz, '{available}'::timestamptz\
)",
mention = record.event_mention_id,
instance = record.event_instance_id,
tenant = record.tenant_record_id,
confidence = record.confidence_score,
system = record.system_time.to_rfc3339(),
available = record.available_time.to_rfc3339(),
))
}
65 changes: 65 additions & 0 deletions crates/persistence_postgres/tests/event_mention_sql_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//! Event-mention SQL must refuse treating a mention as an instance.

use persistence_postgres::{EventMentionRecord, PersistenceError, insert_event_mention_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 mention(instance: Uuid, mention: Uuid, confidence: f64) -> EventMentionRecord {
let (available, system) = clocks();
EventMentionRecord {
event_mention_id: mention,
event_instance_id: instance,
tenant_record_id: Uuid::nil(),
confidence_score: confidence,
system_time: system,
available_time: available,
}
}

#[test]
fn insert_sql_keeps_mention_and_instance_distinct() {
let sql = insert_event_mention_sql(&mention(Uuid::from_u128(1), Uuid::from_u128(2), 0.8))
.expect("sql");
assert!(sql.contains("INSERT INTO event_mention"));
assert!(sql.contains("event_instance_id"));
assert!(sql.contains("0.8"));
}

#[test]
fn mention_as_instance_and_invalid_confidence_fail_closed() {
let same = Uuid::from_u128(7);
assert_eq!(
insert_event_mention_sql(&mention(same, same, 0.8)),
Err(PersistenceError::InvalidEventMention)
);
assert_eq!(
insert_event_mention_sql(&mention(Uuid::from_u128(1), Uuid::from_u128(2), 0.0)),
Err(PersistenceError::InvalidEventMention)
);
assert_eq!(
insert_event_mention_sql(&mention(Uuid::from_u128(1), Uuid::from_u128(2), 1.01)),
Err(PersistenceError::InvalidEventMention)
);
assert_eq!(
insert_event_mention_sql(&mention(Uuid::from_u128(1), Uuid::from_u128(2), f64::NAN)),
Err(PersistenceError::InvalidEventMention)
);
assert_eq!(
insert_event_mention_sql(&mention(
Uuid::from_u128(1),
Uuid::from_u128(2),
f64::INFINITY
)),
Err(PersistenceError::InvalidEventMention)
);
assert!(
insert_event_mention_sql(&mention(Uuid::from_u128(1), Uuid::from_u128(2), 1.0)).is_ok()
);
}
2 changes: 1 addition & 1 deletion docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The full APA 7th standards/literature register remains `docs/research/standards-
| six distinct clocks and uncertain intervals | PRD; ADR 0002 | PR #8 `temporal_core` on protected main; PR #5 historical only | implemented-main |
| Allen relation algebra/bounded closure | ADR 0002; temporal research | PR #9 `temporal_core` path-consistency on protected main | implemented-main |
| forward-only transition subgraph | PRD; ADR 0002/0003 | `relation_graph` on protected main | implemented-main |
| event ontology/evidence mentions | PRD; ADR 0003 | `event_core` mention/instance separation on protected main; full intelligence stack remaining | partial |
| event ontology/evidence mentions | PRD; ADR 0003 | `event_core` mention/instance separation on protected main; `persistence_postgres` mention SQL (active PR) refuses mention-as-instance; full intelligence stack remaining | partial |
| 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 |
Expand Down
36 changes: 36 additions & 0 deletions docs/research/event-mention-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Event-mention persistence (doctoring)

## Scope

`event_mention` already exists on the foundation schema. This slice adds the
fail-closed insert contract that keeps mention identity distinct from the
promoted instance it supports. A mention cannot be persisted as if it were
the instance (`event_mention_id != event_instance_id`), and confidence is
restricted to a finite score in `(0, 1]`.

This does not add a new migration number, so it can land independently of
stacked `0005`/`0006` PRs.

## Authority

Hovy, E., Marcus, M., Palmer, M., Ramshaw, L., & Weischedel, R. (2006).
OntoNotes: The 90% solution. In *Proceedings of the Human Language Technology
Conference of the NAACL, Companion Volume: Short Papers* (pp. 57–60).
Association for Computational Linguistics.

Pustejovsky, J., Castano, J., Ingria, R., Saurí, R., Gaizauskas, R., Setzer,
A., Katz, G., & Radev, D. (2003). TimeML: Robust specification of event and
temporal expressions in text. In *New Directions in Question Answering* (pp.
28–34). AAAI Press.

Mentions are observations; instances are promoted entities. Collapsing those
identities would treat an observation as the event itself (Hovy et al., 2006;
Pustejovsky et al., 2003).

## Verification

- contract tests reject mention-as-instance and non-finite or out-of-range
confidence;
- recording-session coverage for insert SQL;
- live PostgreSQL CI inserts a valid mention and refuses identity collapse
when `TEPP_LIVE_POSTGRES=1`.
Loading