Skip to content
Draft
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
61 changes: 61 additions & 0 deletions .github/workflows/employment-history-postgres-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Employment History PostgreSQL Read Quality

on:
pull_request:
branches:
- develop
- feat/employment-history-http-read
paths:
- "services/people-api/**"
- "packages/hris-kernel/**"
- "packages/keyverse-adapter/**"
- ".github/requirements/foundation-test.txt"
- ".github/workflows/employment-history-postgres-quality.yml"
- "database/migrations/0001_foundation_schema.sql"
- "docs/adr/0156-employment-history-postgres-read.md"
- "docs/doctoring/postgres-employment-history-read-references.md"
- "docs/traceability/postgres-employment-history-read.md"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: employment-history-postgres-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
unit:
name: PostgreSQL Employment-history read contract
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout exact candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- name: Prove exact candidate checkout
env:
ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA"
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
check-latest: false
- name: Install reviewed test toolchain
run: |
python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt
python -m pip check
- name: Compile People API boundary
run: python -m compileall -q services/people-api/src packages/hris-kernel/src packages/keyverse-adapter/src services/people-api/tests
- name: Test governed People contracts with exact statement and branch coverage
env:
PYTHONPATH: services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src
COVERAGE_FILE: /tmp/orgmetra-employment-history-postgres.coverage
run: python -m pytest -c services/people-api/pyproject.toml services/people-api/tests
- name: Require clean checkout
run: |
git diff --exit-code
test -z "$(git status --porcelain)"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to Orgmetra will be documented in this file.

- Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies.
- Active stacked PR #155 adds the customer-callable `EmploymentHistoryAsgiApp` read route, `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}/employment-history`, with exact UTC knowledge-cutoff parsing, purpose/field authorization through the existing Employment-history service, minimized entries, no-store response controls, client-safe errors, a published OpenAPI 3.2 contract, and a dedicated exact-head 100% People API quality workflow. It does not mutate Employment or make an employment decision.
- Active stacked PR #156 adds `PostgresEmploymentHistoryReadPort` behind the existing Employment-history service boundary. It provides exact operational identity/time validation, read-only tenant-scoped parameterized bitemporal SQL, explicit UTC timestamp projection, malformed-row and visibility rechecks, immutable typed results, a dedicated exact-head 100% People API quality workflow, and real PostgreSQL 16.14 validation. It does not authorize fields, mutate HRIS truth, or make an employment decision.
- Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision.
- Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate.
- Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries.
Expand Down
2 changes: 2 additions & 0 deletions docs/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Employment-history service. The response contains only authorized
and maps integrity or unexpected backend failures to client-safe opaque support
references. The route adds no mutation or employment-decision authority.

`PostgresEmploymentHistoryReadPort` is a persistence defense-in-depth boundary, not an authorization substitute. It validates exact operational tenant/Person identities and UTC knowledge time before connection acquisition, uses a read-only transaction and transaction-local tenant setting, restricts the query to the canonical Employment tables, and rechecks returned identity and recorded-time visibility. It accepts no purpose, scope, token, or raw credential and therefore cannot widen the parent service's purpose-bound field decision.

## Mutation security contract

Every mutating HTTP operation and its server-side command handler requires one validated `Idempotency-Key` that crosses the command boundary into durable transactional replay state. The published OpenAPI employment, position, assignment, person, job-profile, and selection-decision command families require `X-Tenant-Reference`, `X-Actor-Reference`, and `X-Purpose-Code`; those values must match the authenticated Keyverse principal and the operation-specific least-privilege scope. The executable People mutation handlers added on this branch currently implement employment, position, and assignment creation with those headers. Person, job-profile, and selection-decision remain published foundation API contracts until their server handlers are integrated; their OpenAPI presence is not runtime evidence. Confirmed-hire materialization instead binds the tenant in `/v1/tenants/{tenant_record_id}/candidate-worker-conversions`, the business purpose in its exact query parameter, and the actor through the authenticated principal. It does not accept weaker duplicate actor/tenant/purpose header authorities.
Expand Down
1 change: 1 addition & 0 deletions docs/TEST_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ The command runs Python repository-integrity validation, the dependency-free Nod
| Governed People mutation idempotency: tenant/route/key uniqueness, identical-command replay, changed-command rejection, rollback safety, append-only/TRUNCATE protection, forced RLS and concurrent exact-key serialization | `bash tests/test_people_mutation_idempotency_postgres.sh` against PostgreSQL 16 in Foundation CI |
| Tenant/actor/purpose authorization matrix and negative high-impact commands | service-specific unit and integration test commands recorded in each service package |
| Employment-history HTTP read parsing, authentication order, purpose/field authorization, bitemporal cutoff forwarding, response minimization, client-safe errors, and exact 100% statement/branch coverage | `PYTHONPATH=services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src python -m pytest -c services/people-api/pyproject.toml services/people-api/tests` and `.github/workflows/employment-history-http-quality.yml` |
| Employment-history PostgreSQL adapter identity/time validation, read-only transaction ordering, tenant context, explicit bitemporal SQL, UTC projection, DB-API integrity, immutable typed output, and seeded PostgreSQL 16.14 execution | `PYTHONPATH=services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src python -m pytest -c services/people-api/pyproject.toml services/people-api/tests`, isolated PostgreSQL 16.14 validation, and `.github/workflows/employment-history-postgres-quality.yml` |
| AsyncAPI/CloudEvents envelope compatibility | provider and consumer contract test commands recorded beside the versioned event schema |
| External adapter timeout, malformed response, tenant mismatch, and unavailable-state handling | fake-server tests in each adapter package |
| Role-workspace keyboard, focus, exact-value, permission-denied, and confirmation states | Storybook interaction/a11y tests plus browser E2E for the owning workspace |
Expand Down
1 change: 1 addition & 0 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| Governed candidate-to-worker conversion | Talent Acquisition / People core | `candidate_worker_conversion_record` with candidate, person, employment, selection decision, audit event and outbox evidence | PostgreSQL exact hire/evidence/audit-envelope binding, correction provenance, tenant RLS, legacy-write rejection and bitemporal history contract | ADR-0001, ADR-0003, ADR-0006 | implemented_on_protected_main |
| GET-only People API | People API / purpose-bound read boundary | `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}`, `read_worker_people_record()`, `PostgresPeopleReadPort` | People API HTTP and PostgreSQL read contracts with exact 100% owned statement/branch coverage; current conversion lineage; no mutation writes | ADR-0002, ADR-0008 | implemented_on_protected_main |
| Purpose-bound Employment-history HTTP read | People API / customer read boundary | `GET /v1/tenants/{tenant_record_id}/people/{person_record_id}/employment-history`, `EmploymentHistoryAsgiApp`, `read_employment_history()`, `EmploymentHistoryReadPort` | exact path/query validation before authentication; Keyverse scope and purpose/field authorization; UTC bitemporal cutoff; minimized entries; client-safe 400/401/403/409/500 errors; exact 100% People API statement/branch coverage | ADR-0008, ADR-0149, ADR-0155 | implemented_on_active_pr |
| Canonical Employment-history PostgreSQL read | People API / HRIS persistence boundary | `PostgresEmploymentHistoryReadPort`, `employment_record`, `employment_record_version` | exact operational identity/time validation; read-only transaction and tenant context; parameterized bitemporal SQL; UTC projection; malformed-row, target-mismatch, cutoff, immutable-result, and real PostgreSQL 16.14 regressions; exact 100% People API statement/branch coverage | ADR-0003, ADR-0008, ADR-0156 | implemented_on_active_pr |
| Governed People writes and confirmed-hire materialization | People API / purpose-bound mutation boundary | `POST /v1/employment-records`, `POST /v1/position-records`, `POST /v1/assignment-records`, `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions`, `people_mutation_idempotency_record` | People command/HTTP/PostgreSQL contracts with exact owned statement/branch coverage plus PostgreSQL tenant-RLS, atomic audit/outbox/idempotency, identical-retry replay, changed-command rejection, rollback, and concurrent-key regression | ADR-0002, ADR-0006, ADR-0008 | implemented_on_protected_main |
| Evidence-grounded Job analysis with governed Task/FJA/KSAO persistence | Job Analysis / Workforce Validation | `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `FunctionalJobAnalysisProfile`, `TaskKSAOLink`, `EvidenceSource`, `job_analysis_snapshot`, `job_analysis_task_item`, `job_analysis_ksao_item`, `job_analysis_task_ksao_link`, `job_analysis_write_command`, `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots`, `GET /v1/tenants/{tenant_record_id}/job-analysis-snapshots/{analysis_record_id}` | domain tenant/Job isolation, source/version/digest provenance, task-KSAO completeness, deterministic canonicalization, accountable human-review and LLM-draft-only regressions; migration 0013 PostgreSQL parent-scope/RLS/append-only/idempotency/audit-outbox persistence; exact route/OpenAPI/error contracts and 100% owned service statement/branch coverage | ADR-0007, ADR-0014 | implemented_on_active_pr |
| Job-, cycle-, and staffing-scoped performance criterion observations | Performance / Workforce Validation | `criterion_observation`, `criterion_blueprint`, `performance_cycle`, `assignment_record`, `employment_record_version`, `position_record`, `position_record_version` | PostgreSQL wrong-Job, pre-assignment, out-of-cycle, frozen-Position, terminated-employment, closed-recorded-time, and session-TimeZone/UTC-midnight rejection plus valid worker-Job/staffing acceptance | ADR-0009 | implemented_on_protected_main |
Expand Down
48 changes: 48 additions & 0 deletions docs/adr/0156-employment-history-postgres-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ADR 0156: Read Employment history from canonical PostgreSQL truth

- **Status:** Proposed on active stacked PR #156; not protected-main truth until integrated
- **Date:** 2026-08-30
- **Owners:** Orgmetra People API / HRIS persistence
- **Extends:** ADR 0003 (bitemporal HRIS data), ADR 0008 (purpose-bound PII authorization), ADR 0149 (Employment-history read contract), ADR 0155 (Employment-history HTTP read)

## Context

PR #155 exposes the customer-callable Employment-history HTTP boundary but keeps persistence injected. An integrated deployment still needs one canonical adapter for normalized `employment_record` and `employment_record_version` truth; otherwise each host could supply persistence code with different tenant or system-time semantics.

The adapter is not an authorization engine or a second source of truth. The parent People service authorizes before calling it and revalidates its typed output before disclosure. The existing schema owns Employment identity, Person binding, bitemporal version facts, tenant RLS, and immutable-history guards.

## Decision

Add `PostgresEmploymentHistoryReadPort` as the PostgreSQL implementation of the `EmploymentHistoryReadPort` protocol.

The adapter:

1. validates exact operational tenant/Person UUIDs and an exact built-in UTC `known_at` before acquiring a connection;
2. opens one `READ COMMITTED, READ ONLY` transaction and sets the transaction-local tenant context before the protected query;
3. joins only Orgmetra-owned `employment_record_version` to its `employment_record` anchor, preserving Person scope without joining another bounded context's application tables;
4. applies explicit tenant, Person, parent-recorded, and version-recorded half-open predicates;
5. projects recorded timestamps with `AT TIME ZONE 'UTC'`, accepts only exact naive UTC DB projections, and attaches built-in UTC after validation; and
6. treats DB-API output as untrusted by checking the default list collection, exact tuple row shape, domain reconstruction, requested target identity, and knowledge-cutoff visibility before returning an immutable tuple.

Purpose-bound field authorization remains in the parent service. This adapter performs no mutation, audit/outbox write, foreign-service call, disclosure, or high-impact employment decision.

## Consequences

### Positive

- The Employment-history application contract can use canonical normalized PostgreSQL truth without host-specific persistence code.
- Read-only transaction mode, explicit predicates, and tenant context provide layered database scope controls.
- Person, Employment identity, and Employment-version history remain separate while business-effective time stays distinct from system-recorded visibility.
- Exact DB timestamp validation prevents driver/session timezone behavior from changing evidence meaning.

### Trade-offs

- The adapter is PostgreSQL/DB-API specific and intentionally requires the default tuple-row contract.
- Database RLS and bitemporal constraints still require independent PostgreSQL tests; this adapter does not claim SQL predicates replace authorization or schema constraints.
- The parent service must continue to revalidate rows before serialization.

## Verification

The contract-first child test head `1a8b9fb7` fails during collection while the adapter module is absent. The final child must show exact-current-head full People API coverage, invalid-input zero-connection behavior, transaction ordering, explicit SQL scope, UTC projection, malformed-row rejection, target/visibility rechecks, immutable results, a real PostgreSQL 16.14 seeded-database validation, and a clean checkout. Parent #155 evidence does not transfer.

The implementation follows PostgreSQL transaction access-mode guidance and the existing protected Orgmetra RLS contract. These controls are defense in depth and do not authorize a merge or protected-main representation while this PR is Draft or central gates lack authoritative verdicts.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
| [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on active implementation branch |
| [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on active implementation branch |
| [0155](0155-employment-history-http-read.md) | Expose governed Employment history through a read-only HTTP boundary | Proposed on active stacked PR #155 |
| [0156](0156-employment-history-postgres-read.md) | Read Employment history from canonical PostgreSQL truth | Proposed on active stacked PR #156 |
Loading