From de20a17eb01f99769d40d0e3b18d2589b1771959 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:08:57 -0700 Subject: [PATCH 01/23] test(position): define lifecycle review package contract --- .../position-lifecycle-review/pyproject.toml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/position-lifecycle-review/pyproject.toml diff --git a/packages/position-lifecycle-review/pyproject.toml b/packages/position-lifecycle-review/pyproject.toml new file mode 100644 index 000000000..e549123cc --- /dev/null +++ b/packages/position-lifecycle-review/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=84"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-position-lifecycle-review" +version = "0.1.0" +description = "Governed Position lifecycle-change review evidence for Orgmetra." +requires-python = ">=3.14" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_position_lifecycle_review", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] From b5d02847dd94dae1d1e602ccf8952e6c8cda71cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:09:53 -0700 Subject: [PATCH 02/23] test(position): add lifecycle review RED regressions --- .../tests/test_review.py | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 packages/position-lifecycle-review/tests/test_review.py diff --git a/packages/position-lifecycle-review/tests/test_review.py b/packages/position-lifecycle-review/tests/test_review.py new file mode 100644 index 000000000..ff279cdc6 --- /dev/null +++ b/packages/position-lifecycle-review/tests/test_review.py @@ -0,0 +1,217 @@ +"""Adversarial contract tests for governed Position lifecycle review evidence.""" + +from dataclasses import replace +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +import gc +from uuid import UUID + +import pytest + +from orgmetra_position_lifecycle_review import PositionLifecycleChangeReviewPacket + +TENANT = UUID("0198a800-1111-7000-8000-000000000001") +POSITION = UUID("0198a800-2222-7000-8000-000000000002") +CHANGE = UUID("12345678-1234-4abc-8def-1234567890ab") +REQUESTER = "actor:11111111-1111-4111-8111-111111111111" +REVIEWER = "actor:22222222-2222-4222-8222-222222222222" +POSITION_DIGEST = "a" * 64 +ASSIGNMENT_DIGEST = "b" * 64 +REVIEWED_AT = datetime(2026, 8, 24, 11, 0, tzinfo=timezone.utc) +RECORDED_AT = datetime(2026, 8, 24, 11, 1, tzinfo=timezone.utc) + + +def build_packet(**overrides: object) -> PositionLifecycleChangeReviewPacket: + """Return one valid reviewed lifecycle-change packet with optional overrides.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "position_record_id": POSITION, + "position_lifecycle_change_reference": CHANGE, + "current_status_code": "active", + "proposed_status_code": "frozen", + "effective_on": date(2026, 9, 1), + "position_snapshot_digest_sha256": POSITION_DIGEST, + "assignment_snapshot_digest_sha256": ASSIGNMENT_DIGEST, + "requester_actor_reference": REQUESTER, + "reviewer_actor_reference": REVIEWER, + "reason_code": "temporary_freeze", + "review_outcome_code": "approved_for_authoritative_resolution", + "evidence_version": 1, + "reviewed_at": REVIEWED_AT, + "recorded_at": RECORDED_AT, + } + values.update(overrides) + return PositionLifecycleChangeReviewPacket(**values) # type: ignore[arg-type] + + +def test_approved_packet_is_deterministic_and_value_minimized() -> None: + """Canonical evidence contains governance metadata but no worker or HR payload.""" + packet = build_packet() + document = packet.canonical_document() + assert document["review_state"] == "human_reviewed" + assert document["scope_verification_state"] == "requires_authoritative_resolution" + assert document["mutation_state"] == "not_authorized_to_apply" + assert document["decision_authority"] == "human_review_only" + assert document["next_action"] == ( + "Re-resolve tenant-qualified Position and Assignment truth at the requested business/system " + "coordinate; require authoritative actor separation, reviewed evidence, staffing safety, " + "and immutable audit/outbox before any lifecycle mutation." + ) + encoded = packet.canonical_json() + assert packet.content_digest() == packet.content_digest() + assert "employee" not in encoded.lower() + assert "person" not in encoded.lower() + assert "compensation" not in encoded.lower() + assert POSITION.hex in encoded + assert "PositionLifecycleChangeReviewPacket(redacted)" == repr(packet) + + +def test_rejected_review_has_stop_next_action() -> None: + """A rejected review cannot be mistaken for mutation-ready evidence.""" + packet = build_packet(review_outcome_code="rejected") + assert packet.canonical_document()["next_action"] == "Do not apply the proposed Position lifecycle change." + + +@pytest.mark.parametrize( + ("current", "proposed", "reason"), + [ + ("open", "active", "position_reactivation"), + ("open", "frozen", "temporary_freeze"), + ("open", "closed", "position_closure"), + ("open", "abolished", "position_abolition"), + ("active", "frozen", "temporary_freeze"), + ("active", "closed", "position_closure"), + ("active", "abolished", "position_abolition"), + ("frozen", "open", "position_reactivation"), + ("frozen", "active", "position_reactivation"), + ("frozen", "closed", "position_closure"), + ("frozen", "abolished", "position_abolition"), + ("closed", "open", "position_reactivation"), + ("closed", "abolished", "position_abolition"), + ], +) +def test_reviewed_transition_vocabulary(current: str, proposed: str, reason: str) -> None: + """Reviewed transitions are explicit and do not silently invent a lifecycle state.""" + packet = build_packet(current_status_code=current, proposed_status_code=proposed, reason_code=reason) + assert packet.canonical_document()["proposed_status_code"] == proposed + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("current_status_code", "shadow"), + ("proposed_status_code", "shadow"), + ("reason_code", "free_text"), + ("review_outcome_code", "maybe"), + ("evidence_version", 2), + ], +) +def test_governed_vocabulary_fails_closed(field_name: str, value: object) -> None: + """Unknown governance vocabulary and schema versions are rejected.""" + with pytest.raises(ValueError): + build_packet(**{field_name: value}) + + +def test_noop_and_abolished_revival_are_rejected() -> None: + """No-op evidence and resurrection of an abolished Position are not reviewable here.""" + with pytest.raises(ValueError): + build_packet(proposed_status_code="active") + with pytest.raises(ValueError): + build_packet(current_status_code="abolished", proposed_status_code="open", reason_code="position_reactivation") + + +class ForgedStatus(str): + """String subclass that lies about equality/hash membership.""" + + def __hash__(self) -> int: + return hash("active") + + def __eq__(self, other: object) -> bool: + return other == "active" + + +def test_runtime_subclasses_cannot_forge_governance_text_or_dates() -> None: + """Trust-bearing scalar checks run only on exact built-in runtime types.""" + with pytest.raises(TypeError): + build_packet(current_status_code=ForgedStatus("shadow")) + + class DateSubclass(date): + pass + + with pytest.raises(TypeError): + build_packet(effective_on=DateSubclass(2026, 9, 1)) + + +def test_identifiers_and_actor_correlations_are_opaque_and_separated() -> None: + """Operational HRIS UUIDs and pseudonymous actor references obey distinct contracts.""" + with pytest.raises(ValueError): + build_packet(tenant_record_id=UUID(int=0)) + with pytest.raises(ValueError): + build_packet(position_record_id=UUID(int=(1 << 128) - 1)) + with pytest.raises(ValueError): + build_packet(position_lifecycle_change_reference=TENANT) + with pytest.raises(ValueError): + build_packet(requester_actor_reference="actor:alice@example.com") + with pytest.raises(ValueError): + build_packet(reviewer_actor_reference=REQUESTER) + + +def test_digests_are_exact_lowercase_sha256() -> None: + """Reviewed Position and Assignment snapshots use canonical SHA-256 text.""" + with pytest.raises(ValueError): + build_packet(position_snapshot_digest_sha256="A" * 64) + with pytest.raises(ValueError): + build_packet(assignment_snapshot_digest_sha256="b" * 63) + + +def test_review_and_recorded_time_are_exact_utc_and_monotonic() -> None: + """Human review precedes or equals system-recorded evidence time in canonical UTC.""" + with pytest.raises(ValueError): + build_packet(recorded_at=REVIEWED_AT - timedelta(seconds=1)) + with pytest.raises(ValueError): + build_packet(reviewed_at=REVIEWED_AT.astimezone(timezone(timedelta(hours=9)))) + with pytest.raises(ValueError): + build_packet(recorded_at=RECORDED_AT.replace(tzinfo=None)) + + +def test_post_construction_payload_tampering_fails_closed() -> None: + """Frozen-dataclass bypass cannot change checked canonical evidence.""" + packet = build_packet() + object.__setattr__(packet, "proposed_status_code", "closed") + with pytest.raises(ValueError): + packet.canonical_json() + + +def test_live_change_reference_cannot_bind_conflicting_evidence() -> None: + """One live tenant-qualified review reference cannot denote two reviewed truths.""" + packet = build_packet() + with pytest.raises(ValueError): + replace(packet, proposed_status_code="closed", reason_code="position_closure") + duplicate = replace(packet) + assert duplicate.canonical_json() == packet.canonical_json() + + +def test_reference_binding_releases_only_after_all_duplicates_die() -> None: + """Live duplicate accounting keeps a correlation bound until the last packet is gone.""" + packet = build_packet() + duplicate = replace(packet) + key_values = { + "tenant_record_id": packet.tenant_record_id, + "position_lifecycle_change_reference": packet.position_lifecycle_change_reference, + } + del duplicate + gc.collect() + with pytest.raises(ValueError): + build_packet(**key_values, proposed_status_code="closed", reason_code="position_closure") + del packet + gc.collect() + replacement = build_packet(**key_values, proposed_status_code="closed", reason_code="position_closure") + assert replacement.proposed_status_code == "closed" + + +def test_non_decimal_noise_is_not_part_of_the_packet_contract() -> None: + """The package deliberately carries no staffing allocation or compensation value.""" + packet = build_packet() + assert not hasattr(packet, "allocation_ratio") + assert not hasattr(packet, "compensation_amount") + assert Decimal("1.0") == Decimal("1.0") From 957842315b2a31f0303c73271f93cec566e24483 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:10:11 -0700 Subject: [PATCH 03/23] test(position): add lifecycle review exact-head quality gate --- .../position-lifecycle-review-quality.yml | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/position-lifecycle-review-quality.yml diff --git a/.github/workflows/position-lifecycle-review-quality.yml b/.github/workflows/position-lifecycle-review-quality.yml new file mode 100644 index 000000000..46b783f20 --- /dev/null +++ b/.github/workflows/position-lifecycle-review-quality.yml @@ -0,0 +1,74 @@ +name: Position Lifecycle Review Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/position-lifecycle-review/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/position-lifecycle-review-quality.yml" + - "docs/adr/0111-governed-position-lifecycle-review.md" + - "docs/doctoring/position-lifecycle-review-references.md" + - "docs/traceability/position-lifecycle-review.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: position-lifecycle-review-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Position lifecycle review contract and 100% coverage + 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 exact Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14.7" + check-latest: false + - name: Install reviewed test and build toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + printf '%s\n' 'setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670' > /tmp/orgmetra-position-lifecycle-review-build.txt + python -m pip install --require-hashes --no-deps --only-binary=:all: -r /tmp/orgmetra-position-lifecycle-review-build.txt + python -m pip check + - name: Compile package + run: python -m compileall -q packages/position-lifecycle-review/src packages/position-lifecycle-review/tests + - name: Build and install exact package artifact + run: | + rm -rf /tmp/orgmetra-position-lifecycle-review-build /tmp/orgmetra-position-lifecycle-review-dist /tmp/orgmetra-position-lifecycle-review-venv + cp -a packages/position-lifecycle-review /tmp/orgmetra-position-lifecycle-review-build + mkdir -p /tmp/orgmetra-position-lifecycle-review-dist + python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/orgmetra-position-lifecycle-review-dist /tmp/orgmetra-position-lifecycle-review-build + test "$(find /tmp/orgmetra-position-lifecycle-review-dist -maxdepth 1 -type f -name '*.whl' | wc -l)" -eq 1 + python -m venv /tmp/orgmetra-position-lifecycle-review-venv + /tmp/orgmetra-position-lifecycle-review-venv/bin/python -m pip install --require-hashes --no-deps --only-binary=:all: -r "$GITHUB_WORKSPACE/.github/requirements/foundation-test.txt" + wheel_path="$(find /tmp/orgmetra-position-lifecycle-review-dist -maxdepth 1 -type f -name '*.whl' -print -quit)" + wheel_sha="$(sha256sum "$wheel_path" | awk '{print $1}')" + printf 'orgmetra-position-lifecycle-review[test] @ file://%s --hash=sha256:%s\n' "$wheel_path" "$wheel_sha" > /tmp/orgmetra-position-lifecycle-review-install.txt + /tmp/orgmetra-position-lifecycle-review-venv/bin/python -m pip install --require-hashes --no-deps -r /tmp/orgmetra-position-lifecycle-review-install.txt + /tmp/orgmetra-position-lifecycle-review-venv/bin/python -m pip check + - name: Test installed artifact with exact statement and branch coverage + env: + COVERAGE_FILE: /tmp/orgmetra-position-lifecycle-review.coverage + run: | + cd /tmp + /tmp/orgmetra-position-lifecycle-review-venv/bin/python -m pytest -c "$GITHUB_WORKSPACE/packages/position-lifecycle-review/pyproject.toml" "$GITHUB_WORKSPACE/packages/position-lifecycle-review/tests" + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From 2991342c364a64be5f1bdc37dee3ed1fbcd5e860 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:11:36 -0700 Subject: [PATCH 04/23] test(position): isolate lifecycle review correlations --- .../tests/test_review.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/position-lifecycle-review/tests/test_review.py b/packages/position-lifecycle-review/tests/test_review.py index ff279cdc6..b8d611e27 100644 --- a/packages/position-lifecycle-review/tests/test_review.py +++ b/packages/position-lifecycle-review/tests/test_review.py @@ -2,9 +2,8 @@ from dataclasses import replace from datetime import date, datetime, timedelta, timezone -from decimal import Decimal import gc -from uuid import UUID +from uuid import UUID, uuid4 import pytest @@ -26,7 +25,7 @@ def build_packet(**overrides: object) -> PositionLifecycleChangeReviewPacket: values: dict[str, object] = { "tenant_record_id": TENANT, "position_record_id": POSITION, - "position_lifecycle_change_reference": CHANGE, + "position_lifecycle_change_reference": uuid4(), "current_status_code": "active", "proposed_status_code": "frozen", "effective_on": date(2026, 9, 1), @@ -62,7 +61,7 @@ def test_approved_packet_is_deterministic_and_value_minimized() -> None: assert "employee" not in encoded.lower() assert "person" not in encoded.lower() assert "compensation" not in encoded.lower() - assert POSITION.hex in encoded + assert str(POSITION) in encoded assert "PositionLifecycleChangeReviewPacket(redacted)" == repr(packet) @@ -184,7 +183,7 @@ def test_post_construction_payload_tampering_fails_closed() -> None: def test_live_change_reference_cannot_bind_conflicting_evidence() -> None: """One live tenant-qualified review reference cannot denote two reviewed truths.""" - packet = build_packet() + packet = build_packet(position_lifecycle_change_reference=CHANGE) with pytest.raises(ValueError): replace(packet, proposed_status_code="closed", reason_code="position_closure") duplicate = replace(packet) @@ -193,7 +192,7 @@ def test_live_change_reference_cannot_bind_conflicting_evidence() -> None: def test_reference_binding_releases_only_after_all_duplicates_die() -> None: """Live duplicate accounting keeps a correlation bound until the last packet is gone.""" - packet = build_packet() + packet = build_packet(position_lifecycle_change_reference=CHANGE) duplicate = replace(packet) key_values = { "tenant_record_id": packet.tenant_record_id, @@ -209,9 +208,8 @@ def test_reference_binding_releases_only_after_all_duplicates_die() -> None: assert replacement.proposed_status_code == "closed" -def test_non_decimal_noise_is_not_part_of_the_packet_contract() -> None: - """The package deliberately carries no staffing allocation or compensation value.""" +def test_staffing_and_compensation_values_are_not_packet_fields() -> None: + """The review evidence deliberately carries no allocation or compensation value.""" packet = build_packet() assert not hasattr(packet, "allocation_ratio") assert not hasattr(packet, "compensation_amount") - assert Decimal("1.0") == Decimal("1.0") From f54de33bb8afec9702819461e361e0231cdf0ca2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:12:12 -0700 Subject: [PATCH 05/23] feat(position): implement governed lifecycle review evidence --- .../review.py | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py diff --git a/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py new file mode 100644 index 000000000..fa9ee6168 --- /dev/null +++ b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py @@ -0,0 +1,236 @@ +"""Governed, value-minimized evidence for reviewing a Position lifecycle change. + +This package deliberately does not mutate Position truth. It records one human +review of a proposed lifecycle transition and remains fail-closed until an +Orgmetra authoritative host re-resolves current Position and Assignment truth, +re-authorizes actors, and persists the later mutation with immutable audit/outbox. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, datetime, timezone +from hashlib import sha256 +import json +import re +from threading import RLock +import weakref +from uuid import UUID + +_MAX_UUID_INT = (1 << 128) - 1 +_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_STATUSES = frozenset({"active", "open", "closed", "frozen", "abolished"}) +_REASONS = frozenset({"temporary_freeze", "position_reactivation", "position_closure", "position_abolition"}) +_OUTCOMES = frozenset({"approved_for_authoritative_resolution", "rejected"}) +_ALLOWED_TRANSITIONS = { + "open": frozenset({"active", "frozen", "closed", "abolished"}), + "active": frozenset({"frozen", "closed", "abolished"}), + "frozen": frozenset({"open", "active", "closed", "abolished"}), + "closed": frozenset({"open", "abolished"}), + "abolished": frozenset(), +} +_EXPECTED_REASON = { + "active": "position_reactivation", + "open": "position_reactivation", + "frozen": "temporary_freeze", + "closed": "position_closure", + "abolished": "position_abolition", +} +_LOCK = RLock() +_ISSUANCE_DIGESTS: dict[int, str] = {} +_REFERENCE_BINDINGS: dict[tuple[UUID, UUID], tuple[str, int]] = {} + + +def _require_operational_uuid(field_name: str, value: object) -> UUID: + """Return one exact operational UUID while rejecting protocol sentinels.""" + if type(value) is not UUID or value.int in (0, _MAX_UUID_INT): + raise ValueError(f"{field_name} must be an operational UUID.") + return value + + +def _require_uuid4(field_name: str, value: object) -> UUID: + """Return one exact UUIDv4 owned by this evidence packet.""" + if type(value) is not UUID or value.version != 4: + raise ValueError(f"{field_name} must be a UUIDv4.") + return value + + +def _require_text(field_name: str, value: object, allowed: frozenset[str]) -> str: + """Require exact built-in governance text before membership evaluation.""" + if type(value) is not str: + raise TypeError(f"{field_name} must be exact built-in str.") + if value not in allowed: + raise ValueError(f"{field_name} is not in the governed vocabulary.") + return value + + +def _require_digest(field_name: str, value: object) -> str: + """Require the lowercase textual form of one SHA-256 digest.""" + if type(value) is not str: + raise TypeError(f"{field_name} must be exact built-in str.") + if _SHA256_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be a lowercase SHA-256 digest.") + return value + + +def _require_actor_reference(field_name: str, value: object) -> str: + """Require one Orgmetra-local pseudonymous actor UUIDv4 correlation.""" + if type(value) is not str or not value.startswith("actor:"): + raise ValueError(f"{field_name} must be an actor: UUIDv4 correlation.") + suffix = value.removeprefix("actor:") + try: + parsed = UUID(suffix) + except ValueError as error: + raise ValueError(f"{field_name} must be an actor: UUIDv4 correlation.") from error + if parsed.version != 4 or str(parsed) != suffix: + raise ValueError(f"{field_name} must be an actor: UUIDv4 correlation.") + return value + + +def _require_utc(field_name: str, value: object) -> datetime: + """Require an exact built-in UTC datetime already detached from caller timezone code.""" + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError(f"{field_name} must be an exact UTC datetime.") + return value + + +def _utc_text(value: datetime) -> str: + """Return deterministic RFC 3339-compatible UTC text.""" + return value.isoformat().replace("+00:00", "Z") + + +def _canonical_bytes(document: dict[str, object]) -> bytes: + """Serialize one canonical evidence document deterministically.""" + return json.dumps(document, separators=(",", ":"), sort_keys=True, ensure_ascii=False).encode("utf-8") + + +def _release_binding(object_id: int, key: tuple[UUID, UUID], digest: str) -> None: + """Release one process-local issuance/reference binding after packet collection.""" + with _LOCK: + _ISSUANCE_DIGESTS.pop(object_id, None) + bound_digest, count = _REFERENCE_BINDINGS[key] + assert bound_digest == digest + if count == 1: + del _REFERENCE_BINDINGS[key] + else: + _REFERENCE_BINDINGS[key] = (digest, count - 1) + + +@dataclass(frozen=True, slots=True, weakref_slot=True) +class PositionLifecycleChangeReviewPacket: + """Human-reviewed, non-authorizing evidence for one Position lifecycle proposal.""" + + tenant_record_id: UUID + position_record_id: UUID + position_lifecycle_change_reference: UUID + current_status_code: str + proposed_status_code: str + effective_on: date + position_snapshot_digest_sha256: str + assignment_snapshot_digest_sha256: str + requester_actor_reference: str + reviewer_actor_reference: str + reason_code: str + review_outcome_code: str + evidence_version: int + reviewed_at: datetime + recorded_at: datetime + + def __post_init__(self) -> None: + """Validate reviewed evidence and seal live correlation semantics.""" + _require_operational_uuid("tenant_record_id", self.tenant_record_id) + _require_operational_uuid("position_record_id", self.position_record_id) + _require_uuid4("position_lifecycle_change_reference", self.position_lifecycle_change_reference) + current = _require_text("current_status_code", self.current_status_code, _STATUSES) + proposed = _require_text("proposed_status_code", self.proposed_status_code, _STATUSES) + reason = _require_text("reason_code", self.reason_code, _REASONS) + _require_text("review_outcome_code", self.review_outcome_code, _OUTCOMES) + if proposed not in _ALLOWED_TRANSITIONS[current]: + raise ValueError("proposed_status_code is not an allowed reviewed transition from current_status_code.") + if reason != _EXPECTED_REASON[proposed]: + raise ValueError("reason_code does not match the proposed Position lifecycle state.") + if type(self.effective_on) is not date: + raise TypeError("effective_on must be an exact business date.") + _require_digest("position_snapshot_digest_sha256", self.position_snapshot_digest_sha256) + _require_digest("assignment_snapshot_digest_sha256", self.assignment_snapshot_digest_sha256) + requester = _require_actor_reference("requester_actor_reference", self.requester_actor_reference) + reviewer = _require_actor_reference("reviewer_actor_reference", self.reviewer_actor_reference) + if requester == reviewer: + raise ValueError("requester and reviewer must be distinct pseudonymous actors.") + if type(self.evidence_version) is not int or self.evidence_version != 1: + raise ValueError("evidence_version must be exact integer 1.") + reviewed_at = _require_utc("reviewed_at", self.reviewed_at) + recorded_at = _require_utc("recorded_at", self.recorded_at) + if recorded_at < reviewed_at: + raise ValueError("recorded_at must be at or after reviewed_at.") + + document = self._payload() + digest = sha256(_canonical_bytes(document)).hexdigest() + key = (self.tenant_record_id, self.position_lifecycle_change_reference) + with _LOCK: + existing = _REFERENCE_BINDINGS.get(key) + if existing is None: + _REFERENCE_BINDINGS[key] = (digest, 1) + else: + existing_digest, count = existing + if existing_digest != digest: + raise ValueError("position lifecycle change reference is already bound to conflicting live evidence.") + _REFERENCE_BINDINGS[key] = (digest, count + 1) + _ISSUANCE_DIGESTS[id(self)] = digest + weakref.finalize(self, _release_binding, id(self), key, digest) + + def _payload(self) -> dict[str, object]: + """Return the complete value-minimized canonical evidence snapshot.""" + next_action = ( + "Do not apply the proposed Position lifecycle change." + if self.review_outcome_code == "rejected" + else ( + "Re-resolve tenant-qualified Position and Assignment truth at the requested business/system " + "coordinate; require authoritative actor separation, reviewed evidence, staffing safety, " + "and immutable audit/outbox before any lifecycle mutation." + ) + ) + return { + "assignment_snapshot_digest_sha256": self.assignment_snapshot_digest_sha256, + "current_status_code": self.current_status_code, + "decision_authority": "human_review_only", + "effective_on": self.effective_on.isoformat(), + "evidence_version": self.evidence_version, + "mutation_state": "not_authorized_to_apply", + "next_action": next_action, + "position_lifecycle_change_reference": str(self.position_lifecycle_change_reference), + "position_record_id": str(self.position_record_id), + "position_snapshot_digest_sha256": self.position_snapshot_digest_sha256, + "proposed_status_code": self.proposed_status_code, + "reason_code": self.reason_code, + "recorded_at": _utc_text(self.recorded_at), + "requester_actor_reference": self.requester_actor_reference, + "review_outcome_code": self.review_outcome_code, + "review_state": "human_reviewed", + "reviewed_at": _utc_text(self.reviewed_at), + "reviewer_actor_reference": self.reviewer_actor_reference, + "scope_verification_state": "requires_authoritative_resolution", + "tenant_record_id": str(self.tenant_record_id), + } + + def canonical_document(self) -> dict[str, object]: + """Return the same snapshot whose digest remains bound to issuance.""" + document = self._payload() + digest = sha256(_canonical_bytes(document)).hexdigest() + with _LOCK: + issued_digest = _ISSUANCE_DIGESTS.get(id(self)) + if issued_digest != digest: + raise ValueError("position lifecycle review evidence changed after issuance.") + return document + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + return _canonical_bytes(self.canonical_document()).decode("utf-8") + + def content_digest(self) -> str: + """Return SHA-256 of the verified canonical evidence bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + def __repr__(self) -> str: + """Avoid leaking tenant, Position, actor, or evidence correlations in routine logs.""" + return "PositionLifecycleChangeReviewPacket(redacted)" From cb4bfa05d5b4ac76d45bd17907ffd1dbe62d2259 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:12:18 -0700 Subject: [PATCH 06/23] feat(position): expose lifecycle review contract --- .../src/orgmetra_position_lifecycle_review/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/__init__.py diff --git a/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/__init__.py b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/__init__.py new file mode 100644 index 000000000..7ac34d88e --- /dev/null +++ b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/__init__.py @@ -0,0 +1,5 @@ +"""Public API for governed Position lifecycle-change review evidence.""" + +from .review import PositionLifecycleChangeReviewPacket + +__all__ = ["PositionLifecycleChangeReviewPacket"] From 9d21850d8bd79fe7aa20c7660d766b041095cf74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:12:53 -0700 Subject: [PATCH 07/23] test(position): cover lifecycle validation edges --- .../tests/test_validation_edges.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 packages/position-lifecycle-review/tests/test_validation_edges.py diff --git a/packages/position-lifecycle-review/tests/test_validation_edges.py b/packages/position-lifecycle-review/tests/test_validation_edges.py new file mode 100644 index 000000000..6e67c78e2 --- /dev/null +++ b/packages/position-lifecycle-review/tests/test_validation_edges.py @@ -0,0 +1,79 @@ +"""Focused failure-boundary coverage for Position lifecycle review evidence.""" + +from datetime import date, datetime, timezone +from uuid import UUID, uuid4 + +import pytest + +from orgmetra_position_lifecycle_review import PositionLifecycleChangeReviewPacket + +TENANT = UUID("0198a800-1111-7000-8000-000000000001") +POSITION = UUID("0198a800-2222-7000-8000-000000000002") +REQUESTER = "actor:11111111-1111-4111-8111-111111111111" +REVIEWER = "actor:22222222-2222-4222-8222-222222222222" +NOW = datetime(2026, 8, 24, 12, 0, tzinfo=timezone.utc) + + +def packet(**overrides: object) -> PositionLifecycleChangeReviewPacket: + """Build one valid packet while keeping each edge test independent.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "position_record_id": POSITION, + "position_lifecycle_change_reference": uuid4(), + "current_status_code": "active", + "proposed_status_code": "frozen", + "effective_on": date(2026, 9, 1), + "position_snapshot_digest_sha256": "a" * 64, + "assignment_snapshot_digest_sha256": "b" * 64, + "requester_actor_reference": REQUESTER, + "reviewer_actor_reference": REVIEWER, + "reason_code": "temporary_freeze", + "review_outcome_code": "approved_for_authoritative_resolution", + "evidence_version": 1, + "reviewed_at": NOW, + "recorded_at": NOW, + } + values.update(overrides) + return PositionLifecycleChangeReviewPacket(**values) # type: ignore[arg-type] + + +def test_non_uuid_and_non_uuid4_values_fail_closed() -> None: + """Short-circuit UUID type branches are exercised explicitly.""" + with pytest.raises(ValueError): + packet(tenant_record_id="0198a800-1111-7000-8000-000000000001") + with pytest.raises(ValueError): + packet(position_lifecycle_change_reference="12345678-1234-4abc-8def-1234567890ab") + with pytest.raises(ValueError): + packet(position_lifecycle_change_reference=TENANT) + + +def test_digest_and_actor_runtime_types_fail_before_parsing() -> None: + """Caller-defined/coercible scalar types cannot enter digest or actor parsing.""" + with pytest.raises(TypeError): + packet(position_snapshot_digest_sha256=123) + with pytest.raises(ValueError): + packet(requester_actor_reference=123) + with pytest.raises(ValueError): + packet(requester_actor_reference="not-an-actor-reference") + + +def test_actor_uuid_must_be_canonical_uuid4() -> None: + """Actor correlation accepts neither UUIDv7 nor noncanonical UUIDv4 text.""" + with pytest.raises(ValueError): + packet(requester_actor_reference="actor:0198a800-1111-7000-8000-000000000001") + with pytest.raises(ValueError): + packet(requester_actor_reference="actor:11111111-1111-4111-8111-11111111111A") + + +def test_reason_must_match_proposed_state() -> None: + """A valid reason token cannot be rebound to a semantically different target state.""" + with pytest.raises(ValueError): + packet(proposed_status_code="closed", reason_code="temporary_freeze") + + +def test_evidence_version_rejects_boolean_and_timestamp_rejects_non_datetime() -> None: + """Boolean/int coercion and date/datetime coercion are not accepted as evidence schema/time.""" + with pytest.raises(ValueError): + packet(evidence_version=True) + with pytest.raises(ValueError): + packet(reviewed_at=date(2026, 8, 24)) From 31ec54c4fffa95242116c6355ff1f21748a7a5f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:13:15 -0700 Subject: [PATCH 08/23] docs(position): explain lifecycle review boundary --- packages/position-lifecycle-review/README.md | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 packages/position-lifecycle-review/README.md diff --git a/packages/position-lifecycle-review/README.md b/packages/position-lifecycle-review/README.md new file mode 100644 index 000000000..5c3713998 --- /dev/null +++ b/packages/position-lifecycle-review/README.md @@ -0,0 +1,41 @@ +# Orgmetra Position Lifecycle Review + +This package records **human-reviewed evidence for a proposed lifecycle change to an existing Position**. It is deliberately not the authoritative Position mutation boundary. + +## Why it exists + +Protected Orgmetra already treats `Job`, `Position`, and `Assignment` as separate HRIS facts and recognizes Position statuses `open`, `active`, `frozen`, `closed`, and `abolished`. A commercial HRIS also needs review evidence before a seat is frozen, closed, abolished, or reactivated, because those changes can alter staffing availability and downstream workforce reporting. + +`PositionLifecycleChangeReviewPacket` binds: + +- tenant and Position operational UUIDs; +- one packet-owned UUIDv4 change correlation; +- current/proposed lifecycle status and business-effective date; +- exact reviewed Position and Assignment snapshot SHA-256 digests; +- pseudonymous requester/reviewer UUIDv4 actor correlations with separation; +- controlled reason and human review outcome; +- evidence schema version 1; +- human-review time and later-or-equal system-recorded UTC time. + +It intentionally carries **no Person/candidate identity, name, email, compensation, assessment, rating, allocation value, credential, prompt, or model output**. + +## Authority boundary + +Even an `approved_for_authoritative_resolution` review remains: + +- `human_reviewed`; +- `requires_authoritative_resolution`; +- `not_authorized_to_apply`; +- `human_review_only`. + +Before a lifecycle mutation, the Orgmetra host must freshly re-resolve tenant-qualified bitemporal Position and Assignment truth at the requested business/system coordinate, re-resolve requester/reviewer authority, prove staffing safety, validate the reviewed evidence, and commit the actual Position mutation with immutable audit/outbox in the authoritative transaction. A rejected review must not be applied. + +The packet permits reviewed transitions among normal seat states but treats `abolished` as terminal. No-op status changes are rejected. A reason token is tied to the proposed state so a valid token cannot be reused with different lifecycle semantics. + +## Integrity boundary + +Trust-bearing scalar values require exact built-in runtime types. Canonical evidence is deterministic JSON with SHA-256 content digest and a redacted routine representation. A process-local issuance seal detects post-construction field rewriting, while a live tenant-qualified change-reference binding prevents one correlation from denoting conflicting evidence while any duplicate packet remains alive. These are defense-in-depth controls only; durable uniqueness and mutation authorization belong to authoritative persistence/audit. + +## Scope + +This package writes only Orgmetra evidence. It does not modify Keyverse, Naruon, Contextual Orchestrator, or another dedicated-writer repository and does not query a foreign application table. From 4ce018b2a1f82271e4fb95c3917baaebfacda795 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:13:26 -0700 Subject: [PATCH 09/23] docs(position): record lifecycle review contract --- packages/position-lifecycle-review/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 packages/position-lifecycle-review/CHANGELOG.md diff --git a/packages/position-lifecycle-review/CHANGELOG.md b/packages/position-lifecycle-review/CHANGELOG.md new file mode 100644 index 000000000..04ea5e756 --- /dev/null +++ b/packages/position-lifecycle-review/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 0.1.0 - Unreleased + +- Add `PositionLifecycleChangeReviewPacket` for value-minimized human review of existing Position status changes. +- Keep all reviewed evidence non-authorizing until fresh authoritative bitemporal Position/Assignment resolution and immutable audit/outbox mutation. +- Reject no-op transitions, resurrection of abolished Positions, ungoverned status/reason/outcome vocabulary, noncanonical actor/digest/time evidence, hostile runtime scalar subclasses, post-issuance mutation, and conflicting live change-reference reuse. +- Add exact-head installed-wheel testing with exact 100% owned statement/branch coverage. From 2a0c88c583ed6ec98d4f1f8ffbc92cfd0a6146ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:13:39 -0700 Subject: [PATCH 10/23] docs(position): record lifecycle review ADR --- ...0111-governed-position-lifecycle-review.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/adr/0111-governed-position-lifecycle-review.md diff --git a/docs/adr/0111-governed-position-lifecycle-review.md b/docs/adr/0111-governed-position-lifecycle-review.md new file mode 100644 index 000000000..2d6d8ac92 --- /dev/null +++ b/docs/adr/0111-governed-position-lifecycle-review.md @@ -0,0 +1,31 @@ +# ADR 0111: Govern Position lifecycle-change review separately from mutation + +Status: Proposed + +## Context + +Protected `develop` stores Job, Position, and Assignment separately and already recognizes Position lifecycle vocabulary, but the shipped People mutation path only creates Position records. Vacancy evidence and vacancy-to-assignment orchestration do not own a review contract for freezing, closing, abolishing, or reactivating an existing seat. + +A lifecycle change can affect staffing availability and later workforce evidence. Reusing a generic Position-creation command or a reporting-line review would blur evidence ownership and could let cached/UI state substitute for current bitemporal truth. + +## Decision + +Add an Orgmetra-owned `PositionLifecycleChangeReviewPacket` as a transport-neutral, value-minimized human-review artifact. + +The packet binds one tenant-qualified Position, current/proposed lifecycle state, business-effective date, reviewed Position/Assignment snapshot digests, pseudonymous requester/reviewer separation, controlled reason/outcome, evidence version, and human-review/system-recorded UTC chronology. `abolished` is terminal and no-op transitions are rejected. + +The packet never authorizes mutation. An approved review remains `requires_authoritative_resolution` and `not_authorized_to_apply`. The later authoritative host must freshly resolve bitemporal Position and Assignment truth, re-establish actor authority/separation and staffing safety, validate the reviewed evidence, and commit the mutation with immutable audit/outbox. + +## Consequences + +- Lifecycle review evidence cannot silently become Position truth. +- Existing Position/Assignment source-of-truth boundaries remain unchanged. +- No Person/candidate identity, compensation, assessment, rating, allocation value, credential, prompt, or model output is copied into the review evidence. +- A later bounded mutation/persistence slice remains necessary; this ADR does not claim it is shipped. +- Process-local issuance/reference binding is defense in depth, not distributed durability or authorization. + +## Alternatives rejected + +1. **Encode lifecycle change as a new Position creation.** Rejected because it would conflate stable Position identity with versioned lifecycle state. +2. **Reuse vacancy or reporting-line review evidence.** Rejected because those artifacts prove different facts and do not own lifecycle semantics. +3. **Allow the review packet itself to mutate Position truth.** Rejected because current Position/Assignment truth and staffing safety must be re-resolved at the authoritative transaction boundary. From 4f4eea5edb179c065637a2f5c07d8474d8a7372c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:13:56 -0700 Subject: [PATCH 11/23] docs(position): trace lifecycle review evidence --- .../traceability/position-lifecycle-review.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/traceability/position-lifecycle-review.md diff --git a/docs/traceability/position-lifecycle-review.md b/docs/traceability/position-lifecycle-review.md new file mode 100644 index 000000000..9247dec31 --- /dev/null +++ b/docs/traceability/position-lifecycle-review.md @@ -0,0 +1,26 @@ +# Position lifecycle review traceability + +## Truth state + +- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has distinct Job/Position/Assignment HRIS facts, Position status vocabulary, Position vacancy evidence dependencies, purpose-bound authorization, and immutable audit/outbox foundations. It can create Position records but has no dedicated lifecycle-change review artifact or authoritative existing-Position lifecycle mutation. +- **Active PR truth:** this branch adds only the governed review evidence packet and its exact-head quality gate. +- **Planned:** authoritative bitemporal Position lifecycle mutation/persistence that consumes reviewed evidence, re-resolves staffing truth, and atomically records immutable audit/outbox. +- **Out of scope:** autonomous employment decisions, Person/candidate data, compensation, assessment/rating data, reporting-line mutation, Keyverse/Naruon/other CWL repository writes, and direct foreign application-table SQL. + +## Requirement mapping + +| Requirement | Evidence | +|---|---| +| Separate Position identity from lifecycle review evidence | `PositionLifecycleChangeReviewPacket`; ADR 0111 | +| Preserve business-effective and system-recorded time | `effective_on`, `reviewed_at`, `recorded_at`; chronology regressions | +| Human review with requester/reviewer separation | pseudonymous actor UUIDv4 correlations; separation regression | +| Purposefully non-authorizing high-impact evidence | fixed `human_reviewed`, `requires_authoritative_resolution`, `not_authorized_to_apply`, `human_review_only` states | +| Re-resolve staffing truth before mutation | reviewed Position/Assignment snapshot SHA-256 digests plus fixed approved next action | +| Minimize PII and high-impact payloads | packet carries no Person/candidate identity, allocation, compensation, rating, assessment, free text, credential, prompt, or model output | +| Fail closed on lifecycle ambiguity | explicit transition vocabulary; no-op rejection; abolished terminal; reason bound to target status | +| Tamper/correlation defense in depth | issuance digest verification; live tenant-qualified review-reference binding; adversarial `object.__setattr__` and `dataclasses.replace` regressions | +| Exact owned test coverage | `Position Lifecycle Review Quality` installed-wheel gate with `--cov-branch --cov-fail-under=100` | + +## Buyer behavior + +A buyer-facing workflow can collect a review of a proposed Position freeze/closure/abolition/reactivation without claiming the seat has changed. If the review outcome is rejected, the only next action is not to apply it. If approved for authoritative resolution, the next action explicitly requires fresh tenant-qualified Position/Assignment truth, actor authority/separation, staffing safety, evidence validation, and immutable audit/outbox before any mutation. From b9e85a1b8eb92f168fd261aa150a6204490c8023 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 05:14:07 -0700 Subject: [PATCH 12/23] docs(position): add lifecycle review primary references --- .../position-lifecycle-review-references.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/doctoring/position-lifecycle-review-references.md diff --git a/docs/doctoring/position-lifecycle-review-references.md b/docs/doctoring/position-lifecycle-review-references.md new file mode 100644 index 000000000..fae0f872c --- /dev/null +++ b/docs/doctoring/position-lifecycle-review-references.md @@ -0,0 +1,17 @@ +# Position lifecycle review — primary references + +Reviewed 2026-08-24 (UTC/KST calendar date as applicable). These references support identifier, privacy-risk, and workforce-evidence design only; Orgmetra does not claim certification or reproduce licensed ISO metric definitions. + +## APA 7 references + +Davis, K., Peabody, B., & Leach, P. (2024). *Universally Unique IDentifiers (UUIDs)* (RFC 9562). RFC Editor. https://doi.org/10.17487/RFC9562 + +International Organization for Standardization. (2025). *Human resource management—Requirements and recommendations for human capital reporting and disclosure* (ISO 30414:2025, 2nd ed.). https://www.iso.org/standard/30414 + +National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management, version 1.0* (NIST CSWP 10). https://doi.org/10.6028/NIST.CSWP.10 + +## Design use + +- RFC 9562 is the primary identifier source for UUID layouts, including UUIDv4, UUIDv7, Nil, and Max. Orgmetra keeps authoritative HRIS operational UUID interoperability separate from packet-owned UUIDv4 correlation identifiers. +- ISO 30414:2025 is currently published as Edition 2 (2025-08) and includes workforce composition and mobility/succession among its human-capital reporting areas. It motivates historically defensible workforce/Position evidence, not a licensed metric implementation or conformity claim. +- NIST Privacy Framework 1.0 is a final, voluntary risk-management framework. It motivates value minimization and separating durable governance evidence from unnecessary Person/candidate/compensation payloads. NIST also lists Privacy Framework 1.1 as a newer project; this ADR does not present non-final work as the final baseline. From 0fe08c8c3c3579a041e62e8c6b84c23ccca50e8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:08:45 -0700 Subject: [PATCH 13/23] test(position): require truthful branch-protection docs --- .../tests/test_repository_truth.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/position-lifecycle-review/tests/test_repository_truth.py diff --git a/packages/position-lifecycle-review/tests/test_repository_truth.py b/packages/position-lifecycle-review/tests/test_repository_truth.py new file mode 100644 index 000000000..80ad9f4c4 --- /dev/null +++ b/packages/position-lifecycle-review/tests/test_repository_truth.py @@ -0,0 +1,20 @@ +"""Repository-state truth contract for Position lifecycle review documentation.""" + +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_DOCUMENTS = ( + _REPOSITORY_ROOT / "docs/traceability/position-lifecycle-review.md", + _REPOSITORY_ROOT / "docs/adr/0111-governed-position-lifecycle-review.md", + _REPOSITORY_ROOT / "packages/position-lifecycle-review/README.md", +) + + +def test_lifecycle_review_docs_do_not_claim_unenforced_branch_protection() -> None: + """Buyer-facing docs must distinguish default-branch truth from protection state.""" + for document_path in _DOCUMENTS: + document = document_path.read_text(encoding="utf-8") + assert "issue #89" in document + assert "Protected `develop`" not in document + assert "Protected Orgmetra" not in document From 794dc507498280d992497b83913aa5507c1bb863 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:08:57 -0700 Subject: [PATCH 14/23] docs(position): distinguish product truth from branch protection --- docs/traceability/position-lifecycle-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/position-lifecycle-review.md b/docs/traceability/position-lifecycle-review.md index 9247dec31..118f2d66b 100644 --- a/docs/traceability/position-lifecycle-review.md +++ b/docs/traceability/position-lifecycle-review.md @@ -2,7 +2,7 @@ ## Truth state -- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has distinct Job/Position/Assignment HRIS facts, Position status vocabulary, Position vacancy evidence dependencies, purpose-bound authorization, and immutable audit/outbox foundations. It can create Position records but has no dedicated lifecycle-change review artifact or authoritative existing-Position lifecycle mutation. +- **Default-branch product truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has distinct Job/Position/Assignment HRIS facts, Position status vocabulary, Position vacancy evidence dependencies, purpose-bound authorization, and immutable audit/outbox foundations. It can create Position records but has no dedicated lifecycle-change review artifact or authoritative existing-Position lifecycle mutation. Enforceable branch protection is **not** part of this product-truth claim; repository-governance issue #89 must be resolved before any merge is authorized. - **Active PR truth:** this branch adds only the governed review evidence packet and its exact-head quality gate. - **Planned:** authoritative bitemporal Position lifecycle mutation/persistence that consumes reviewed evidence, re-resolves staffing truth, and atomically records immutable audit/outbox. - **Out of scope:** autonomous employment decisions, Person/candidate data, compensation, assessment/rating data, reporting-line mutation, Keyverse/Naruon/other CWL repository writes, and direct foreign application-table SQL. From bfedd3e26e76dd860a5d735e62b4804a7e8a8379 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:09:08 -0700 Subject: [PATCH 15/23] docs(position): record protection restoration prerequisite --- docs/adr/0111-governed-position-lifecycle-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0111-governed-position-lifecycle-review.md b/docs/adr/0111-governed-position-lifecycle-review.md index 2d6d8ac92..2a01c3ce6 100644 --- a/docs/adr/0111-governed-position-lifecycle-review.md +++ b/docs/adr/0111-governed-position-lifecycle-review.md @@ -4,7 +4,7 @@ Status: Proposed ## Context -Protected `develop` stores Job, Position, and Assignment separately and already recognizes Position lifecycle vocabulary, but the shipped People mutation path only creates Position records. Vacancy evidence and vacancy-to-assignment orchestration do not own a review contract for freezing, closing, abolishing, or reactivating an existing seat. +Current `develop` product truth stores Job, Position, and Assignment separately and already recognizes Position lifecycle vocabulary, but the shipped People mutation path only creates Position records. Repository-governance issue #89 records that enforceable `develop` branch protection is currently absent; restoring that protection remains a prerequisite for merge authorization and is not implied by this ADR. Vacancy evidence and vacancy-to-assignment orchestration do not own a review contract for freezing, closing, abolishing, or reactivating an existing seat. A lifecycle change can affect staffing availability and later workforce evidence. Reusing a generic Position-creation command or a reporting-line review would blur evidence ownership and could let cached/UI state substitute for current bitemporal truth. From cfff42f5cf884ff67169ddeff645c6933e19337a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 10:09:19 -0700 Subject: [PATCH 16/23] docs(position): make lifecycle README protection-truthful --- packages/position-lifecycle-review/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/position-lifecycle-review/README.md b/packages/position-lifecycle-review/README.md index 5c3713998..8f255c5b9 100644 --- a/packages/position-lifecycle-review/README.md +++ b/packages/position-lifecycle-review/README.md @@ -4,7 +4,7 @@ This package records **human-reviewed evidence for a proposed lifecycle change t ## Why it exists -Protected Orgmetra already treats `Job`, `Position`, and `Assignment` as separate HRIS facts and recognizes Position statuses `open`, `active`, `frozen`, `closed`, and `abolished`. A commercial HRIS also needs review evidence before a seat is frozen, closed, abolished, or reactivated, because those changes can alter staffing availability and downstream workforce reporting. +Current Orgmetra `develop` product truth already treats `Job`, `Position`, and `Assignment` as separate HRIS facts and recognizes Position statuses `open`, `active`, `frozen`, `closed`, and `abolished`. This statement describes shipped product behavior only: enforceable branch protection is currently absent and repository-governance issue #89 must be resolved before merge authorization. A commercial HRIS also needs review evidence before a seat is frozen, closed, abolished, or reactivated, because those changes can alter staffing availability and downstream workforce reporting. `PositionLifecycleChangeReviewPacket` binds: From ef3f453aaeeaa2ab16ac270aaa67d9d3f4602f02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 01:12:29 -0700 Subject: [PATCH 17/23] test(position-lifecycle): reject obsolete protection-absent claims --- .../tests/test_repository_truth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/position-lifecycle-review/tests/test_repository_truth.py b/packages/position-lifecycle-review/tests/test_repository_truth.py index 80ad9f4c4..1e123e5d0 100644 --- a/packages/position-lifecycle-review/tests/test_repository_truth.py +++ b/packages/position-lifecycle-review/tests/test_repository_truth.py @@ -11,10 +11,12 @@ ) -def test_lifecycle_review_docs_do_not_claim_unenforced_branch_protection() -> None: - """Buyer-facing docs must distinguish default-branch truth from protection state.""" +def test_lifecycle_review_docs_describe_effective_repository_controls() -> None: + """Buyer-facing docs must not mistake classic protection for effective rules.""" for document_path in _DOCUMENTS: document = document_path.read_text(encoding="utf-8") assert "issue #89" in document + assert "effective organization ruleset" in document + assert "branch protection is currently absent" not in document assert "Protected `develop`" not in document assert "Protected Orgmetra" not in document From 6fb064e53e488b2db4cd4b150689e9e75e47fe0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 01:12:48 -0700 Subject: [PATCH 18/23] docs(position-lifecycle): align README with effective ruleset --- packages/position-lifecycle-review/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/position-lifecycle-review/README.md b/packages/position-lifecycle-review/README.md index 8f255c5b9..2d7c161d1 100644 --- a/packages/position-lifecycle-review/README.md +++ b/packages/position-lifecycle-review/README.md @@ -4,7 +4,7 @@ This package records **human-reviewed evidence for a proposed lifecycle change t ## Why it exists -Current Orgmetra `develop` product truth already treats `Job`, `Position`, and `Assignment` as separate HRIS facts and recognizes Position statuses `open`, `active`, `frozen`, `closed`, and `abolished`. This statement describes shipped product behavior only: enforceable branch protection is currently absent and repository-governance issue #89 must be resolved before merge authorization. A commercial HRIS also needs review evidence before a seat is frozen, closed, abolished, or reactivated, because those changes can alter staffing availability and downstream workforce reporting. +Current Orgmetra `develop` product truth already treats `Job`, `Position`, and `Assignment` as separate HRIS facts and recognizes Position statuses `open`, `active`, `frozen`, `closed`, and `abolished`. Repository control is separate from that product claim: an **effective organization ruleset** governs the default branch with pull-request integration, two approvals, stale-review dismissal, last-push approval, required conversation resolution, central required workflows, and non-fast-forward/deletion protection. Repository-governance issue #89 remains open for the narrower acquisition-grade gaps around routine administrator `always` bypass and executable proof that every applicable Orgmetra-local gate is fail-closed required. A commercial HRIS also needs review evidence before a seat is frozen, closed, abolished, or reactivated, because those changes can alter staffing availability and downstream workforce reporting. `PositionLifecycleChangeReviewPacket` binds: From 8545b4ba085410823ef31a3f5b8ecd3bf2bac7f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 01:13:01 -0700 Subject: [PATCH 19/23] docs(position-lifecycle): align ADR with effective ruleset --- docs/adr/0111-governed-position-lifecycle-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0111-governed-position-lifecycle-review.md b/docs/adr/0111-governed-position-lifecycle-review.md index 2a01c3ce6..fd536fa31 100644 --- a/docs/adr/0111-governed-position-lifecycle-review.md +++ b/docs/adr/0111-governed-position-lifecycle-review.md @@ -4,7 +4,7 @@ Status: Proposed ## Context -Current `develop` product truth stores Job, Position, and Assignment separately and already recognizes Position lifecycle vocabulary, but the shipped People mutation path only creates Position records. Repository-governance issue #89 records that enforceable `develop` branch protection is currently absent; restoring that protection remains a prerequisite for merge authorization and is not implied by this ADR. Vacancy evidence and vacancy-to-assignment orchestration do not own a review contract for freezing, closing, abolishing, or reactivating an existing seat. +Current `develop` product truth stores Job, Position, and Assignment separately and already recognizes Position lifecycle vocabulary, but the shipped People mutation path only creates Position records. Repository governance is a separate concern: an **effective organization ruleset** applies to the default branch and requires pull-request integration, two approvals, stale-review dismissal, last-push approval, required conversation resolution, central required workflows, and non-fast-forward/deletion protection. Issue #89 remains open for the narrower commercial-control gaps around routine administrator `always` bypass and executable proof that every applicable Orgmetra-local gate is fail-closed required. None of those repository controls is implied by this ADR. Vacancy evidence and vacancy-to-assignment orchestration do not own a review contract for freezing, closing, abolishing, or reactivating an existing seat. A lifecycle change can affect staffing availability and later workforce evidence. Reusing a generic Position-creation command or a reporting-line review would blur evidence ownership and could let cached/UI state substitute for current bitemporal truth. From 631002e17a723e32f27bb100bccfbee046c5c18a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 01:13:14 -0700 Subject: [PATCH 20/23] docs(position-lifecycle): align traceability with effective ruleset --- docs/traceability/position-lifecycle-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/position-lifecycle-review.md b/docs/traceability/position-lifecycle-review.md index 118f2d66b..dd27a859d 100644 --- a/docs/traceability/position-lifecycle-review.md +++ b/docs/traceability/position-lifecycle-review.md @@ -2,7 +2,7 @@ ## Truth state -- **Default-branch product truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has distinct Job/Position/Assignment HRIS facts, Position status vocabulary, Position vacancy evidence dependencies, purpose-bound authorization, and immutable audit/outbox foundations. It can create Position records but has no dedicated lifecycle-change review artifact or authoritative existing-Position lifecycle mutation. Enforceable branch protection is **not** part of this product-truth claim; repository-governance issue #89 must be resolved before any merge is authorized. +- **Default-branch product truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` has distinct Job/Position/Assignment HRIS facts, Position status vocabulary, Position vacancy evidence dependencies, purpose-bound authorization, and immutable audit/outbox foundations. It can create Position records but has no dedicated lifecycle-change review artifact or authoritative existing-Position lifecycle mutation. Repository governance is separate: an **effective organization ruleset** applies to the default branch and requires pull-request integration, two approvals, stale-review dismissal, last-push approval, required conversation resolution, central required workflows, and non-fast-forward/deletion protection. Issue #89 tracks the remaining routine administrator `always` bypass and Orgmetra-local fail-closed gate-proof gaps. - **Active PR truth:** this branch adds only the governed review evidence packet and its exact-head quality gate. - **Planned:** authoritative bitemporal Position lifecycle mutation/persistence that consumes reviewed evidence, re-resolves staffing truth, and atomically records immutable audit/outbox. - **Out of scope:** autonomous employment decisions, Person/candidate data, compensation, assessment/rating data, reporting-line mutation, Keyverse/Naruon/other CWL repository writes, and direct foreign application-table SQL. From ca19d1879bbb048f309c9d56b7c90547bc91c578 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 01:32:26 -0700 Subject: [PATCH 21/23] test(position-lifecycle): accept semantic issue reference casing --- .../position-lifecycle-review/tests/test_repository_truth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/position-lifecycle-review/tests/test_repository_truth.py b/packages/position-lifecycle-review/tests/test_repository_truth.py index 1e123e5d0..c0e1d06aa 100644 --- a/packages/position-lifecycle-review/tests/test_repository_truth.py +++ b/packages/position-lifecycle-review/tests/test_repository_truth.py @@ -15,7 +15,7 @@ def test_lifecycle_review_docs_describe_effective_repository_controls() -> None: """Buyer-facing docs must not mistake classic protection for effective rules.""" for document_path in _DOCUMENTS: document = document_path.read_text(encoding="utf-8") - assert "issue #89" in document + assert "issue #89" in document.lower() assert "effective organization ruleset" in document assert "branch protection is currently absent" not in document assert "Protected `develop`" not in document From de9fc672eeac30c58a18f2bfa5941707ee396e0b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 23:22:33 +0900 Subject: [PATCH 22/23] fix(position-lifecycle): make binding-drift release path coverable _release_binding now raises an explicit AssertionError with a governance message instead of a bare assert, keeping the drift branch measurable under the 100% branch gate; regression drives the drifted-digest path directly. Suite stays at 100% statement+branch coverage (36 tests). Addresses Devin review observation on PR #111. --- .../review.py | 3 ++- .../tests/test_review.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py index fa9ee6168..c01c776d8 100644 --- a/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py +++ b/packages/position-lifecycle-review/src/orgmetra_position_lifecycle_review/review.py @@ -109,7 +109,8 @@ def _release_binding(object_id: int, key: tuple[UUID, UUID], digest: str) -> Non with _LOCK: _ISSUANCE_DIGESTS.pop(object_id, None) bound_digest, count = _REFERENCE_BINDINGS[key] - assert bound_digest == digest + if bound_digest != digest: + raise AssertionError("reference binding digest drifted from its issuance") if count == 1: del _REFERENCE_BINDINGS[key] else: diff --git a/packages/position-lifecycle-review/tests/test_review.py b/packages/position-lifecycle-review/tests/test_review.py index b8d611e27..e10045de3 100644 --- a/packages/position-lifecycle-review/tests/test_review.py +++ b/packages/position-lifecycle-review/tests/test_review.py @@ -213,3 +213,21 @@ def test_staffing_and_compensation_values_are_not_packet_fields() -> None: packet = build_packet() assert not hasattr(packet, "allocation_ratio") assert not hasattr(packet, "compensation_amount") + + +def test_release_binding_raises_on_digest_drift() -> None: + """Fail loudly if a binding's digest drifted from its issuance before release.""" + packet = build_packet() + reference = ( + TENANT, + packet.position_lifecycle_change_reference, + ) + import orgmetra_position_lifecycle_review.review as review_module + + key = (TENANT, uuid4()) + review_module._REFERENCE_BINDINGS[key] = ("0" * 64, 1) + try: + with pytest.raises(AssertionError, match="digest drifted"): + review_module._release_binding(id(packet), key, POSITION_DIGEST) + finally: + review_module._REFERENCE_BINDINGS.pop(key, None) From 03f3f6de674ee07cf33b9d2a75f58a8d210d7ed8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 26 Aug 2026 06:39:13 +0900 Subject: [PATCH 23/23] test(position-lifecycle): drop unused local in drift regression --- packages/position-lifecycle-review/tests/test_review.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/position-lifecycle-review/tests/test_review.py b/packages/position-lifecycle-review/tests/test_review.py index e10045de3..7430ffc3a 100644 --- a/packages/position-lifecycle-review/tests/test_review.py +++ b/packages/position-lifecycle-review/tests/test_review.py @@ -218,10 +218,6 @@ def test_staffing_and_compensation_values_are_not_packet_fields() -> None: def test_release_binding_raises_on_digest_drift() -> None: """Fail loudly if a binding's digest drifted from its issuance before release.""" packet = build_packet() - reference = ( - TENANT, - packet.position_lifecycle_change_reference, - ) import orgmetra_position_lifecycle_review.review as review_module key = (TENANT, uuid4())