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
66 changes: 66 additions & 0 deletions .github/workflows/position-history-http-quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Position History HTTP Quality

on:
pull_request:
branches:
- develop
- feat/people-position-history-postgres-adapter
paths:
- "services/people-api/**"
- "packages/hris-kernel/**"
- "packages/keyverse-adapter/**"
- "schemas/openapi.yaml"
- ".github/requirements/foundation-test.txt"
- ".github/workflows/position-history-http-quality.yml"
- "docs/API_CONTRACT.md"
- "docs/SECURITY.md"
- "docs/TEST_STRATEGY.md"
- "docs/TRACEABILITY.md"
- "docs/adr/0154-position-history-http-read.md"
- "docs/doctoring/position-history-http-read-references.md"
- "docs/traceability/position-history-http-read.md"
- "services/people-api/README.md"
workflow_dispatch:

permissions:
contents: read

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

jobs:
unit:
name: Position-history HTTP 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-position-history-http.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 @@ -6,6 +6,7 @@ All notable changes to Orgmetra will be documented in this file.

### Added

- Active stacked Position-history HTTP read boundary: `GET /v1/tenants/{tenant_record_id}/positions/{position_record_id}/history` validates an exact UTC `known_at` cutoff and requested fields before bearer authentication, delegates purpose-bound authorization and canonical PostgreSQL history retrieval to the existing People contracts, returns only authorized Position fields, and fails closed with the published client-safe error envelope. The dedicated quality workflow enforces exact current-head checkout and 100% People API statement/branch coverage.
- 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-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.
Expand Down
6 changes: 5 additions & 1 deletion docs/API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The baseline scope contract is:
|---|---|
| People mutations | `orgmetra.people.write` |
| Confirmed-hire materialization | `orgmetra.people.materialize_worker` |
| Position-history reads | `orgmetra.people.position_history.read` |
| Job-architecture mutations | `orgmetra.job_architecture.write` |
| Talent-acquisition mutations | `orgmetra.talent_acquisition.write` |

Expand Down Expand Up @@ -49,6 +50,7 @@ The server rejects a reused idempotency key when its method, resource, tenant, a
POST /v1/person-records
GET /v1/person-records/{person_record_id}
POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions?purpose=candidate_hire
GET /v1/tenants/{tenant_record_id}/positions/{position_record_id}/history?known_at=2026-08-30T00:00:00Z&purpose=workforce_position_review&fields=effective_from,position_status_code
POST /v1/employment-records
POST /v1/position-records
POST /v1/assignment-records
Expand All @@ -62,6 +64,8 @@ POST /v1/validity-studies

The foundation OpenAPI contract covers the shared command vocabulary and baseline person, employment, position, assignment, job-profile, and selection-decision operations. Runtime services must publish any additional path-specific contract before release and may not weaken the shared `Idempotency-Key`, least-privilege scope, authorization, evidence, or error semantics. Employment and assignment writes fail closed when exclusive jobs overlap, a seat is not staffable, or visible seat allocations exceed 1.0000.

Position-history reads are read-only and bitemporal. The route requires an RFC 3339 UTC `known_at` system-recorded cutoff with a trailing `Z`, an explicit business `purpose`, and a comma-separated `fields` set. The service authorizes the exact `position_history:{position_record_id}` target and returns only the authorized fields for Position versions visible at that cutoff; an empty visible history is a successful empty `entries` collection. The route performs no Person, Employment, Assignment, compensation, candidate, performance, credential, or employment-decision expansion.

## Error shape

```json
Expand All @@ -73,4 +77,4 @@ The foundation OpenAPI contract covers the shared command vocabulary and baselin
}
```

`support_reference` is a randomly generated client-safe lookup key. It maps to restricted internal telemetry but never encodes or exposes an internal trace/span identifier, topology, timestamp, tenant identifier, credential, or PII.
`support_reference` is a randomly generated client-safe lookup key. It maps to restricted internal telemetry but never encodes or exposes an internal trace/span identifier, topology, timestamp, tenant identifier, credential, or PII.
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 @@ All mutation families additionally require resource-scoped authorization and a v

A caller-controlled purpose value cannot substitute for a missing token scope. The OpenAPI contract is executable input to generated gateway and server validation; an implementation that accepts a request outside its published contract fails CI.

The Position-history HTTP boundary binds `known_at` to one explicit UTC system-time cutoff and authorizes the exact opaque Position-history target before invoking persistence. It returns only the requested authorized fields and fails closed on malformed identifiers, duplicate query fields, cross-tenant policy context, contradictory bitemporal rows, and unexpected backend failures; it does not expand the read into Person, Employment, Assignment, or high-impact decision data.

Internal traces remain in restricted telemetry. Customer-facing failures return a bounded `error_code`, actionable `message`, `next_action`, and random `support_reference`; the support lookup is access-controlled and retention-bound.

The same governance contract applies to selection decisions, compensation changes, terminations, promotions, job-profile publication, validation-study policy changes, data exports, and identity deprovisioning. Draft creation may use a narrower permission, but publication or authoritative state transition may not reuse draft-only authorization.
Expand Down
1 change: 1 addition & 0 deletions docs/TEST_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The command runs Python repository-integrity validation, the dependency-free Nod
| Performance criterion observation Job, cycle, staffing, current-recorded-time, and UTC date-boundary integrity | `bash tests/test_criterion_observation_scope_postgres.sh` against PostgreSQL 16 in Foundation CI |
| 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 |
| Position-history HTTP parsing, authentication order, purpose/field minimization, bitemporal cutoff binding, error privacy, and PostgreSQL-backed read integration | `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` with exact 100% statement and branch coverage |
| 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 |
| 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 |
| Purpose-bound bitemporal Position-history reads | People API / read-only history boundary | `GET /v1/tenants/{tenant_record_id}/positions/{position_record_id}/history`, `PositionHistoryAsgiApp`, `read_position_history()`, `PostgresPositionHistoryReadPort` | HTTP parser/authentication-order/error-envelope contract plus typed service and real PostgreSQL tenant/RLS, UTC, knowledge-cutoff, Position/Job/organization-lineage evidence; exact 100% People API statement/branch coverage | ADR-0008, ADR-0152, ADR-0153, ADR-0154 | implemented_on_active_pr |
| 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 |
| Governed immutable audit and transactional outbox persistence | Audit Provenance / Integration Hub | `AuditOutboxEvent.canonical_json()`, `audit_event_record`, `outbox_delivery_record`, SHA-256 envelope digest | canonical-byte/digest regression plus PostgreSQL digest, allowlist/PII, high-impact confirmation, append-only, atomicity, lease-transition, terminal-state, and reserved-UUID tests | ADR-0006 | implemented_on_active_pr |
Expand Down
27 changes: 26 additions & 1 deletion docs/UML.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ flowchart LR

The cluster is physically shared in the initial modular deployment. Each bounded context has a separate schema and role; direct reads of another context's application tables are prohibited.

## Position-history read sequence

```mermaid
sequenceDiagram
actor Customer
participant Gateway
participant PositionHistory
participant Policy
participant PostgreSQL

Customer->>Gateway: GET Position history(tenant, Position, known_at, purpose, fields)
Gateway->>Gateway: Validate route/query and authenticate Bearer token
Gateway->>Policy: Authorize exact target, purpose, scope, and fields
Policy-->>Gateway: Authorized field decision
Gateway->>PositionHistory: Read authorized bitemporal history
PositionHistory->>PostgreSQL: Tenant-scoped read-only query at known_at
PostgreSQL-->>PositionHistory: Typed Position/Job/organization lineage
PositionHistory-->>Gateway: Minimized authorized entries
Gateway-->>Customer: no-store response with opaque resource reference
```

Authorization precedes protected persistence access. The route does not join
Person, Employment, Assignment, compensation, candidate, performance,
credential, or employment-decision data.

## Selection decision sequence

```mermaid
Expand Down Expand Up @@ -119,4 +144,4 @@ sequenceDiagram
PeopleCore->>Audit: Persist assignment, audit/outbox, and idempotency binding
PeopleCore-->>Gateway: assignment_record Location
Gateway-->>HROps: Review the roster, then approve or correct
```
```
62 changes: 62 additions & 0 deletions docs/adr/0154-position-history-http-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# ADR 0154: Expose governed Position history through a read-only HTTP boundary

- **Status:** Proposed on active stacked PR #154; not protected-main truth until integrated
- **Date:** 2026-08-30
- **Owners:** Orgmetra People API / customer read boundary
- **Extends:** ADR 0008 (purpose-bound PII authorization), ADR 0152 (Position-history read), ADR 0153 (PostgreSQL Position-history read)

## Context

PR #152 defines the purpose-bound Position-history use case and PR #153 supplies
the canonical PostgreSQL read adapter, but neither exposes a customer-callable
transport route. Deployments need one stable boundary that preserves the same
tenant, purpose, field, bitemporal, and no-disclosure controls without adding
Person, Employment, Assignment, or employment-decision authority.

## Decision

Add `PositionHistoryAsgiApp` with this route:

```text
GET /v1/tenants/{tenant_record_id}/positions/{position_record_id}/history
?known_at=YYYY-MM-DDTHH:MM:SSZ
&purpose=workforce_position_review
&fields=effective_from,position_status_code
```

The boundary validates operational UUIDs, exact required query keys, ASCII
query syntax, a UTC RFC 3339 `known_at` ending in `Z`, lower snake-case purpose
and fields, and duplicate-field/parameter rejection before authentication. It
reuses the existing People ASGI JSON transport and authorization-header parser,
authenticates exactly one Bearer credential, then delegates to
`read_position_history()`. The operation declares
`orgmetra.people.position_history.read`, returns only authorized fields, uses
`Cache-Control: no-store` and `Vary: Authorization`, and maps malformed input,
authentication, authorization, integrity, and unexpected failures to the
published client-safe error envelope.

OpenAPI publishes the route, query/path parameters, `PositionHistoryView`, and
400/401/403/409/500 responses. The dedicated workflow checks the exact PR head,
compiles the service, and runs the complete People suite at 100% statement and
branch coverage.

## Consequences

- Customers receive one stable, read-only Position-history boundary.
- Existing Position-history service and PostgreSQL ownership boundaries remain
the only owners of authorization, bitemporal validation, and persistence.
- Error support references are opaque and safe for customer correlation; the
route does not expose backend exception details.
- The route intentionally does not add pagination, writes, cross-service joins,
or high-impact employment decisions; those require separate contracts.

## Verification

The test-only child head `86cc40b1` fails during collection while the HTTP
adapter module is absent. The implementation must retain that test-first chain,
show exact-current-head hosted evidence, and remain a Draft stacked PR until
independent review and all protected central gates are authoritative.

RFC 3339, OpenAPI 3.2.0, NIST SP 800-53 Rev. 5, and PostgreSQL RLS/read-only
transaction guidance inform the boundary. They are defense-in-depth references,
not certification or merge evidence.
3 changes: 3 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@
| [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on active implementation branch |
| [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 |
| [0152](0152-purpose-bound-position-history-read.md) | Purpose-bound Position-history read contract | Accepted on active implementation branch |
| [0153](0153-postgres-position-history-read.md) | Read Position history from canonical PostgreSQL truth | Proposed on active stacked implementation branch |
| [0154](0154-position-history-http-read.md) | Expose governed Position history through a read-only HTTP boundary | Proposed on active stacked implementation branch |
Loading