Skip to content
Merged
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
6 changes: 4 additions & 2 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

- Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal.
- `orgmetra_hris_kernel` 0.4.0 with exclusive-versus-concurrent employment, staffable position coverage, exclusive-seat capacity, and `validate_assignment_write` at 100% statement and branch coverage.
- `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records` with the same Keyverse mutation context, confirmation, and versioned evidence composition as other high-impact commands.
- `employment_record_version.employment_concurrency_code` constrained to `exclusive` or `concurrent`.
Expand Down Expand Up @@ -43,6 +44,7 @@ All notable changes to Orgmetra will be documented in this file.
- Pinned the PostgreSQL 16.14 CI service image to the reviewed Docker Official Image index digest and added a regression that rejects a mutable `postgres:16` service tag.
- Split employment and position identity from versioned status so corrections no longer mint a new employment or position identifier.
- Made assignment coverage status-aware: `active` and `leave` remain staffable while `terminated` and other non-eligible employment statuses fail closed.
- Made organization hierarchy reconstruction fail closed on a cycle at the requested tenant, effective day, and knowledge cutoff while ignoring future-recorded and foreign-tenant facts.

### Security

Expand All @@ -52,11 +54,11 @@ All notable changes to Orgmetra will be documented in this file.
- Service-owned database schemas and roles inside the initially shared physical PostgreSQL cluster.
- Database guards for reversed or zero-length temporal intervals and append-only candidate-worker, selection-decision, decision-evidence, and validation-study linkage records.
- Database-level rejection of cross-tenant references, post-decision evidence insertion, caller-supplied open-set evidence digests, empty decision evidence, sealed evidence-set reuse, and RFC 9562 Nil/Max UUID sentinels across foundation identity columns.
- Bitemporal reconstruction plus assignment, position-seat, and employment-exclusivity kernel decisions are tenant-scoped so foreign-tenant identifiers cannot leak historical facts, provide coverage, consume capacity, or create false conflicts.
- Bitemporal reconstruction plus assignment, position-seat, employment-exclusivity, and organization-hierarchy kernel decisions are tenant-scoped so foreign-tenant identifiers cannot leak historical facts, provide coverage, consume capacity, or create false structural conflicts.
- Keyverse outage policy that blocks PII and high-risk actions when current authorization cannot be verified.
- Cross-tenant threat, denial evidence, and negative authorization test contracts.
- Replaced client-visible internal trace identifiers with random support references and actionable next-step error guidance.

### Notes

- The protected default branch contains only the minimal bootstrap commit. This baseline is proposed through `feat/foundation-product-baseline` and becomes shipped truth only after review and merge.
- Protected `bootstrap` contains the integrated foundation and hire-to-assignment kernel through merge commit `99bffec05d0732e332cfbcb64f34838395bbd078`; entries above that introduced by active PRs become shipped truth only after their own protected merge.
5 changes: 3 additions & 2 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
|---|---|---|---|---|---|
| Separate person/employment/organization/job/position/assignment | Core bounded contexts | `person_record`, `employment_record`, `employment_record_version`, `organization_unit`, `job_profile`, `position_record`, `position_record_version`, `assignment_record` | schema/domain and `orgmetra_hris_kernel` tests | ADR-0001, ADR-0004, ADR-0005 | implemented_on_active_pr |
| Exclusive employment and staffable seats | Core bounded contexts | `employment_concurrency_code`, staffable `position_status_code`, assignment allocation totals | Memorial Hospital exclusivity, freeze, and seat-capacity kernel tests plus OpenAPI employment/position/assignment commands | ADR-0005 | implemented_on_active_pr |
| Tenant-qualified HRIS integrity and fail-closed isolation | Core bounded contexts / Security architecture | `tenant_record`, tenant-qualified foreign keys, forced row-level security policies, tenant-scoped kernel query parameters | PostgreSQL cross-tenant FK/application-role RLS contracts plus kernel cross-tenant reconstruction, employment coverage, position coverage, seat-capacity, portfolio, and exclusivity regressions | ADR-0001, ADR-0003 | implemented_on_active_pr |
| Tenant-qualified HRIS integrity and fail-closed isolation | Core bounded contexts / Security architecture | `tenant_record`, tenant-qualified foreign keys, forced row-level security policies, tenant-scoped kernel query parameters | PostgreSQL cross-tenant FK/application-role RLS contracts plus kernel cross-tenant reconstruction, employment coverage, position coverage, seat-capacity, portfolio, exclusivity, and organization-hierarchy regressions | ADR-0001, ADR-0003 | implemented_on_active_pr |
| Reserved UUID sentinel exclusion | Persistence integrity boundary | every foundation UUID `*_id` column | PostgreSQL inventory proof plus Nil/Max tenant and person persistence regressions | ADR-0001, RFC 9562 | implemented_on_active_pr |
| Normalized bitemporal organization/job/employment/position history | Core bounded contexts | `organization_unit_version`, `job_profile_version`, `employment_record_version`, `position_record_version` | PostgreSQL non-overlap, concurrent conflict, correction, rewrite-rejection, and assignment-employment binding | ADR-0001, ADR-0003, ADR-0004 | implemented_on_active_pr |
| Normalized bitemporal organization/job/employment/position history | Core bounded contexts | `organization_unit_version`, `job_profile_version`, `employment_record_version`, `position_record_version` | PostgreSQL non-overlap, concurrent conflict, correction, rewrite-rejection, assignment-employment binding, and single-valued historical reconstruction | ADR-0001, ADR-0003, ADR-0004 | implemented_on_active_pr |
| Acyclic organization hierarchy at historical coordinates | Organization core | `organization_unit_version.parent_organization_unit_id` | indirect A→B→C→A rejection plus future-recorded and foreign-tenant isolation in `orgmetra_hris_kernel` | ADR-0001, ADR-0003 | implemented_on_active_pr |
| Effective/system time | Bitemporal HRIS | `effective_from`, `recorded_from` | strict half-open interval and historical-coordinate tests | ADR-0003 | implemented_on_active_pr |
| Evidence-backed human selection decisions | Talent Acquisition | `decision_evidence_set`, `selection_decision_evidence`, `selection_decision` | database-owned SHA-256 sealing, non-empty evidence, drift/reuse rejection, OpenAPI human-confirmation tests | ADR-0001 | implemented_on_active_pr |
| Predictive-validity evidence lineage | Workforce Validation | `validity_study`, `validity_study_decision_link`, `validity_study_evidence_set_link`, `validity_study_outcome_link`, `criterion_observation` | normalized linkage and append-only database contract; statistical validation remains subsequent work | ADR-0001 | implemented_on_active_pr |
Expand Down
12 changes: 6 additions & 6 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
},
{
"path": "CHANGELOG.md",
"sha256": "f0a31c33803a9ca730d04a0215ac491f8580874bd2b32b519fe3cfdf2ec0a84d",
"bytes": 6285,
"lines": 62
"sha256": "7cb98d19ad5c487d34f91d69f1509ee2967d8f07bcbeda4604469692da2bdebf",
"bytes": 6747,
"lines": 64
},
{
"path": "CLAUDE.md",
Expand Down Expand Up @@ -131,9 +131,9 @@
},
{
"path": "docs/TRACEABILITY.md",
"sha256": "e1e4e0a3d432a8354783aa79caf05f06cb732c62a0cdf1b38c654fba8242b306",
"bytes": 5417,
"lines": 31
"sha256": "26554f60babaf77f01a81f19d795bff9600d5f6f95d681b213e31ea105ed7033",
"bytes": 5780,
"lines": 32
},
{
"path": "docs/TRD.md",
Expand Down
12 changes: 11 additions & 1 deletion packages/hris-kernel/src/orgmetra_hris_kernel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@
IdentityScopeError,
IntervalError,
KernelError,
OrganizationHierarchyError,
PositionCoverageError,
PositionSeatError,
SingleValuedFactError,
)
from orgmetra_hris_kernel.facts import AssignmentFact, EmploymentVersion, PositionVersion
from orgmetra_hris_kernel.facts import (
AssignmentFact,
EmploymentVersion,
OrganizationUnitVersion,
PositionVersion,
)
from orgmetra_hris_kernel.intervals import DateInterval, RecordedInterval
from orgmetra_hris_kernel.organization import validate_organization_hierarchy
from orgmetra_hris_kernel.resolution import resolve_bitemporal_facts, resolve_single_valued_fact

__all__ = [
Expand All @@ -41,6 +48,8 @@
"IdentityScopeError",
"IntervalError",
"KernelError",
"OrganizationHierarchyError",
"OrganizationUnitVersion",
"PositionCoverageError",
"PositionSeatError",
"PositionVersion",
Expand All @@ -53,6 +62,7 @@
"validate_assignment_portfolio",
"validate_assignment_position_coverage",
"validate_assignment_write",
"validate_organization_hierarchy",
"validate_person_employment_exclusivity",
"validate_position_seat_capacity",
]
4 changes: 4 additions & 0 deletions packages/hris-kernel/src/orgmetra_hris_kernel/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class EmploymentExclusivityError(KernelError):
"""Two exclusive employments overlap for one person, or concurrency is unknown."""


class OrganizationHierarchyError(KernelError):
"""Visible parent links form a cycle inside one tenant's organization hierarchy."""


class PositionCoverageError(KernelError):
"""An assignment is not covered by a staffable position version."""

Expand Down
16 changes: 15 additions & 1 deletion packages/hris-kernel/src/orgmetra_hris_kernel/facts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Immutable employment, position, and assignment facts."""
"""Immutable employment, organization, position, and assignment facts."""

from __future__ import annotations

Expand All @@ -13,9 +13,11 @@
"tenant_record_id",
"employment_record_id",
"person_record_id",
"organization_unit_id",
"position_record_id",
"assignment_record_id",
"employment_record_version_id",
"organization_unit_version_id",
"position_record_version_id",
}
)
Expand All @@ -35,6 +37,18 @@ class EmploymentVersion:
employment_concurrency_code: str = "exclusive"


@dataclass(frozen=True, slots=True)
class OrganizationUnitVersion:
"""One recorded parent-link version of a durable organization unit."""

tenant_record_id: UUID
organization_unit_id: UUID
organization_unit_version_id: UUID
parent_organization_unit_id: UUID | None
effective: DateInterval
recorded: RecordedInterval


@dataclass(frozen=True, slots=True)
class PositionVersion:
"""One recorded version of a durable position seat."""
Expand Down
69 changes: 69 additions & 0 deletions packages/hris-kernel/src/orgmetra_hris_kernel/organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Tenant-scoped bitemporal organization hierarchy integrity rules."""

from __future__ import annotations

from datetime import date, datetime
from uuid import UUID

from orgmetra_hris_kernel.errors import OrganizationHierarchyError
from orgmetra_hris_kernel.facts import OrganizationUnitVersion
from orgmetra_hris_kernel.resolution import resolve_single_valued_fact


def validate_organization_hierarchy(
organization_versions: list[OrganizationUnitVersion],
*,
tenant_record_id: UUID,
effective_on: date,
known_at: datetime,
) -> None:
"""Reject a visible parent cycle inside one tenant at one bitemporal coordinate.

Each durable unit is first resolved through the normal single-valued
bitemporal rule, so two simultaneously visible versions fail closed before
graph traversal. Parent anchors without a visible version terminate the
currently known chain rather than importing facts from another tenant or a
future knowledge state.

Args:
organization_versions: Candidate parent-link versions, including other tenants.
tenant_record_id: Tenant namespace whose organization chart is reviewed.
effective_on: Business day represented by the chart.
known_at: System knowledge cutoff used to reconstruct it.

Raises:
SingleValuedFactError: One unit has two visible versions at the coordinate.
OrganizationHierarchyError: Visible parent links contain a cycle.
"""
scoped = [
version
for version in organization_versions
if version.tenant_record_id == tenant_record_id
]
unit_ids = {version.organization_unit_id for version in scoped}
parents: dict[UUID, UUID | None] = {}
for unit_id in unit_ids:
visible = resolve_single_valued_fact(
scoped,
tenant_record_id=tenant_record_id,
identity_of="organization_unit_id",
identity_value=unit_id,
effective_on=effective_on,
known_at=known_at,
)
if visible is not None:
parents[unit_id] = visible.parent_organization_unit_id

for start in parents:
seen: set[UUID] = set()
current: UUID | None = start
while current is not None:
if current in seen:
raise OrganizationHierarchyError(
"Visible organization parent links form a cycle in this tenant.",
next_action=(
"Close or correct the superseded parent link, then validate the organization chart again."
),
)
seen.add(current)
current = parents.get(current)
4 changes: 3 additions & 1 deletion packages/hris-kernel/src/orgmetra_hris_kernel/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def resolve_bitemporal_facts(
if identity_of not in IDENTITY_FIELDS:
raise IdentityScopeError(
f"Unsupported identity field: {identity_of}",
next_action="Query by employment, person, position, assignment, or tenant identity.",
next_action=(
"Query by employment, person, organization, position, assignment, or tenant identity."
),
)
visible: list[FactT] = []
for fact in facts:
Expand Down
98 changes: 98 additions & 0 deletions packages/hris-kernel/tests/test_organization_hierarchy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Organization hierarchy validation must reject visible bitemporal cycles."""

from datetime import date
from uuid import UUID

import pytest

from orgmetra_hris_kernel import (
OrganizationHierarchyError,
OrganizationUnitVersion,
validate_organization_hierarchy,
)

from .conftest import effective, recorded, utc

TENANT_ALPHA = UUID("00000000-0000-7000-8000-000000000001")
TENANT_BETA = UUID("00000000-0000-7000-8000-000000000002")
UNIT_A = UUID("50000000-0000-7000-8000-000000000001")
UNIT_B = UUID("50000000-0000-7000-8000-000000000002")
UNIT_C = UUID("50000000-0000-7000-8000-000000000003")
UNIT_D = UUID("50000000-0000-7000-8000-000000000004")
UNIT_E = UUID("50000000-0000-7000-8000-000000000005")
UNIT_F = UUID("50000000-0000-7000-8000-000000000006")


def _unit(
*,
version_id: str,
unit_id: UUID,
parent_id: UUID | None,
tenant_id: UUID = TENANT_ALPHA,
recorded_from_year: int = 2024,
) -> OrganizationUnitVersion:
"""Build one organization parent-link fact for hierarchy examples."""
return OrganizationUnitVersion(
tenant_record_id=tenant_id,
organization_unit_id=unit_id,
organization_unit_version_id=UUID(version_id),
parent_organization_unit_id=parent_id,
effective=effective(date(2024, 1, 1)),
recorded=recorded(utc(recorded_from_year, 1, 1)),
)


def test_rejects_indirect_cycle_visible_in_one_tenant() -> None:
"""A→B→C→A cannot become the authoritative organization hierarchy."""
versions = [
_unit(version_id="51000000-0000-7000-8000-000000000001", unit_id=UNIT_A, parent_id=UNIT_B),
_unit(version_id="51000000-0000-7000-8000-000000000002", unit_id=UNIT_B, parent_id=UNIT_C),
_unit(version_id="51000000-0000-7000-8000-000000000003", unit_id=UNIT_C, parent_id=UNIT_A),
]

with pytest.raises(OrganizationHierarchyError, match="cycle"):
validate_organization_hierarchy(
versions,
tenant_record_id=TENANT_ALPHA,
effective_on=date(2024, 6, 1),
known_at=utc(2024, 6, 1),
)


def test_future_and_foreign_tenant_facts_do_not_poison_current_hierarchy() -> None:
"""Only facts visible at the requested tenant/effective/knowledge coordinate count."""
versions = [
_unit(version_id="52000000-0000-7000-8000-000000000001", unit_id=UNIT_A, parent_id=None),
_unit(version_id="52000000-0000-7000-8000-000000000002", unit_id=UNIT_B, parent_id=UNIT_A),
_unit(
version_id="52000000-0000-7000-8000-000000000003",
unit_id=UNIT_C,
parent_id=UNIT_B,
recorded_from_year=2026,
),
_unit(
version_id="52000000-0000-7000-8000-000000000004",
unit_id=UNIT_D,
parent_id=UNIT_E,
tenant_id=TENANT_BETA,
),
_unit(
version_id="52000000-0000-7000-8000-000000000005",
unit_id=UNIT_E,
parent_id=UNIT_D,
tenant_id=TENANT_BETA,
),
_unit(
version_id="52000000-0000-7000-8000-000000000006",
unit_id=UNIT_F,
parent_id=UNIT_B,
tenant_id=TENANT_ALPHA,
),
]

validate_organization_hierarchy(
versions,
tenant_record_id=TENANT_ALPHA,
effective_on=date(2024, 6, 1),
known_at=utc(2024, 6, 1),
)
Loading