Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
defee26
test(workforce-validation): require owner-schema PostgreSQL read port
seonghobae Sep 5, 2026
712ca77
test(workforce-validation): require forward-only registry adoption
seonghobae Sep 5, 2026
55ddef2
test(workforce-validation): make tenant-role probe output deterministic
seonghobae Sep 5, 2026
f903c2b
feat(workforce-validation): adopt validity registry into owner schema
seonghobae Sep 5, 2026
000d9e6
feat(workforce-validation): add tenant-bound PostgreSQL registry adapter
seonghobae Sep 5, 2026
66571e7
feat(workforce-validation): publish PostgreSQL owner adapter
seonghobae Sep 5, 2026
ebf08af
ci(workforce-validation): admit registry adoption contract
seonghobae Sep 5, 2026
55afd35
ci(workforce-validation): preserve sealed Foundation workflow
seonghobae Sep 5, 2026
a64610f
test(workforce-validation): extend canonical PostgreSQL owner acceptance
seonghobae Sep 5, 2026
b13b0c0
test(workforce-validation): consolidate adoption into admitted Postgr…
seonghobae Sep 5, 2026
090a729
test(workforce-validation): cover both foreign target branches
seonghobae Sep 5, 2026
3b5ebe8
docs(workforce-validation): describe durable registry owner path
seonghobae Sep 5, 2026
960b2aa
test(workforce-validation): lock PostgreSQL port dependency
seonghobae Sep 5, 2026
5d390bc
fix(workforce-validation): lock PostgreSQL port dependency
seonghobae Sep 5, 2026
1644fe5
test(workforce-validation): cover case trigger after registry adoption
seonghobae Sep 5, 2026
b210b6b
fix(workforce-validation): preserve case trigger after registry adoption
seonghobae Sep 5, 2026
ceb72ff
test(workforce-validation): bind stored connection capability
seonghobae Sep 5, 2026
d54d44d
fix(workforce-validation): use structurally bound connection factory
seonghobae Sep 5, 2026
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
24 changes: 15 additions & 9 deletions services/workforce-validation-api/README.md
Original file line number Diff line number Diff line change
@@ -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(...)`:

Expand All @@ -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 \
Expand All @@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
-- 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;

-- 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;
GRANT EXECUTE ON FUNCTION public.current_tenant_record_id() TO workforce_validation_runtime_role;

COMMIT;
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,6 +12,7 @@
)

__all__ = [
"PostgresValidityStudyReadPort",
"ValidationPrincipal",
"ValidityStudyIntegrityError",
"ValidityStudyNotFound",
Expand Down
Loading