From defee2604e95e50cdfac36cfd743efad1538e86d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:00:15 +0900 Subject: [PATCH 01/18] test(workforce-validation): require owner-schema PostgreSQL read port --- .../tests/test_postgres_registry.py | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 services/workforce-validation-api/tests/test_postgres_registry.py diff --git a/services/workforce-validation-api/tests/test_postgres_registry.py b/services/workforce-validation-api/tests/test_postgres_registry.py new file mode 100644 index 00000000..c0793e8d --- /dev/null +++ b/services/workforce-validation-api/tests/test_postgres_registry.py @@ -0,0 +1,208 @@ +"""PostgreSQL adapter contract for the workforce-validation registry owner.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from uuid import UUID + +import pytest + +from orgmetra_workforce_validation_api.postgres_registry import PostgresValidityStudyReadPort +from orgmetra_workforce_validation_api.registry import ValidityStudyIntegrityError, ValidityStudyRecord + +TENANT = UUID("10000000-0000-7000-8000-000000000001") +STUDY = UUID("00000000-0000-7000-8000-0000000000c1") +CRITERION = UUID("00000000-0000-7000-8000-0000000000b1") +RECORDED_FROM = datetime(2026, 7, 1, tzinfo=timezone.utc) + + +def _row( + *, + tenant_record_id: UUID = TENANT, + validity_study_id: UUID = STUDY, + study_status_code: str = "active", +) -> tuple[object, ...]: + return ( + tenant_record_id, + validity_study_id, + CRITERION, + study_status_code, + RECORDED_FROM, + None, + ) + + +class _Cursor: + """Record exact DB-API calls and return a bounded row set.""" + + def __init__(self, rows: list[object]) -> None: + self.rows = rows + self.calls: list[tuple[str, object]] = [] + self.fetch_size: int | None = None + + def __enter__(self) -> _Cursor: + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + del exc_type, exc, traceback + + def execute(self, sql: str, params: object = None) -> None: + self.calls.append((sql, params)) + + def fetchmany(self, size: int) -> list[object]: + self.fetch_size = size + return self.rows[:size] + + +class _Connection: + """Provide one deterministic cursor through a context-manager connection.""" + + def __init__(self, cursor: _Cursor) -> None: + self.cursor_instance = cursor + + def __enter__(self) -> _Connection: + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + del exc_type, exc, traceback + + def cursor(self) -> _Cursor: + return self.cursor_instance + + +class _Factory: + """Expose whether executable connection acquisition was reached.""" + + def __init__(self, rows: list[object], callback=None) -> None: + self.cursor = _Cursor(rows) + self.callback = callback + self.calls = 0 + + def __call__(self) -> _Connection: + self.calls += 1 + if self.callback is not None: + self.callback() + return _Connection(self.cursor) + + +def test_constructor_rejects_non_callable_factory() -> None: + with pytest.raises(TypeError, match="connection_factory must be callable"): + PostgresValidityStudyReadPort(connection_factory=object()) # type: ignore[arg-type] + + +def test_invalid_target_is_rejected_before_connection_acquisition() -> None: + factory = _Factory([]) + port = PostgresValidityStudyReadPort(connection_factory=factory) + + with pytest.raises(ValueError, match="tenant_record_id must be an exact operational UUID"): + port.read_validity_study(tenant_record_id="not-a-uuid", validity_study_id=STUDY) # type: ignore[arg-type] + + assert factory.calls == 0 + + +def test_empty_read_is_tenant_bound_read_only_and_schema_qualified() -> None: + factory = _Factory([]) + port = PostgresValidityStudyReadPort(connection_factory=factory) + + assert port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) is None + + assert factory.cursor.fetch_size == 2 + assert len(factory.cursor.calls) == 3 + read_only_sql, read_only_params = factory.cursor.calls[0] + tenant_sql, tenant_params = factory.cursor.calls[1] + registry_sql, registry_params = factory.cursor.calls[2] + assert read_only_sql == "SET TRANSACTION READ ONLY" + assert read_only_params is None + assert tenant_sql == "SELECT pg_catalog.set_config('orgmetra.tenant_record_id', %s, true)" + assert tenant_params == (str(TENANT),) + assert "FROM workforce_validation.validity_study" in registry_sql + assert "public.validity_study" not in registry_sql + assert registry_params == (TENANT, STUDY) + + +def test_multiple_rows_fail_closed() -> None: + port = PostgresValidityStudyReadPort(connection_factory=_Factory([_row(), _row()])) + + with pytest.raises(ValidityStudyIntegrityError, match="multiple current validity-study rows"): + port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) + + +@pytest.mark.parametrize( + "row", + [ + list(_row()), + _row()[:-1], + ], +) +def test_noncanonical_row_container_fails_closed(row: object) -> None: + port = PostgresValidityStudyReadPort(connection_factory=_Factory([row])) + + with pytest.raises(ValidityStudyIntegrityError, match="non-canonical validity-study row"): + port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) + + +def test_invalid_row_scalar_is_reported_as_persistence_integrity_failure() -> None: + port = PostgresValidityStudyReadPort( + connection_factory=_Factory([_row(study_status_code="NOT_CANONICAL")]) + ) + + with pytest.raises(ValidityStudyIntegrityError, match="invalid validity-study row"): + port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) + + +def test_foreign_row_target_fails_closed() -> None: + other_study = UUID("00000000-0000-7000-8000-0000000000c2") + port = PostgresValidityStudyReadPort( + connection_factory=_Factory([_row(validity_study_id=other_study)]) + ) + + with pytest.raises(ValidityStudyIntegrityError, match="another target"): + port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) + + +def test_valid_row_reconstructs_owner_record() -> None: + port = PostgresValidityStudyReadPort(connection_factory=_Factory([_row()])) + + record = port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) + + assert type(record) is ValidityStudyRecord + assert record.tenant_record_id == TENANT + assert record.validity_study_id == STUDY + assert record.criterion_blueprint_id == CRITERION + assert record.study_status_code == "active" + assert record.recorded_from == RECORDED_FROM + assert record.recorded_to is None + + +def test_connection_callback_cannot_switch_snapshotted_target() -> None: + tenant = UUID("10000000-0000-7000-8000-000000000001") + study = UUID("00000000-0000-7000-8000-0000000000c1") + original_tenant_int = tenant.int + original_study_int = study.int + other_tenant = UUID("10000000-0000-7000-8000-000000000002") + other_study = UUID("00000000-0000-7000-8000-0000000000c2") + + def mutate_retained_inputs() -> None: + object.__setattr__(tenant, "int", other_tenant.int) + object.__setattr__(study, "int", other_study.int) + + factory = _Factory( + [ + _row( + tenant_record_id=UUID(int=original_tenant_int), + validity_study_id=UUID(int=original_study_int), + ) + ], + callback=mutate_retained_inputs, + ) + port = PostgresValidityStudyReadPort(connection_factory=factory) + + record = port.read_validity_study(tenant_record_id=tenant, validity_study_id=study) + + _, tenant_params = factory.cursor.calls[1] + _, registry_params = factory.cursor.calls[2] + assert tenant_params == (str(UUID(int=original_tenant_int)),) + assert registry_params == (UUID(int=original_tenant_int), UUID(int=original_study_int)) + assert record is not None + assert record.tenant_record_id == UUID(int=original_tenant_int) + assert record.validity_study_id == UUID(int=original_study_int) From 712ca77a7b3198cb6b7bd075adc1ad5dcbc728c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:01:35 +0900 Subject: [PATCH 02/18] test(workforce-validation): require forward-only registry adoption --- ...e_validation_registry_adoption_postgres.sh | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/test_workforce_validation_registry_adoption_postgres.sh diff --git a/tests/test_workforce_validation_registry_adoption_postgres.sh b/tests/test_workforce_validation_registry_adoption_postgres.sh new file mode 100644 index 00000000..5b72605b --- /dev/null +++ b/tests/test_workforce_validation_registry_adoption_postgres.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" + +TENANT_ID="10000000-0000-7000-8000-000000000001" +JOB_ID="00000000-0000-7000-8000-0000000000a1" +CRITERION_ID="00000000-0000-7000-8000-0000000000b1" +STUDY_ID="00000000-0000-7000-8000-0000000000c1" + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f services/workforce-validation-api/database/migrations/0001_owner_schema.sql + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f services/workforce-validation-api/database/migrations/0002_registry_adoption.sql + +if [[ "$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT to_regclass('public.validity_study') IS NULL;")" != "t" ]]; then + echo "legacy public.validity_study relation still exists after owner adoption" >&2 + exit 1 +fi + +owner_oid="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT 'workforce_validation.validity_study'::regclass::oid;")" +if [[ "${owner_oid}" != "${legacy_oid}" ]]; then + echo "registry adoption copied/recreated the table instead of preserving relation identity" >&2 + exit 1 +fi + +owner_name="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT pg_get_userbyid(relowner) +FROM pg_class +WHERE oid = ${owner_oid}; +")" +if [[ "${owner_name}" != "workforce_validation_role" ]]; then + echo "registry table has unexpected owner: ${owner_name}" >&2 + exit 1 +fi + +owner_fk_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) FROM pg_constraint WHERE contype = 'f' AND confrelid = ${owner_oid}; +")" +if [[ "${owner_fk_count}" != "${legacy_fk_count}" ]]; then + echo "registry adoption broke existing FK dependencies: before=${legacy_fk_count} after=${owner_fk_count}" >&2 + exit 1 +fi + +rls_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT relrowsecurity, relforcerowsecurity +FROM pg_class +WHERE oid = ${owner_oid}; +")" +if [[ "${rls_flags}" != "t|t" ]]; then + echo "registry adoption did not preserve forced tenant RLS: ${rls_flags}" >&2 + exit 1 +fi + +bitemporal_trigger="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) +FROM pg_trigger +WHERE tgrelid = ${owner_oid} + AND tgname = 'validity_study_bitemporal_guard' + AND NOT tgisinternal; +")" +if [[ "${bitemporal_trigger}" != "1" ]]; then + echo "registry adoption lost the bitemporal mutation guard" >&2 + exit 1 +fi + +runtime_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolreplication, rolbypassrls +FROM pg_roles +WHERE rolname = 'workforce_validation_runtime_role'; +")" +if [[ "${runtime_flags}" != "f|f|f|f|f|f|f" ]]; then + echo "workforce_validation_runtime_role flags are not deny-default: ${runtime_flags}" >&2 + exit 1 +fi + +runtime_privileges="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT + has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'USAGE'), + has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'CREATE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'SELECT'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'INSERT'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'UPDATE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'DELETE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'TRUNCATE'); +")" +if [[ "${runtime_privileges}" != "t|f|t|f|f|f|f" ]]; then + echo "runtime role privileges are not least-privilege read-only: ${runtime_privileges}" >&2 + exit 1 +fi + +missing_tenant_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SET ROLE workforce_validation_runtime_role; +SELECT count(*) FROM workforce_validation.validity_study; +RESET ROLE; +")" +if [[ "${missing_tenant_count}" != "0" ]]; then + echo "runtime read returned rows without tenant context: ${missing_tenant_count}" >&2 + exit 1 +fi + +tenant_read="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT pg_catalog.set_config('orgmetra.tenant_record_id', '${TENANT_ID}', false); +SET ROLE workforce_validation_runtime_role; +SELECT validity_study_id::text FROM workforce_validation.validity_study; +RESET ROLE; +")" +if [[ "${tenant_read}" != "${TENANT_ID}|${STUDY_ID}" && "${tenant_read}" != "${STUDY_ID}" ]]; then + echo "runtime role did not read the tenant-scoped owner registry: ${tenant_read}" >&2 + exit 1 +fi From 55ddef242d97eb7cf5d0110525999757c518b0c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:02:41 +0900 Subject: [PATCH 03/18] test(workforce-validation): make tenant-role probe output deterministic --- .../test_workforce_validation_registry_adoption_postgres.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_workforce_validation_registry_adoption_postgres.sh b/tests/test_workforce_validation_registry_adoption_postgres.sh index 5b72605b..c461fef5 100644 --- a/tests/test_workforce_validation_registry_adoption_postgres.sh +++ b/tests/test_workforce_validation_registry_adoption_postgres.sh @@ -144,12 +144,13 @@ if [[ "${missing_tenant_count}" != "0" ]]; then fi tenant_read="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT pg_catalog.set_config('orgmetra.tenant_record_id', '${TENANT_ID}', false); +SET orgmetra.tenant_record_id = '${TENANT_ID}'; SET ROLE workforce_validation_runtime_role; SELECT validity_study_id::text FROM workforce_validation.validity_study; RESET ROLE; +RESET orgmetra.tenant_record_id; ")" -if [[ "${tenant_read}" != "${TENANT_ID}|${STUDY_ID}" && "${tenant_read}" != "${STUDY_ID}" ]]; then +if [[ "${tenant_read}" != "${STUDY_ID}" ]]; then echo "runtime role did not read the tenant-scoped owner registry: ${tenant_read}" >&2 exit 1 fi From f903c2baed1ca35a882082a26e7daf6bc2892964 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:03:10 +0900 Subject: [PATCH 04/18] feat(workforce-validation): adopt validity registry into owner schema --- .../migrations/0002_registry_adoption.sql | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 services/workforce-validation-api/database/migrations/0002_registry_adoption.sql diff --git a/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql b/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql new file mode 100644 index 00000000..591b50dc --- /dev/null +++ b/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql @@ -0,0 +1,23 @@ +-- Adopt the existing validity-study registry into its canonical bounded-context schema. +-- ALTER TABLE ... SET SCHEMA preserves the relation OID, rows, constraints, indexes, +-- RLS policy and bitemporal trigger instead of copying authoritative HR evidence. + +BEGIN; + +CREATE ROLE workforce_validation_runtime_role NOLOGIN + NOSUPERUSER + NOCREATEDB + NOCREATEROLE + NOINHERIT + NOREPLICATION + NOBYPASSRLS; + +ALTER TABLE public.validity_study SET SCHEMA workforce_validation; +ALTER TABLE workforce_validation.validity_study OWNER TO workforce_validation_role; + +REVOKE ALL ON TABLE workforce_validation.validity_study FROM PUBLIC; +GRANT USAGE ON SCHEMA workforce_validation TO workforce_validation_runtime_role; +GRANT SELECT ON TABLE workforce_validation.validity_study TO workforce_validation_runtime_role; +GRANT EXECUTE ON FUNCTION public.current_tenant_record_id() TO workforce_validation_runtime_role; + +COMMIT; From 000d9e6f77b0b6ef99d4a4de39f56787619bedd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:03:41 +0900 Subject: [PATCH 05/18] feat(workforce-validation): add tenant-bound PostgreSQL registry adapter --- .../postgres_registry.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py diff --git a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py new file mode 100644 index 00000000..30605baa --- /dev/null +++ b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py @@ -0,0 +1,108 @@ +"""Tenant-bound PostgreSQL read adapter for the workforce-validation registry. + +The adapter reads only the canonical ``workforce_validation.validity_study`` +relation. Deployment code owns pooling, TLS, credentials and assumption of the +least-privilege runtime role; this boundary owns transaction-local tenant RLS, +parameterized SQL, target snapshots and fail-closed row reconstruction. +""" + +from __future__ import annotations + +from contextlib import AbstractContextManager +from dataclasses import dataclass +from typing import Any, Callable +from uuid import UUID + +from orgmetra_workforce_validation_api.registry import ( + ValidityStudyIntegrityError, + ValidityStudyRecord, + _restore_operational_uuid, + _store_operational_uuid, +) + +PostgresConnectionFactory = Callable[[], AbstractContextManager[Any]] + +_READ_ONLY_SQL = "SET TRANSACTION READ ONLY" +_TENANT_CONTEXT_SQL = "SELECT pg_catalog.set_config('orgmetra.tenant_record_id', %s, true)" +_REGISTRY_READ_SQL = """ +SELECT + tenant_record_id, + validity_study_id, + criterion_blueprint_id, + study_status_code, + recorded_from, + recorded_to +FROM workforce_validation.validity_study +WHERE tenant_record_id = %s + AND validity_study_id = %s + AND recorded_to IS NULL +LIMIT 2 +""".strip() + + +@dataclass(frozen=True, slots=True) +class PostgresValidityStudyReadPort: + """Read one current validity-study header under forced tenant RLS. + + ``connection_factory`` must return a DB-API-compatible connection context + manager configured by deployment code. The adapter snapshots UUID identity + before invoking that executable factory so retained caller UUID aliases cannot + change the authorized SQL target during connection acquisition. + """ + + connection_factory: PostgresConnectionFactory + + def __post_init__(self) -> None: + """Reject an unusable connection dependency before protected reads.""" + if not callable(self.connection_factory): + raise TypeError("connection_factory must be callable") + + def read_validity_study( + self, + *, + tenant_record_id: UUID, + validity_study_id: UUID, + ) -> ValidityStudyRecord | None: + """Return one exact current owner record or ``None`` for the tenant target.""" + tenant_identity = _store_operational_uuid("tenant_record_id", tenant_record_id) + study_identity = _store_operational_uuid("validity_study_id", validity_study_id) + + sql_tenant_id = _restore_operational_uuid("tenant_record_id", tenant_identity) + sql_study_id = _restore_operational_uuid("validity_study_id", study_identity) + + with self.connection_factory() as connection: + with connection.cursor() as cursor: + cursor.execute(_READ_ONLY_SQL) + cursor.execute(_TENANT_CONTEXT_SQL, (str(sql_tenant_id),)) + cursor.execute(_REGISTRY_READ_SQL, (sql_tenant_id, sql_study_id)) + rows = cursor.fetchmany(2) + + if not rows: + return None + if len(rows) != 1: + raise ValidityStudyIntegrityError("multiple current validity-study rows match the target") + + row = rows[0] + if type(row) is not tuple or len(row) != 6: + raise ValidityStudyIntegrityError("repository returned a non-canonical validity-study row") + + try: + record = ValidityStudyRecord( + tenant_record_id=row[0], + validity_study_id=row[1], + criterion_blueprint_id=row[2], + study_status_code=row[3], + recorded_from=row[4], + recorded_to=row[5], + ) + except (TypeError, ValueError) as exc: + raise ValidityStudyIntegrityError("repository returned an invalid validity-study row") from exc + + if ( + _store_operational_uuid("record tenant_record_id", record.tenant_record_id) + != tenant_identity + or _store_operational_uuid("record validity_study_id", record.validity_study_id) + != study_identity + ): + raise ValidityStudyIntegrityError("repository returned a validity-study row for another target") + return record From 66571e7e4f336dd4a03df77fdfe093cfc20ac0bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:04:16 +0900 Subject: [PATCH 06/18] feat(workforce-validation): publish PostgreSQL owner adapter --- .../src/orgmetra_workforce_validation_api/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/__init__.py b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/__init__.py index 48570a97..e592a98c 100644 --- a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/__init__.py +++ b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/__init__.py @@ -1,5 +1,6 @@ """Canonical workforce-validation application contracts for Orgmetra.""" +from orgmetra_workforce_validation_api.postgres_registry import PostgresValidityStudyReadPort from orgmetra_workforce_validation_api.registry import ( ValidationPrincipal, ValidityStudyIntegrityError, @@ -11,6 +12,7 @@ ) __all__ = [ + "PostgresValidityStudyReadPort", "ValidationPrincipal", "ValidityStudyIntegrityError", "ValidityStudyNotFound", From ebf08af84baf2f977009400cb8a6f274b4d851e3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:05:17 +0900 Subject: [PATCH 07/18] ci(workforce-validation): admit registry adoption contract --- .github/workflows/foundation-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 4f6d0536..46baace5 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -85,6 +85,7 @@ jobs: test_candidate_worker_conversion_postgres.sh test_validity_study_case_postgres.sh test_workforce_validation_owner_schema_postgres.sh + test_workforce_validation_registry_adoption_postgres.sh test_criterion_observation_scope_postgres.sh test_people_mutation_idempotency_postgres.sh test_job_analysis_snapshot_postgres.sh From 55afd357d86e0cd6e355c628ee647c2e76289abb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:06:18 +0900 Subject: [PATCH 08/18] ci(workforce-validation): preserve sealed Foundation workflow --- .github/workflows/foundation-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 46baace5..4f6d0536 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -85,7 +85,6 @@ jobs: test_candidate_worker_conversion_postgres.sh test_validity_study_case_postgres.sh test_workforce_validation_owner_schema_postgres.sh - test_workforce_validation_registry_adoption_postgres.sh test_criterion_observation_scope_postgres.sh test_people_mutation_idempotency_postgres.sh test_job_analysis_snapshot_postgres.sh From a64610fda3ea544ccc781761bcfc4ef1252f4c1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:06:53 +0900 Subject: [PATCH 09/18] test(workforce-validation): extend canonical PostgreSQL owner acceptance --- ...kforce_validation_owner_schema_postgres.sh | 160 +++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/tests/test_workforce_validation_owner_schema_postgres.sh b/tests/test_workforce_validation_owner_schema_postgres.sh index c79b659c..956edb16 100644 --- a/tests/test_workforce_validation_owner_schema_postgres.sh +++ b/tests/test_workforce_validation_owner_schema_postgres.sh @@ -3,8 +3,9 @@ set -euo pipefail : "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" -migration="services/workforce-validation-api/database/migrations/0001_owner_schema.sql" -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +owner_migration="services/workforce-validation-api/database/migrations/0001_owner_schema.sql" +adoption_migration="services/workforce-validation-api/database/migrations/0002_registry_adoption.sql" +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${owner_migration}" role_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolreplication, rolbypassrls @@ -74,3 +75,158 @@ fi psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -qc "DROP ROLE workforce_validation_public_probe;" trap - EXIT + +# The adoption phase runs after the protected foundation migration chain has +# created the legacy registry. Keeping it in this already-admitted isolated +# contract avoids a second CI orchestration path while preserving the bootstrap +# assertion above as an independently observable prerequisite. +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql + +TENANT_ID="10000000-0000-7000-8000-000000000001" +JOB_ID="00000000-0000-7000-8000-0000000000a1" +CRITERION_ID="00000000-0000-7000-8000-0000000000b1" +STUDY_ID="00000000-0000-7000-8000-0000000000c1" + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + +psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${adoption_migration}" + +if [[ "$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT to_regclass('public.validity_study') IS NULL;")" != "t" ]]; then + echo "legacy public.validity_study relation still exists after owner adoption" >&2 + exit 1 +fi + +owner_oid="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT 'workforce_validation.validity_study'::regclass::oid;")" +if [[ "${owner_oid}" != "${legacy_oid}" ]]; then + echo "registry adoption copied/recreated the table instead of preserving relation identity" >&2 + exit 1 +fi + +registry_owner="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT pg_get_userbyid(relowner) +FROM pg_class +WHERE oid = ${owner_oid}; +")" +if [[ "${registry_owner}" != "workforce_validation_role" ]]; then + echo "registry table has unexpected owner: ${registry_owner}" >&2 + exit 1 +fi + +owner_fk_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) FROM pg_constraint WHERE contype = 'f' AND confrelid = ${owner_oid}; +")" +if [[ "${owner_fk_count}" != "${legacy_fk_count}" ]]; then + echo "registry adoption broke existing FK dependencies: before=${legacy_fk_count} after=${owner_fk_count}" >&2 + exit 1 +fi + +rls_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT relrowsecurity, relforcerowsecurity +FROM pg_class +WHERE oid = ${owner_oid}; +")" +if [[ "${rls_flags}" != "t|t" ]]; then + echo "registry adoption did not preserve forced tenant RLS: ${rls_flags}" >&2 + exit 1 +fi + +bitemporal_trigger="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT count(*) +FROM pg_trigger +WHERE tgrelid = ${owner_oid} + AND tgname = 'validity_study_bitemporal_guard' + AND NOT tgisinternal; +")" +if [[ "${bitemporal_trigger}" != "1" ]]; then + echo "registry adoption lost the bitemporal mutation guard" >&2 + exit 1 +fi + +runtime_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolreplication, rolbypassrls +FROM pg_roles +WHERE rolname = 'workforce_validation_runtime_role'; +")" +if [[ "${runtime_flags}" != "f|f|f|f|f|f|f" ]]; then + echo "workforce_validation_runtime_role flags are not deny-default: ${runtime_flags}" >&2 + exit 1 +fi + +runtime_privileges="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SELECT + has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'USAGE'), + has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'CREATE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'SELECT'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'INSERT'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'UPDATE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'DELETE'), + has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'TRUNCATE'); +")" +if [[ "${runtime_privileges}" != "t|f|t|f|f|f|f" ]]; then + echo "runtime role privileges are not least-privilege read-only: ${runtime_privileges}" >&2 + exit 1 +fi + +missing_tenant_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SET ROLE workforce_validation_runtime_role; +SELECT count(*) FROM workforce_validation.validity_study; +RESET ROLE; +")" +if [[ "${missing_tenant_count}" != "0" ]]; then + echo "runtime read returned rows without tenant context: ${missing_tenant_count}" >&2 + exit 1 +fi + +tenant_read="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " +SET orgmetra.tenant_record_id = '${TENANT_ID}'; +SET ROLE workforce_validation_runtime_role; +SELECT validity_study_id::text FROM workforce_validation.validity_study; +RESET ROLE; +RESET orgmetra.tenant_record_id; +")" +if [[ "${tenant_read}" != "${STUDY_ID}" ]]; then + echo "runtime role did not read the tenant-scoped owner registry: ${tenant_read}" >&2 + exit 1 +fi From b13b0c011513833b37eb522555acaae9011e9eee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:07:14 +0900 Subject: [PATCH 10/18] test(workforce-validation): consolidate adoption into admitted PostgreSQL contract --- ...e_validation_registry_adoption_postgres.sh | 156 ------------------ 1 file changed, 156 deletions(-) delete mode 100644 tests/test_workforce_validation_registry_adoption_postgres.sh diff --git a/tests/test_workforce_validation_registry_adoption_postgres.sh b/tests/test_workforce_validation_registry_adoption_postgres.sh deleted file mode 100644 index c461fef5..00000000 --- a/tests/test_workforce_validation_registry_adoption_postgres.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${DATABASE_URL:=postgresql://orgmetra:orgmetra@localhost:5432/orgmetra}" - -TENANT_ID="10000000-0000-7000-8000-000000000001" -JOB_ID="00000000-0000-7000-8000-0000000000a1" -CRITERION_ID="00000000-0000-7000-8000-0000000000b1" -STUDY_ID="00000000-0000-7000-8000-0000000000c1" - -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f services/workforce-validation-api/database/migrations/0001_owner_schema.sql - -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 - exit 1 -fi - -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f services/workforce-validation-api/database/migrations/0002_registry_adoption.sql - -if [[ "$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT to_regclass('public.validity_study') IS NULL;")" != "t" ]]; then - echo "legacy public.validity_study relation still exists after owner adoption" >&2 - exit 1 -fi - -owner_oid="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc "SELECT 'workforce_validation.validity_study'::regclass::oid;")" -if [[ "${owner_oid}" != "${legacy_oid}" ]]; then - echo "registry adoption copied/recreated the table instead of preserving relation identity" >&2 - exit 1 -fi - -owner_name="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT pg_get_userbyid(relowner) -FROM pg_class -WHERE oid = ${owner_oid}; -")" -if [[ "${owner_name}" != "workforce_validation_role" ]]; then - echo "registry table has unexpected owner: ${owner_name}" >&2 - exit 1 -fi - -owner_fk_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT count(*) FROM pg_constraint WHERE contype = 'f' AND confrelid = ${owner_oid}; -")" -if [[ "${owner_fk_count}" != "${legacy_fk_count}" ]]; then - echo "registry adoption broke existing FK dependencies: before=${legacy_fk_count} after=${owner_fk_count}" >&2 - exit 1 -fi - -rls_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT relrowsecurity, relforcerowsecurity -FROM pg_class -WHERE oid = ${owner_oid}; -")" -if [[ "${rls_flags}" != "t|t" ]]; then - echo "registry adoption did not preserve forced tenant RLS: ${rls_flags}" >&2 - exit 1 -fi - -bitemporal_trigger="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT count(*) -FROM pg_trigger -WHERE tgrelid = ${owner_oid} - AND tgname = 'validity_study_bitemporal_guard' - AND NOT tgisinternal; -")" -if [[ "${bitemporal_trigger}" != "1" ]]; then - echo "registry adoption lost the bitemporal mutation guard" >&2 - exit 1 -fi - -runtime_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolreplication, rolbypassrls -FROM pg_roles -WHERE rolname = 'workforce_validation_runtime_role'; -")" -if [[ "${runtime_flags}" != "f|f|f|f|f|f|f" ]]; then - echo "workforce_validation_runtime_role flags are not deny-default: ${runtime_flags}" >&2 - exit 1 -fi - -runtime_privileges="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SELECT - has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'USAGE'), - has_schema_privilege('workforce_validation_runtime_role', 'workforce_validation', 'CREATE'), - has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'SELECT'), - has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'INSERT'), - has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'UPDATE'), - has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'DELETE'), - has_table_privilege('workforce_validation_runtime_role', 'workforce_validation.validity_study', 'TRUNCATE'); -")" -if [[ "${runtime_privileges}" != "t|f|t|f|f|f|f" ]]; then - echo "runtime role privileges are not least-privilege read-only: ${runtime_privileges}" >&2 - exit 1 -fi - -missing_tenant_count="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SET ROLE workforce_validation_runtime_role; -SELECT count(*) FROM workforce_validation.validity_study; -RESET ROLE; -")" -if [[ "${missing_tenant_count}" != "0" ]]; then - echo "runtime read returned rows without tenant context: ${missing_tenant_count}" >&2 - exit 1 -fi - -tenant_read="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " -SET orgmetra.tenant_record_id = '${TENANT_ID}'; -SET ROLE workforce_validation_runtime_role; -SELECT validity_study_id::text FROM workforce_validation.validity_study; -RESET ROLE; -RESET orgmetra.tenant_record_id; -")" -if [[ "${tenant_read}" != "${STUDY_ID}" ]]; then - echo "runtime role did not read the tenant-scoped owner registry: ${tenant_read}" >&2 - exit 1 -fi From 090a7294bcbf3f976d2c247b4709b2a3079f2b21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:10:23 +0900 Subject: [PATCH 11/18] test(workforce-validation): cover both foreign target branches --- .../tests/test_postgres_registry.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/services/workforce-validation-api/tests/test_postgres_registry.py b/services/workforce-validation-api/tests/test_postgres_registry.py index c0793e8d..cfbe77f2 100644 --- a/services/workforce-validation-api/tests/test_postgres_registry.py +++ b/services/workforce-validation-api/tests/test_postgres_registry.py @@ -150,11 +150,15 @@ def test_invalid_row_scalar_is_reported_as_persistence_integrity_failure() -> No port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) -def test_foreign_row_target_fails_closed() -> None: - other_study = UUID("00000000-0000-7000-8000-0000000000c2") - port = PostgresValidityStudyReadPort( - connection_factory=_Factory([_row(validity_study_id=other_study)]) - ) +@pytest.mark.parametrize( + "row", + [ + _row(tenant_record_id=UUID("10000000-0000-7000-8000-000000000002")), + _row(validity_study_id=UUID("00000000-0000-7000-8000-0000000000c2")), + ], +) +def test_foreign_row_target_fails_closed(row: tuple[object, ...]) -> None: + port = PostgresValidityStudyReadPort(connection_factory=_Factory([row])) with pytest.raises(ValidityStudyIntegrityError, match="another target"): port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) From 3b5ebe8341711e72cf3bacc592af8eddae06c9f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:12:31 +0900 Subject: [PATCH 12/18] docs(workforce-validation): describe durable registry owner path --- services/workforce-validation-api/README.md | 24 +++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/services/workforce-validation-api/README.md b/services/workforce-validation-api/README.md index d3d84736..27823dc8 100644 --- a/services/workforce-validation-api/README.md +++ b/services/workforce-validation-api/README.md @@ -1,10 +1,10 @@ # Orgmetra Workforce Validation API -This package is the application boundary for the `workforce_validation` bounded context. The current slice exposes one purpose-bound read use case for the existing validity-study registry header and establishes the context-local PostgreSQL ownership bootstrap. +This package is the application and persistence boundary for the `workforce_validation` bounded context. The current stack exposes a purpose-bound validity-study registry read and adopts the existing registry table into the context-owned PostgreSQL schema without copying authoritative HR evidence. -It does **not** query People, Talent Acquisition, Performance Management, Job Architecture, Psychometrics Commons, fast-mlsirm, or TEPP tables. Those contexts remain separate owners. Exact foreign identifiers and immutable specialist result references cross the boundary only through published contracts. +It does **not** query People, Talent Acquisition, Performance Management, Job Architecture, Psychometrics Commons, fast-mlsirm, or TEPP tables. Those contexts remain separate owners. Exact foreign identifiers and immutable specialist result references cross the boundary only through published contracts or explicitly approved database constraints inside the modular deployment. -## Current slice +## Current read boundary `read_validity_study(...)`: @@ -22,15 +22,21 @@ It does **not** query People, Talent Acquisition, Performance Management, Job Ar `ValidityStudyView` is a data projection, not a durable authorization credential or cryptographic capability. Downstream consequential actions must perform their own purpose-bound authorization and authoritative re-resolution rather than treating the Python runtime type as reusable authority. Low-level interpreter construction is outside the supported public API and is not accepted as proof that authorization occurred. -`services/workforce-validation-api/database/migrations/0001_owner_schema.sql` starts this bounded context's own migration history. It creates the `workforce_validation` schema and deny-default `workforce_validation_role`, revokes public schema access, and intentionally creates or moves no application table yet. The role is a **NOLOGIN migration/schema owner only**; runtime principals must not be granted that owner role. PostgreSQL applies role-level configuration defaults at login and does not re-apply them on `SET ROLE`, so an `ALTER ROLE ... SET search_path` entry on this NOLOGIN role is not treated as a runtime isolation control. The later durable adapter must use a distinct least-privilege runtime role, schema-qualified `workforce_validation` relations, and explicit function-level `search_path` where `SECURITY DEFINER` code is introduced. +## PostgreSQL ownership -Protected foundation migrations still create validity-study tables in the legacy foundation schema, so the next forward-only persistence increment must adopt those records without normalizing `public.validity_study` as a long-lived service contract or breaking existing linkage evidence. +`database/migrations/0001_owner_schema.sql` creates the deny-default `workforce_validation` schema and `workforce_validation_role`. That role remains a **NOLOGIN migration/schema owner only**; runtime principals must not be granted it. PostgreSQL role-level configuration defaults are not treated as runtime isolation because `SET ROLE` does not re-apply login-time defaults. -Issue #234 owns the remaining order: durable owner-schema adoption and PostgreSQL adapter, idempotent registration, explicit predictor/sample/decision-policy/analysis-protocol versions, scientific adapters, OpenAPI/gateway exposure, and realistic p95 measurement. Issues #236–#244 retain the current bootstrap trust-boundary findings through exact-head acceptance and protected integration: persisted-record immutability, principal immutability and constructor revalidation, owner-role/runtime-role separation, inert repository-capability validation, immutable minimized output, non-public issuance of that output, detached UUID storage/target snapshots, and exact validation of UUID internal payloads before comparison. +`database/migrations/0002_registry_adoption.sql` is a forward-only adoption migration. It uses `ALTER TABLE public.validity_study SET SCHEMA workforce_validation`, so the existing relation OID, rows, indexes, foreign-key dependencies, forced tenant RLS policy, and bitemporal mutation trigger stay attached to the same table object. It creates a separate deny-default `workforce_validation_runtime_role`, grants only schema `USAGE`, registry `SELECT`, and the tenant-context helper required by the preserved RLS policy, and grants no registry mutation privilege. No `public.validity_study` compatibility view or second mutable registry is created. + +`PostgresValidityStudyReadPort` uses only `workforce_validation.validity_study` and `pg_catalog.set_config(...)`. It snapshots tenant/study UUID authority into immutable integer payloads before executable connection acquisition, reconstructs fresh UUID parameters, opens a read-only transaction, binds `orgmetra.tenant_record_id` transaction-locally, fetches at most two rows, and fails closed on duplicate, malformed, non-canonical, or foreign-target persistence results. Deployment code owns the actual login, pooling, TLS and assumption/grant of the runtime role; the adapter does not elevate itself with `SET ROLE`. + +The legacy decision/evidence/outcome links remain separate relations for now. Their existing foreign keys continue to reference the moved registry by relation identity, which PostgreSQL preserves across `SET SCHEMA`. Later increments must adopt the remaining `workforce_validation` relations deliberately rather than create cross-service SQL or duplicate the registry. + +Issue #234 owns the broader FR-007 order. Issue #247 owns this durable registry adoption/read-port slice. After its exact-head acceptance and the parent #235 protected integration, the next buyer/scientific work is idempotent validity-study registration, explicit predictor/sample/decision-policy/analysis-protocol versions, scientific adapters, versioned OpenAPI/gateway exposure, and realistic PostgreSQL-backed p95 evidence. ## Test -The Draft branch is admitted to the canonical Foundation quality workflow with the same hash-locked test toolchain and direct source-tree dependency policy used by the existing owner services: +The service remains in the canonical Foundation unit gate with the repository's hash-locked test toolchain: ```bash PYTHONPATH=services/workforce-validation-api/src:packages/keyverse-adapter/src \ @@ -39,6 +45,6 @@ PYTHONPATH=services/workforce-validation-api/src:packages/keyverse-adapter/src \ services/workforce-validation-api/tests ``` -The same Foundation job also runs `tests/test_workforce_validation_owner_schema_postgres.sh` in its own pinned PostgreSQL 16.14 container. That contract executes the service-local owner migration and checks the exact deny-default role flags, schema owner, absence of ineffective login-only `rolconfig`, actual `SET ROLE` search-path behavior, absence of inherited PUBLIC `USAGE`/`CREATE`, and absence of application relations in the bootstrap schema. The test intentionally demonstrates that `SET ROLE` retains the caller's existing `search_path`; runtime isolation therefore cannot be inferred from owner-role metadata. +`tests/test_workforce_validation_owner_schema_postgres.sh`, already admitted to the pinned PostgreSQL 16.14 Foundation lane, first proves the empty owner-schema bootstrap and then applies the foundation schema plus `0002_registry_adoption.sql`. It verifies preserved relation OID/FK dependencies/RLS/bitemporal guard, absence of `public.validity_study`, deny-default runtime-role flags, read-only privileges, no-row behavior without tenant context, and tenant-scoped owner reads. -Those source contracts are not terminal acceptance by themselves. The slice remains Draft until the exact current head actually executes with 100% owned statement/branch coverage, the PostgreSQL owner-schema contract is GREEN, applicable security workflows are terminal, and the normal review/governance requirements are satisfied. Only then may the next forward-only owner-table adoption and durable adapter be treated as eligible for integration. +Source contracts are not terminal acceptance by themselves. This child remains Draft while #235 is mutable and until, after parent integration/retarget, its exact head executes with 100% owned statement/branch coverage, the isolated PostgreSQL contract is GREEN, applicable security workflows are terminal, and normal review/governance requirements are satisfied. From 960b2aace97f3a6fa5a73c12415222cf9d4fa3e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:38:15 +0900 Subject: [PATCH 13/18] test(workforce-validation): lock PostgreSQL port dependency --- .../tests/test_postgres_registry.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/services/workforce-validation-api/tests/test_postgres_registry.py b/services/workforce-validation-api/tests/test_postgres_registry.py index cfbe77f2..0a2678ba 100644 --- a/services/workforce-validation-api/tests/test_postgres_registry.py +++ b/services/workforce-validation-api/tests/test_postgres_registry.py @@ -90,6 +90,20 @@ def test_constructor_rejects_non_callable_factory() -> None: PostgresValidityStudyReadPort(connection_factory=object()) # type: ignore[arg-type] +def test_connection_factory_cannot_be_replaced_after_port_validation() -> None: + original_factory = _Factory([]) + replacement_factory = _Factory([]) + port = PostgresValidityStudyReadPort(connection_factory=original_factory) + + with pytest.raises(AttributeError): + object.__setattr__(port, "connection_factory", replacement_factory) + + assert port.connection_factory is original_factory + assert port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) is None + assert original_factory.calls == 1 + assert replacement_factory.calls == 0 + + def test_invalid_target_is_rejected_before_connection_acquisition() -> None: factory = _Factory([]) port = PostgresValidityStudyReadPort(connection_factory=factory) From 5d390bc4e86c6db7bcf95a51662cc71500811ad3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 21:38:55 +0900 Subject: [PATCH 14/18] fix(workforce-validation): lock PostgreSQL port dependency --- .../postgres_registry.py | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py index 30605baa..0890b00d 100644 --- a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py +++ b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py @@ -9,7 +9,6 @@ from __future__ import annotations from contextlib import AbstractContextManager -from dataclasses import dataclass from typing import Any, Callable from uuid import UUID @@ -40,22 +39,33 @@ """.strip() -@dataclass(frozen=True, slots=True) -class PostgresValidityStudyReadPort: +class PostgresValidityStudyReadPort(tuple): """Read one current validity-study header under forced tenant RLS. ``connection_factory`` must return a DB-API-compatible connection context - manager configured by deployment code. The adapter snapshots UUID identity - before invoking that executable factory so retained caller UUID aliases cannot - change the authorized SQL target during connection acquisition. + manager configured by deployment code. Tuple-backed storage prevents a + retained port reference from replacing that accepted dependency after + validation. The adapter snapshots UUID identity before invoking the factory so + retained caller UUID aliases cannot change the authorized SQL target during + connection acquisition. """ - connection_factory: PostgresConnectionFactory + __slots__ = () - def __post_init__(self) -> None: - """Reject an unusable connection dependency before protected reads.""" - if not callable(self.connection_factory): + def __new__( + cls, + *, + connection_factory: PostgresConnectionFactory, + ) -> PostgresValidityStudyReadPort: + """Validate and structurally bind the executable connection dependency.""" + if not callable(connection_factory): raise TypeError("connection_factory must be callable") + return tuple.__new__(cls, (connection_factory,)) + + @property + def connection_factory(self) -> PostgresConnectionFactory: + """Return the structurally bound connection dependency.""" + return self[0] def read_validity_study( self, From 1644fe53cea801b4a270f38e58dc4a31ece27e3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 22:03:45 +0900 Subject: [PATCH 15/18] test(workforce-validation): cover case trigger after registry adoption --- ...kforce_validation_owner_schema_postgres.sh | 198 +++++++++++++++--- 1 file changed, 164 insertions(+), 34 deletions(-) diff --git a/tests/test_workforce_validation_owner_schema_postgres.sh b/tests/test_workforce_validation_owner_schema_postgres.sh index 956edb16..8642d294 100644 --- a/tests/test_workforce_validation_owner_schema_postgres.sh +++ b/tests/test_workforce_validation_owner_schema_postgres.sh @@ -76,49 +76,154 @@ fi psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -qc "DROP ROLE workforce_validation_public_probe;" trap - EXIT -# The adoption phase runs after the protected foundation migration chain has -# created the legacy registry. Keeping it in this already-admitted isolated -# contract avoids a second CI orchestration path while preserving the bootstrap -# assertion above as an independently observable prerequisite. -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f database/migrations/0001_foundation_schema.sql +# Adoption must be proven against the protected migration state that already owns +# the normalized validity-study case trigger. A table move is not complete if +# schema-qualified SQL inside that trigger still names the legacy relation. +for migration in \ + database/migrations/0001_foundation_schema.sql \ + database/migrations/0002_sealed_evidence_digest.sql \ + database/migrations/0003_audit_outbox_persistence.sql \ + database/migrations/0004_outbox_delivery_claim.sql \ + database/migrations/0005_outbox_delivery_finalization.sql \ + database/migrations/0006_outbox_delivery_dead_letter.sql \ + database/migrations/0007_outbox_retry_exhaustion.sql \ + database/migrations/0008_audit_outbox_review_hardening.sql \ + database/migrations/0009_candidate_worker_conversion_governance.sql \ + database/migrations/0010_validity_study_case_integrity.sql; do + psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -f "${migration}" +done TENANT_ID="10000000-0000-7000-8000-000000000001" -JOB_ID="00000000-0000-7000-8000-0000000000a1" -CRITERION_ID="00000000-0000-7000-8000-0000000000b1" +PERSON_ID="00000000-0000-7000-8000-000000000001" +EMPLOYMENT_ID="00000000-0000-7000-8000-000000000011" +JOB_ID="00000000-0000-7000-8000-000000000021" +CANDIDATE_ID="00000000-0000-7000-8000-000000000031" +EVIDENCE_ID="00000000-0000-7000-8000-000000000041" +DECISION_ID="00000000-0000-7000-8000-000000000051" +AUDIT_ID="00000000-0000-7000-8000-000000000062" +OUTBOX_ID="00000000-0000-7000-8000-000000000072" +CONVERSION_ID="00000000-0000-7000-8000-000000000081" +CYCLE_ID="00000000-0000-7000-8000-000000000091" +CRITERION_ID="00000000-0000-7000-8000-0000000000a1" +OBSERVATION_ID="00000000-0000-7000-8000-0000000000b1" STUDY_ID="00000000-0000-7000-8000-0000000000c1" +CASE_ID="00000000-0000-7000-8000-0000000000e4" -psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 +if [[ "${legacy_fk_count}" != "4" ]]; then + echo "unexpected protected FK dependency count before adoption: ${legacy_fk_count}" >&2 exit 1 fi @@ -184,6 +289,31 @@ if [[ "${bitemporal_trigger}" != "1" ]]; then exit 1 fi +# Regression for #251: the normalized case-governance trigger must remain +# executable after the registry table moves out of public. The predecessor +# function body names public.validity_study and therefore fails this insert. +tenant_psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 <&2 + exit 1 +fi + runtime_flags="$(psql "${DATABASE_URL}" -v ON_ERROR_STOP=1 -Atqc " SELECT rolcanlogin, rolsuper, rolcreatedb, rolcreaterole, rolinherit, rolreplication, rolbypassrls FROM pg_roles From b210b6bd201a9b62949da33bc4a9feb92ec6a095 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 22:04:26 +0900 Subject: [PATCH 16/18] fix(workforce-validation): preserve case trigger after registry adoption --- .../migrations/0002_registry_adoption.sql | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql b/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql index 591b50dc..cb87fadc 100644 --- a/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql +++ b/services/workforce-validation-api/database/migrations/0002_registry_adoption.sql @@ -15,6 +15,184 @@ CREATE ROLE workforce_validation_runtime_role NOLOGIN ALTER TABLE public.validity_study SET SCHEMA workforce_validation; ALTER TABLE workforce_validation.validity_study OWNER TO workforce_validation_role; +-- The protected validity-study case trigger predates owner-schema adoption and its +-- PL/pgSQL body names public.validity_study explicitly. ALTER TABLE ... SET SCHEMA +-- preserves the trigger/function objects but cannot rewrite relation names embedded +-- in function source. Replace the existing function in place so normalized case +-- governance continues to read the same registry relation after ownership moves. +CREATE OR REPLACE FUNCTION public.validate_validity_study_case() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp +AS $$ +DECLARE + study_criterion_id uuid; + study_recorded_from timestamptz; + study_recorded_to timestamptz; + criterion_job_id uuid; + decision_candidate_id uuid; + decision_job_id uuid; + decision_evidence_id uuid; + decision_recorded_at timestamptz; + evidence_sealed_at timestamptz; + evidence_sealed_decision_id uuid; + outcome_criterion_id uuid; + outcome_person_id uuid; + outcome_recorded_from timestamptz; + outcome_recorded_to timestamptz; + conversion_candidate_id uuid; + conversion_person_id uuid; + conversion_decision_id uuid; + conversion_recorded_from timestamptz; + conversion_recorded_to timestamptz; +BEGIN + SELECT + study.criterion_blueprint_id, + study.recorded_from, + study.recorded_to, + criterion.job_profile_id + INTO + study_criterion_id, + study_recorded_from, + study_recorded_to, + criterion_job_id + FROM workforce_validation.validity_study AS study + JOIN public.criterion_blueprint AS criterion + ON criterion.tenant_record_id = study.tenant_record_id + AND criterion.criterion_blueprint_id = study.criterion_blueprint_id + WHERE study.tenant_record_id = NEW.tenant_record_id + AND study.validity_study_id = NEW.validity_study_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'validity-study case requires a tenant-local study and criterion' + USING ERRCODE = '23503'; + END IF; + + IF NEW.linked_at < study_recorded_from + OR (study_recorded_to IS NOT NULL AND NEW.linked_at >= study_recorded_to) THEN + RAISE EXCEPTION 'validity-study case must bind a study version visible at linked_at' + USING ERRCODE = '23514'; + END IF; + + SELECT + decision.candidate_profile_id, + decision.job_profile_id, + decision.decision_evidence_set_id, + decision.recorded_at + INTO + decision_candidate_id, + decision_job_id, + decision_evidence_id, + decision_recorded_at + FROM public.selection_decision AS decision + WHERE decision.tenant_record_id = NEW.tenant_record_id + AND decision.selection_decision_id = NEW.selection_decision_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'validity-study case requires a tenant-local selection decision' + USING ERRCODE = '23503'; + END IF; + + IF decision_job_id IS DISTINCT FROM criterion_job_id THEN + RAISE EXCEPTION 'validity-study case decision belongs to a different Job' + USING ERRCODE = '23514'; + END IF; + + IF decision_evidence_id IS DISTINCT FROM NEW.decision_evidence_set_id THEN + RAISE EXCEPTION 'validity-study case requires the selection decision''s exact evidence set' + USING ERRCODE = '23514'; + END IF; + + SELECT evidence.sealed_at, evidence.sealed_selection_decision_id + INTO evidence_sealed_at, evidence_sealed_decision_id + FROM public.decision_evidence_set AS evidence + WHERE evidence.tenant_record_id = NEW.tenant_record_id + AND evidence.decision_evidence_set_id = NEW.decision_evidence_set_id; + + IF NOT FOUND + OR evidence_sealed_at IS NULL + OR evidence_sealed_decision_id IS DISTINCT FROM NEW.selection_decision_id THEN + RAISE EXCEPTION 'validity-study case requires evidence sealed by the exact selection decision' + USING ERRCODE = '23514'; + END IF; + + SELECT + observation.criterion_blueprint_id, + observation.person_record_id, + observation.recorded_from, + observation.recorded_to + INTO + outcome_criterion_id, + outcome_person_id, + outcome_recorded_from, + outcome_recorded_to + FROM public.criterion_observation AS observation + WHERE observation.tenant_record_id = NEW.tenant_record_id + AND observation.criterion_observation_id = NEW.criterion_observation_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'validity-study case requires a tenant-local criterion observation' + USING ERRCODE = '23503'; + END IF; + + IF outcome_criterion_id IS DISTINCT FROM study_criterion_id THEN + RAISE EXCEPTION 'validity-study case outcome uses a different criterion' + USING ERRCODE = '23514'; + END IF; + + SELECT + conversion.candidate_profile_id, + conversion.person_record_id, + conversion.selection_decision_id, + conversion.recorded_from, + conversion.recorded_to + INTO + conversion_candidate_id, + conversion_person_id, + conversion_decision_id, + conversion_recorded_from, + conversion_recorded_to + FROM public.candidate_worker_conversion_record AS conversion + WHERE conversion.tenant_record_id = NEW.tenant_record_id + AND conversion.candidate_worker_conversion_record_id = + NEW.candidate_worker_conversion_record_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'validity-study case requires a governed candidate-worker conversion' + USING ERRCODE = '23503'; + END IF; + + IF conversion_decision_id IS DISTINCT FROM NEW.selection_decision_id + OR conversion_candidate_id IS DISTINCT FROM decision_candidate_id THEN + RAISE EXCEPTION 'validity-study case conversion does not bind the selected candidate' + USING ERRCODE = '23514'; + END IF; + + IF conversion_person_id IS DISTINCT FROM outcome_person_id THEN + RAISE EXCEPTION 'validity-study case outcome belongs to a different worker' + USING ERRCODE = '23514'; + END IF; + + IF NEW.linked_at < decision_recorded_at + OR NEW.linked_at < evidence_sealed_at + OR NEW.linked_at < outcome_recorded_from + OR ( + outcome_recorded_to IS NOT NULL + AND NEW.linked_at >= outcome_recorded_to + ) + OR NEW.linked_at < conversion_recorded_from + OR ( + conversion_recorded_to IS NOT NULL + AND NEW.linked_at >= conversion_recorded_to + ) THEN + RAISE EXCEPTION 'validity-study case may use only evidence visible at linked_at' + USING ERRCODE = '23514'; + END IF; + + RETURN NEW; +END; +$$; + REVOKE ALL ON TABLE workforce_validation.validity_study FROM PUBLIC; GRANT USAGE ON SCHEMA workforce_validation TO workforce_validation_runtime_role; GRANT SELECT ON TABLE workforce_validation.validity_study TO workforce_validation_runtime_role; From ceb72ff997fdf54640a146c8569b0ee225312507 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 23:07:49 +0900 Subject: [PATCH 17/18] test(workforce-validation): bind stored connection capability --- ...test_postgres_registry_bound_capability.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 services/workforce-validation-api/tests/test_postgres_registry_bound_capability.py diff --git a/services/workforce-validation-api/tests/test_postgres_registry_bound_capability.py b/services/workforce-validation-api/tests/test_postgres_registry_bound_capability.py new file mode 100644 index 00000000..b4c711af --- /dev/null +++ b/services/workforce-validation-api/tests/test_postgres_registry_bound_capability.py @@ -0,0 +1,74 @@ +"""Regression contract for the bound PostgreSQL connection capability.""" + +from __future__ import annotations + +from uuid import UUID + +from orgmetra_workforce_validation_api.postgres_registry import PostgresValidityStudyReadPort + +TENANT = UUID("10000000-0000-7000-8000-000000000001") +STUDY = UUID("00000000-0000-7000-8000-0000000000c1") + + +class _Cursor: + """Provide the smallest DB-API cursor needed by an empty registry read.""" + + def __enter__(self) -> _Cursor: + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + del exc_type, exc, traceback + + def execute(self, sql: str, params: object = None) -> None: + del sql, params + + def fetchmany(self, size: int) -> list[object]: + del size + return [] + + +class _Connection: + """Expose one deterministic cursor through the connection context protocol.""" + + def __enter__(self) -> _Connection: + return self + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: + del exc_type, exc, traceback + + def cursor(self) -> _Cursor: + return _Cursor() + + +class _Factory: + """Count connection acquisition without performing external I/O.""" + + def __init__(self) -> None: + self.calls = 0 + + def __call__(self) -> _Connection: + self.calls += 1 + return _Connection() + + +ORIGINAL_FACTORY = _Factory() +REPLACEMENT_FACTORY = _Factory() + + +class _SwitchingPort(PostgresValidityStudyReadPort): + """Present a different dynamic property than the callable stored at construction.""" + + @property + def connection_factory(self) -> _Factory: + return REPLACEMENT_FACTORY + + +def test_inherited_read_uses_exact_factory_stored_by_base_constructor() -> None: + """A subclass property must not replace the already-validated executable dependency.""" + ORIGINAL_FACTORY.calls = 0 + REPLACEMENT_FACTORY.calls = 0 + port = _SwitchingPort(connection_factory=ORIGINAL_FACTORY) + + assert port.read_validity_study(tenant_record_id=TENANT, validity_study_id=STUDY) is None + assert ORIGINAL_FACTORY.calls == 1 + assert REPLACEMENT_FACTORY.calls == 0 From d54d44d795444df572efbb301a667d74ac574d58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 23:08:22 +0900 Subject: [PATCH 18/18] fix(workforce-validation): use structurally bound connection factory --- .../orgmetra_workforce_validation_api/postgres_registry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py index 0890b00d..5dc604ae 100644 --- a/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py +++ b/services/workforce-validation-api/src/orgmetra_workforce_validation_api/postgres_registry.py @@ -64,8 +64,8 @@ def __new__( @property def connection_factory(self) -> PostgresConnectionFactory: - """Return the structurally bound connection dependency.""" - return self[0] + """Return the exact callable stored by the validating base constructor.""" + return tuple.__getitem__(self, 0) def read_validity_study( self, @@ -74,13 +74,14 @@ def read_validity_study( validity_study_id: UUID, ) -> ValidityStudyRecord | None: """Return one exact current owner record or ``None`` for the tenant target.""" + connection_factory = tuple.__getitem__(self, 0) tenant_identity = _store_operational_uuid("tenant_record_id", tenant_record_id) study_identity = _store_operational_uuid("validity_study_id", validity_study_id) sql_tenant_id = _restore_operational_uuid("tenant_record_id", tenant_identity) sql_study_id = _restore_operational_uuid("validity_study_id", study_identity) - with self.connection_factory() as connection: + with connection_factory() as connection: with connection.cursor() as cursor: cursor.execute(_READ_ONLY_SQL) cursor.execute(_TENANT_CONTEXT_SQL, (str(sql_tenant_id),))