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
64 changes: 64 additions & 0 deletions docs/adr/0153-postgres-position-history-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ADR 0153: Read Position history from canonical PostgreSQL truth

- **Status:** Proposed on active stacked PR #153; not protected `develop` 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 0152 (Position-history read contract)

## Context

PR #152 defines the buyer-facing, purpose-bound Position-history read but intentionally injects its persistence port. An integrated application still needs a canonical adapter for normalized `position_record` and `position_record_version` truth; otherwise each deployment would supply bespoke persistence code and could silently widen the read.

The adapter is not a second authorization engine or source of truth. The parent People service authorizes before calling it and revalidates its typed output before disclosure. The existing schema already owns Position/Job lineage, bitemporal version facts, tenant RLS, and immutable-history guards.

A PostgreSQL statement that says `SET TRANSACTION ... READ ONLY` is not sufficient proof by itself. The adapter also uses transaction-local tenant context (`set_config(..., true)`), so both controls require an actual non-autocommit transaction. Likewise, validating a connection factory and then retaining it in a writable instance slot would create a checked-versus-used capability gap, and retaining caller UUID objects after validation would allow alias mutation between validation and SQL execution.

## Decision

Add `PostgresPositionHistoryReadPort` as the canonical PostgreSQL implementation of the `PositionHistoryReadPort` protocol introduced by PR #152.

The adapter:

1. validates exact operational tenant/Position UUIDs and an exact built-in UTC `known_at` before acquiring a connection;
2. immediately reduces tenant and Position UUIDs to exact built-in integer scalar authority, then reconstructs fresh UUID views only for DB-API parameter contracts;
3. stores the validated `connection_factory` in immutable tuple payload rather than a writable dataclass slot and uses that exact capability for the read;
4. requires the acquired connection to prove `autocommit is False` before cursor creation; absent, truthy, numeric-zero, string, or otherwise unproven modes fail closed;
5. only after that proof executes `SET TRANSACTION ISOLATION LEVEL READ COMMITTED, READ ONLY`, sets transaction-local tenant context, and runs the protected SELECT inside the same connection context;
6. joins only `public.position_record_version` to its Orgmetra-owned `public.position_record` anchor, preserving Job and organization lineage without Person, Employment, Assignment, compensation, candidate, performance, credential, or decision joins;
7. applies explicit tenant, Position, parent-recorded, and version-recorded half-open predicates;
8. projects recorded timestamps with `AT TIME ZONE 'UTC'`, accepts only exact naive UTC DB projections, and attaches built-in UTC after validation;
9. treats DB-API output as untrusted by checking the default list collection, exact tuple row shape, parent-record integrity, requested target identity, and knowledge-cutoff visibility before returning an immutable tuple;
10. compares returned tenant/Position scalar authority to the detached request scalars, so post-validation mutation of caller UUID aliases cannot change the authorized query target.

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

## Workflow ownership

Repository workflow consolidation on protected `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f` makes Foundation CI the repository acceptance owner. The pre-consolidation `.github/workflows/position-history-postgres-read-quality.yml` is not carried forward by the semantic restack. Stacked feature heads are not treated as GREEN merely because their parent or predecessor once passed a feature-local workflow.

## Consequences

### Positive

- The Position-history application contract can use canonical normalized PostgreSQL truth without host-specific persistence code.
- The read-only and transaction-local tenant controls are only attempted after the connection proves a non-autocommit transaction mode.
- The connection capability used by the adapter cannot be swapped through ordinary or `object.__setattr__` instance mutation after construction.
- Caller-retained UUID aliases cannot change tenant/Position SQL parameters after validation.
- Explicit predicates and forced-RLS tenant context provide layered database scope controls.
- Position, Job, and Assignment remain separate concepts, and business-effective history remains distinct from system-recorded visibility.
- Exact DB timestamp validation prevents driver/session timezone behavior from silently changing evidence meaning.

### Trade-offs

- The adapter is PostgreSQL/DB-API specific and intentionally requires the default tuple-row contract.
- Compatible connection objects must expose `autocommit` and prove it with the exact built-in value `False`; implicit or driver-specific lookalikes are rejected at this high-trust boundary.
- Database RLS and bitemporal constraints still require independent PostgreSQL tests; this adapter does not claim that SQL predicates replace authorization or schema constraints.
- The parent service must be integrated first and must continue to revalidate rows before serialization.

## Verification

The original contract-first child head `bf93924e` established the adapter boundary while the module was absent. Historical local and isolated PostgreSQL checks from the pre-consolidation branch remain development evidence only; they are not current exact-head merge evidence.

After PR #152 moved to its protected-workflow-consolidated and retained-authority-hardened head, ordinary two-parent reconciliation `a8edc7d2fa69842d4515def5c8a3e710ed4b4e2b` restacked the adapter without force push and deliberately omitted the stale feature-local quality workflow. Test-only head `800b783a74594a59f4c0d5dd819a6fce4fced4b8` added four contracts: immutable connection capability, explicit rejection of `autocommit=True`, rejection of unproven transaction modes, and request UUID alias detachment before connection acquisition. Production repair `00d8f58d0863ef0d3270c7197d41856de3131e34` implements those boundaries, and `d0a8b9b9b5e43bbc522b97c921a1d67c51b5fb6c` updates the ordinary test fixture to state its non-autocommit contract explicitly.

Because #153 remains stacked on a feature branch, no hosted RED or GREEN is inferred from the absence of PR-triggered Foundation runs. The final descendant must first inherit an integrated/protected parent, retarget to `develop`, and then obtain fresh exact-current-head Foundation/Security/SAST/CodeQL evidence plus qualifying independent review. Parent #152 and predecessor evidence do not transfer.
25 changes: 25 additions & 0 deletions docs/doctoring/postgres-position-history-read-references.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# PostgreSQL Position-history read references

**Scope:** Standards and research basis for active PR #153. This file does not claim certification or protected-main integration.

## APA 7 references

Joint Task Force. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53, Revision 5). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-53r5

Klyne, G., & Newman, C. (2002). *Date and time on the Internet: Timestamps* (RFC 3339). RFC Editor. https://doi.org/10.17487/RFC3339

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: SET TRANSACTION*. https://www.postgresql.org/docs/18/sql-set-transaction.html

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: Row security policies*. https://www.postgresql.org/docs/18/ddl-rowsecurity.html

PostgreSQL Global Development Group. (2026). *PostgreSQL 18 documentation: Range types*. https://www.postgresql.org/docs/18/rangetypes.html

## Decision relevance

PostgreSQL's transaction access mode and isolation level support the adapter's explicit `READ COMMITTED, READ ONLY` boundary. Row security remains database defense in depth, while the application still binds tenant context and checks exact returned identity. PostgreSQL range/exclusion semantics remain the schema-level basis for bitemporal non-overlap; this read adapter does not replace those constraints.

RFC 3339 and explicit UTC projection support one interoperable representation for system-recorded evidence. NIST SP 800-53 Rev. 5 informs least privilege, access control, and information-integrity evidence readiness; no compliance or certification claim follows from this PR.

## Research classification

These references constrain the accepted adapter architecture for PR #153. They do not authorize scope expansion into worker data, Assignment joins, compensation, candidate, performance, or employment-decision automation.
54 changes: 54 additions & 0 deletions docs/traceability/postgres-position-history-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# PostgreSQL Position-history read traceability

**Lifecycle status:** Proposed on active stacked PR #153 only. This document does not claim protected-`develop` integration.

## Buyer problem

PR #152 defines an authorized Position-history read but leaves persistence injected. Without a canonical adapter, an Orgmetra deployment cannot obtain that bounded history from normalized `position_record` and `position_record_version` truth without bespoke host code.

## Requirement-to-evidence matrix

| Requirement | Production boundary | Regression |
| --- | --- | --- |
| No DB access on invalid input | exact tenant/Position UUID and built-in UTC `known_at` validation before `connection_factory()` | invalid UUID/time cases assert zero connection calls |
| Stable request identity | tenant/Position UUIDs are reduced to built-in integer scalar authority before connection acquisition and fresh UUID views are used for DB parameters | a connection factory that mutates retained caller UUID aliases cannot change tenant GUC or SELECT target |
| Stable DB capability | `connection_factory` is held in tuple payload, not a writable instance slot | `object.__setattr__` cannot replace the accepted capability |
| Real transaction context | acquired connection must expose exact `autocommit is False` before `cursor()` | `True`, missing/`None`, numeric `0`, and string `"false"` modes fail before cursor access |
| Database cannot mutate HR truth | only after non-autocommit proof, execute `SET TRANSACTION ISOLATION LEVEL READ COMMITTED, READ ONLY` | execution-order and autocommit regressions |
| Tenant defense in depth | transaction-local `pg_catalog.set_config('orgmetra.tenant_record_id', ..., true)` before SELECT in the same proven transaction context | exact SQL and detached tenant parameter assertion |
| Explicit Position scope | fully qualified join between `public.position_record_version` and `public.position_record` with tenant/Position predicates | SQL contract assertions |
| Preserve system knowledge | half-open parent/version `recorded_from`/`recorded_to` predicates at `known_at` | future and closed-at-cutoff rows fail closed |
| Preserve business history | no effective-date filter; deterministic effective start/version ordering | returned typed dates and SQL ordering assertion |
| Canonical UTC | `AT TIME ZONE 'UTC'` projection and exact naive DB timestamp validation | string/aware/non-datetime timestamp regressions |
| Untrusted DB-API boundary | exact list result, exact tuple row shape, parent record reconstruction | malformed collection/row/value regressions |
| Immutable typed result | tuple of scalar-backed `PositionHistoryRecord` values | empty/non-empty result and parent retained-authority regressions |
| Parent authority remains single owner | adapter accepts no purpose or authorization input | PR #152 performs authorization and service revalidation |
| Repository acceptance owner | consolidated Foundation CI after descendant reaches a protected-parent/`develop` integration lane | feature-local pre-consolidation workflow is absent from the current stack |

## Test-first and restack chain

1. **Original contract-only child head:** `bf93924e` added adapter regressions while `orgmetra_people_api.postgres_position_history` was absent.
2. **Original implementation lineage:** the pre-consolidation branch added the adapter, tests, ADR/doctoring/traceability, and a feature-local quality workflow. Historical local/full-suite and isolated PostgreSQL results remain development evidence only and are not transferred to the current exact head.
3. **Parent reconciliation:** after #152 advanced to `616ed8c8a410e4dcdf4c31d293dfb082f7ce8297`, ordinary two-parent commit `a8edc7d2fa69842d4515def5c8a3e710ed4b4e2b` restacked #153 on that owner head without force push. The stale `.github/workflows/position-history-postgres-read-quality.yml` was deliberately not overlaid because protected #161 consolidated repository acceptance under Foundation CI.
4. **Retained-authority/transaction test-only head:** `800b783a74594a59f4c0d5dd819a6fce4fced4b8` adds regressions proving immutable connection capability, exact non-autocommit mode, rejection of unproven transaction modes, and detached request UUID authority before connection acquisition.
5. **Production repair:** `00d8f58d0863ef0d3270c7197d41856de3131e34` changes the adapter to a tuple-backed capability holder, snapshots request UUIDs to integer scalars, requires `autocommit is False` before cursor creation, uses fresh detached UUID views for tenant/query parameters, and compares returned scalar identity to the request snapshot.
6. **Fixture contract:** `d0a8b9b9b5e43bbc522b97c921a1d67c51b5fb6c` makes the normal DB-API fixture explicitly non-autocommit rather than relying on an unspecified connection mode.
7. **ADR currentization:** `6d0e185fdbce6d3d5b6a49c70353bcb0809830c2` records transaction proof, immutable capability ownership, UUID detachment, and consolidated workflow ownership.

No hosted RED or GREEN is claimed for the new stacked heads merely because PR-triggered Foundation runs are absent. Once its parent lineage is protected and #153 can target `develop`, the exact final child head must reacquire Foundation/Security/SAST/CodeQL and qualifying independent review. Parent, predecessor, manual, or historical isolated-PostgreSQL evidence does not transfer.

## Security and data boundary

The adapter reads only Position anchor lineage and Position-version fields. It does not join Person, Employment, Assignment, compensation, candidate, performance, credential, prompt, or model-output data. Purpose-bound authorization-before-retrieval remains in the parent service; the adapter performs no mutation, audit/outbox write, or high-impact employment decision.

The database transaction is intentionally narrow: connection acquisition → explicit non-autocommit proof → read-only/tenant-context setup → bounded SELECT/fetch → context exit. Authorization and other potentially long-running work do not execute while this adapter transaction is open.

## Out of scope

- Position-history HTTP/presentation integration (#154).
- Position mutation or correction workflows.
- Assignment/Employment history joins.
- Database migrations; the protected schema already owns these relations and RLS policies.
- Release, tag, publication, or protected-default-branch authority.

Any later descendant must preserve these boundaries and consume this adapter through its owner stack rather than copying its source.
2 changes: 2 additions & 0 deletions services/people-api/src/orgmetra_people_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
from orgmetra_people_api.postgres import PostgresPeopleReadPort
from orgmetra_people_api.postgres_hire import PostgresHireAcceptancePort
from orgmetra_people_api.postgres_mutations import PostgresPeopleMutationPort
from orgmetra_people_api.postgres_position_history import PostgresPositionHistoryReadPort

__all__ = [
"AuthenticatedPrincipal",
Expand Down Expand Up @@ -80,6 +81,7 @@
"PostgresHireAcceptancePort",
"PostgresPeopleMutationPort",
"PostgresPeopleReadPort",
"PostgresPositionHistoryReadPort",
"AssignmentMutationCommand",
"AssignmentMutationResult",
"EmploymentMutationCommand",
Expand Down
Loading