From dfbb22e927246052dd56d47b8fd36ca0c22dfab4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:14:47 -0700 Subject: [PATCH 01/63] feat: add compensation change review quality gate --- .../compensation-change-review-quality.yml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/compensation-change-review-quality.yml diff --git a/.github/workflows/compensation-change-review-quality.yml b/.github/workflows/compensation-change-review-quality.yml new file mode 100644 index 000000000..64a2b4dbb --- /dev/null +++ b/.github/workflows/compensation-change-review-quality.yml @@ -0,0 +1,57 @@ +name: Compensation Change Review Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/compensation-change-review/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/compensation-change-review-quality.yml" + - "docs/adr/0022-governed-compensation-change-review.md" + - "docs/doctoring/compensation-change-review-references.md" + - "docs/traceability/compensation-change-review.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: compensation-change-review-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Compensation change 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 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 compensation change review package + run: python -m compileall -q packages/compensation-change-review/src packages/compensation-change-review/tests + - name: Test compensation change review with exact statement and branch coverage + env: + PYTHONPATH: packages/compensation-change-review/src + COVERAGE_FILE: /tmp/orgmetra-compensation-change-review.coverage + run: python -m pytest -c packages/compensation-change-review/pyproject.toml packages/compensation-change-review/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From 7486d44219ceb7d805fb018bc7f4aaeaf19275eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:02 -0700 Subject: [PATCH 02/63] docs: add governed compensation change ADR --- ...022-governed-compensation-change-review.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/adr/0022-governed-compensation-change-review.md diff --git a/docs/adr/0022-governed-compensation-change-review.md b/docs/adr/0022-governed-compensation-change-review.md new file mode 100644 index 000000000..f210e0ee2 --- /dev/null +++ b/docs/adr/0022-governed-compensation-change-review.md @@ -0,0 +1,53 @@ +# ADR 0022: Govern compensation changes before authoritative mutation + +- Status: Proposed (active PR only) +- Date: 2026-08-19 + +## Context + +Compensation changes are high-impact employment actions. A useful enterprise review boundary must correlate the proposed change to authoritative worker scope, the exact current/proposed compensation artifacts, the governing policy, pay-equity review, budget authorization, and payroll handoff without copying salary, wage, bonus, benefit, equity, protected-attribute, or free-form case values into portable evidence. + +Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. + +Current primary-source context is recorded in `docs/doctoring/compensation-change-review-references.md`. ISO 30414:2025 provides current human-capital reporting/disclosure context; U.S. EEOC compensation guidance demonstrates why compensation governance must not turn protected-attribute review into uncontrolled evidence copying; U.S. Department of Labor FLSA recordkeeping guidance demonstrates the need for accountable wage/time records. These are governance inputs only, not certification or universal legal-compliance claims. + +## Decision + +Orgmetra will expose a value-minimized `CompensationChangeReviewPacket` before any authoritative compensation-related HRIS mutation or payroll execution. + +The packet binds one canonical tenant to UUID-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. + +The packet deliberately excludes compensation values, protected-attribute values, free-form case narrative, credentials, and free-form model output. It explicitly acknowledges remaining personal-data correlation with `contains_personal_data = true`. + +Direct construction and replacement fail closed unless the packet remains: + +- `human_confirmation_required = true`; +- `decision_authority = human_review_only`; +- `review_state = requires_human_review`; +- `scope_verification_state = requires_authoritative_resolution`; +- `mutation_state = not_authorized_to_apply`; and +- `external_execution_state = not_authorized_to_execute`. + +Requester and reviewer opaque references must differ, but authoritative separation of duties requires both identities to be re-resolved inside the packet tenant immediately before approval. The host must also re-resolve every packet reference, prove Person-to-Employment and active Assignment/Job/Position scope, and verify the current compensation snapshot, proposed plan, exact compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance without copying compensation or protected-attribute values into the packet. + +Any authorized HRIS change must use the authoritative Orgmetra People boundary with its own purpose-bound authorization, idempotency, bitemporal persistence, and immutable audit/outbox evidence. Payroll execution remains behind the payroll owner's published contract. This package performs no foreign mutation and no direct cross-service application-table SQL. + +## Consequences + +### Positive + +- A review envelope cannot masquerade as compensation approval, an applied HRIS change, or completed payroll execution. +- Buyers can correlate exact policy/equity/budget/proposed-plan evidence while minimizing duplicated pay and protected-attribute values. +- Changes to governed evidence or `evidence_version` change the canonical packet digest. +- Cross-tenant, wrong-worker, stale-policy, actor-separation, and effective-date questions remain explicit authoritative-resolution obligations instead of being inferred from reference syntax. + +### Trade-offs + +- The packet is not anonymous; opaque worker/evidence correlations still require purpose-bound access, retention, export, and audit controls. +- A pay-equity evidence reference/digest proves which artifact was reviewed, not that compensation is fair, nondiscriminatory, or legally sufficient. +- The packet does not calculate compensation, payroll, taxes, protected-class statistics, pay-equity findings, or legal conclusions. +- Hosts must perform authoritative scope and actor resolution at approval time. + +## Verification + +The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict UUID-backed opaque references; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. From c6e19da442ad693fae58d513170791951b32e7aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:10 -0700 Subject: [PATCH 03/63] docs: record compensation review references --- .../compensation-change-review-references.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/doctoring/compensation-change-review-references.md diff --git a/docs/doctoring/compensation-change-review-references.md b/docs/doctoring/compensation-change-review-references.md new file mode 100644 index 000000000..9023334f0 --- /dev/null +++ b/docs/doctoring/compensation-change-review-references.md @@ -0,0 +1,15 @@ +# Compensation change review references + +Verified/retrieved: August 19, 2026. + +## APA 7 references + +International Organization for Standardization. (2025). *ISO 30414:2025 Human resource management — Requirements and recommendations for human capital reporting and disclosure* (2nd ed.). https://www.iso.org/standard/30414 + +U.S. Equal Employment Opportunity Commission. (2000). *Section 10: Compensation discrimination*. https://www.eeoc.gov/laws/guidance/section-10-compensation-discrimination + +U.S. Department of Labor, Wage and Hour Division. (n.d.). *Fact Sheet #21: Recordkeeping requirements under the Fair Labor Standards Act (FLSA).* Retrieved August 19, 2026, from https://www.dol.gov/agencies/whd/fact-sheets/21-flsa-recordkeeping + +## Applied boundary + +These sources support a conservative governance boundary: compensation evidence should be versioned, attributable, reviewable, and minimized rather than copied into a broad review envelope. U.S. EEOC and DOL material is jurisdiction-specific context, not a universal rule engine. This package does not infer discrimination, determine legal compliance, calculate pay, perform payroll, or claim ISO certification. It binds only the exact reviewed policy/equity/budget/compensation-plan artifacts and preserves accountable human review before authoritative action. From c29f3cbca7223a06ab33741a7e1aea1c5a869535 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:19 -0700 Subject: [PATCH 04/63] docs: trace compensation change review --- .../compensation-change-review.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/traceability/compensation-change-review.md diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md new file mode 100644 index 000000000..1ea13989b --- /dev/null +++ b/docs/traceability/compensation-change-review.md @@ -0,0 +1,21 @@ +# Compensation change review traceability + +Status: **active PR only / proposed**, not protected-`develop` product truth until merged. + +| Buyer or governance need | Contract | Executable evidence | +| --- | --- | --- | +| Correlate a proposed pay change without copying pay amounts | Opaque Person/Employment/current/proposed compensation references + independent digests; compensation/protected values excluded | `test_builds_deterministic_value_minimized_packet`, `test_every_reference_and_digest_is_validated` | +| Prevent reference fields from becoming a PII/value smuggling channel | Every trust-bearing reference is the expected namespace plus canonical non-sentinel UUID | `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated` | +| Preserve accountable separation of duties | Requester/reviewer references differ; canonical next action requires tenant-scoped identity re-resolution and distinct resolved actors | `test_same_opaque_actor_reference_is_rejected_early`, `test_next_action_requires_identity_scope_and_evidence_before_approval` | +| Prevent review evidence from authorizing a high-impact action | Human-only review, unresolved authoritative scope, no HRIS mutation, no external execution are immutable | `test_direct_constructor_and_replace_fail_closed` | +| Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | +| Avoid accidental logging of worker/pay correlations | Generated dataclass representation disabled; custom representation fully redacted | `test_repr_redacts_personal_and_compensation_correlations` | +| Preserve exact audit time semantics | Timezone-aware timestamps normalize to UTC without discarding fractional precision | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected` | + +## Quality gate + +`.github/workflows/compensation-change-review-quality.yml` checks out the exact PR head, uses the repository's hash-pinned reviewed Python test toolchain, compiles source/tests, requires 100% package statement and branch coverage, and rejects checkout side effects. + +## Ownership boundary + +This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. From 519236e9a4949be1efe898e68cd701f1e37d7462 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:27 -0700 Subject: [PATCH 05/63] docs: add compensation review changelog --- packages/compensation-change-review/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/compensation-change-review/CHANGELOG.md diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md new file mode 100644 index 000000000..29951c6a4 --- /dev/null +++ b/packages/compensation-change-review/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## [Unreleased] + +### Added + +- `CompensationChangeReviewPacket`, a value-minimized pre-mutation evidence contract that binds authoritative worker scope, reviewed compensation-plan/policy evidence, pay-equity review, budget authorization, and payroll handoff provenance without copying compensation or protected-attribute values. +- Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. +- Deterministic canonical JSON/SHA-256 evidence, redacted representations, strict UUID-backed opaque references, and exact 100% owned statement/branch coverage regressions. From 6cf658779f1a3b2b332f430521e22fd9508d908a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:41 -0700 Subject: [PATCH 06/63] docs: explain compensation change review --- packages/compensation-change-review/README.md | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/compensation-change-review/README.md diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md new file mode 100644 index 000000000..93a8623d6 --- /dev/null +++ b/packages/compensation-change-review/README.md @@ -0,0 +1,57 @@ +# Orgmetra Compensation Change Review + +`orgmetra-compensation-change-review` defines a portable, value-minimized evidence envelope for reviewing a proposed compensation change before any authoritative HRIS mutation or payroll execution. + +## What the packet carries + +The packet correlates one tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. Trust-bearing artifacts use canonical UUID-backed opaque references; evidence artifacts also carry independent lowercase SHA-256 digests. + +The envelope intentionally **does not carry salary, wage, bonus, benefit, equity, protected-attribute, credential, or free-form case/model values**. Opaque Person/Employment and evidence references still create sensitive personal-data correlation, so the packet explicitly reports `contains_personal_data = true` rather than claiming anonymity. + +## Human and system boundary + +A valid packet is not an approval. It remains: + +- `decision_authority = human_review_only`; +- `review_state = requires_human_review`; +- `scope_verification_state = requires_authoritative_resolution`; +- `mutation_state = not_authorized_to_apply`; and +- `external_execution_state = not_authorized_to_execute`. + +Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. + +## Example + +```python +from datetime import date, datetime, timezone +from orgmetra_compensation_change_review import build_compensation_change_review_packet + +packet = build_compensation_change_review_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + compensation_review_reference="compensation_change_review:22222222-2222-4222-8222-222222222222", + person_record_reference="person_record:33333333-3333-4333-8333-333333333333", + employment_record_reference="employment_record:44444444-4444-4444-8444-444444444444", + active_assignment_snapshot_reference="active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + active_assignment_snapshot_digest="a" * 64, + current_compensation_snapshot_reference="compensation_snapshot:66666666-6666-4666-8666-666666666666", + current_compensation_snapshot_digest="b" * 64, + proposed_compensation_plan_reference="compensation_plan:77777777-7777-4777-8777-777777777777", + proposed_compensation_plan_digest="c" * 64, + compensation_policy_reference="compensation_policy:88888888-8888-4888-8888-888888888888", + compensation_policy_digest="d" * 64, + pay_equity_review_reference="pay_equity_review:99999999-9999-4999-8999-999999999999", + pay_equity_review_digest="e" * 64, + budget_authorization_reference="budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + budget_authorization_digest="f" * 64, + payroll_handoff_plan_reference="payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + payroll_handoff_plan_digest="1" * 64, + requester_reference="actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + reviewer_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + purpose_code="compensation_change_review", + reason_code="annual_compensation_review", + proposed_effective_on=date(2026, 10, 1), + generated_at=datetime.now(timezone.utc), +) +``` + +`packet.sha256_digest()` is immutable correlation evidence only. It is not proof that pay is fair, lawful, budgeted, approved, applied in the HRIS, or executed in payroll. From cf170a0791a1a28970edcb839125a4ee067e3bf3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:48 -0700 Subject: [PATCH 07/63] feat: add compensation review package metadata --- .../compensation-change-review/pyproject.toml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 packages/compensation-change-review/pyproject.toml diff --git a/packages/compensation-change-review/pyproject.toml b/packages/compensation-change-review/pyproject.toml new file mode 100644 index 000000000..b9e2c44b3 --- /dev/null +++ b/packages/compensation-change-review/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-compensation-change-review" +version = "0.1.0" +description = "Governed pre-mutation compensation change review evidence for Orgmetra." +requires-python = ">=3.12" + +[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_compensation_change_review", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] From 2611ea8efbeaec846b3e1afab51a866560b22c7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:56 -0700 Subject: [PATCH 08/63] feat: export compensation review contract --- .../src/orgmetra_compensation_change_review/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/compensation-change-review/src/orgmetra_compensation_change_review/__init__.py diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/__init__.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/__init__.py new file mode 100644 index 000000000..55aa42147 --- /dev/null +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/__init__.py @@ -0,0 +1,5 @@ +"""Governed compensation-change review contracts for Orgmetra.""" + +from .packet import CompensationChangeReviewPacket, build_compensation_change_review_packet + +__all__ = ["CompensationChangeReviewPacket", "build_compensation_change_review_packet"] From 989eee36357de6c064455c8bce84b39396df3fa8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:16:41 -0700 Subject: [PATCH 09/63] feat: add governed compensation change review packet --- .../packet.py | 353 ++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py new file mode 100644 index 000000000..9b9d4d4b7 --- /dev/null +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -0,0 +1,353 @@ +"""Governed, value-minimized pre-mutation compensation-change review evidence. + +The packet correlates one proposed compensation change to authoritative worker scope and +exact reviewed policy/evidence artifacts without copying pay amounts, protected-attribute +values, narrative case material, credentials, or model output into the review envelope. +Opaque worker and evidence references remain sensitive correlating metadata. Authoritative +scope resolution, human approval, HRIS mutation, and payroll execution remain outside this +package and must occur through their published owner boundaries. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import date, datetime, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) +_PURPOSE_CODE = "compensation_change_review" +_ALLOWED_REASON_CODES = frozenset( + { + "annual_compensation_review", + "promotion_compensation_review", + "market_adjustment_review", + "retention_adjustment_review", + "role_change_compensation_review", + } +) +_DECISION_AUTHORITY = "human_review_only" +_REVIEW_STATE = "requires_human_review" +_SCOPE_VERIFICATION_STATE = "requires_authoritative_resolution" +_MUTATION_STATE = "not_authorized_to_apply" +_EXTERNAL_EXECUTION_STATE = "not_authorized_to_execute" +_NEXT_ACTION = ( + "Re-resolve every packet reference within tenant_record_id; specifically re-resolve " + "requester_reference and reviewer_reference within tenant_record_id and verify their " + "resolved actor identities are distinct, then prove the Person-to-Employment binding " + "and active Assignment/Job/Position scope represented by the snapshot, then verify the " + "current compensation snapshot, proposed compensation plan, exact compensation policy, " + "pay-equity review, budget authorization, proposed effective date, and payroll-handoff " + "provenance without copying compensation or protected-attribute values into this packet; " + "then record accountable human approval, apply any authorized HRIS change only through " + "the authoritative Orgmetra People boundary, and execute payroll only through its " + "published owner contract." +) + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require canonical non-sentinel UUID text for a governance identity.""" + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_reference(value: str, prefix: str, field_name: str) -> None: + """Require the expected namespace plus a canonical operational UUID suffix.""" + message = f"{field_name} must be an opaque {prefix}: reference" + if ( + not isinstance(value, str) + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(message) from exc + if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + raise ValueError(message) + + +def _validate_digest(value: str, field_name: str) -> None: + """Require lowercase SHA-256 hexadecimal evidence.""" + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _canonical_timestamp(value: datetime) -> str: + """Render an aware instant as precision-preserving UTC RFC 3339 text.""" + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise ValueError("generated_at must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def _validate_business_date(value: date, field_name: str) -> None: + """Require a business date rather than a datetime or textual date.""" + if type(value) is not date: + raise ValueError(f"{field_name} must be a date") + + +def _validate_evidence_version(value: int) -> None: + """Require a bounded positive integer version for high-impact review evidence.""" + if type(value) is not int or value < 1 or value > 2_147_483_647: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + + +@dataclass(frozen=True, slots=True, repr=False) +class CompensationChangeReviewPacket: + """Immutable compensation-review evidence that cannot authorize mutation or payroll.""" + + tenant_record_id: str + compensation_review_reference: str + person_record_reference: str + employment_record_reference: str + active_assignment_snapshot_reference: str + active_assignment_snapshot_digest: str + current_compensation_snapshot_reference: str + current_compensation_snapshot_digest: str + proposed_compensation_plan_reference: str + proposed_compensation_plan_digest: str + compensation_policy_reference: str + compensation_policy_digest: str + pay_equity_review_reference: str + pay_equity_review_digest: str + budget_authorization_reference: str + budget_authorization_digest: str + payroll_handoff_plan_reference: str + payroll_handoff_plan_digest: str + requester_reference: str + reviewer_reference: str + purpose_code: str + reason_code: str + proposed_effective_on: date + generated_at: datetime + evidence_version: int = 1 + contains_personal_data: bool = True + contains_compensation_values: bool = False + contains_protected_attribute_values: bool = False + contains_free_form_case_narrative: bool = False + contains_free_form_model_output: bool = False + human_confirmation_required: bool = True + decision_authority: str = _DECISION_AUTHORITY + review_state: str = _REVIEW_STATE + scope_verification_state: str = _SCOPE_VERIFICATION_STATE + mutation_state: str = _MUTATION_STATE + external_execution_state: str = _EXTERNAL_EXECUTION_STATE + next_action: str = _NEXT_ACTION + + def __repr__(self) -> str: + """Return a representation that never emits sensitive correlation evidence.""" + return "CompensationChangeReviewPacket()" + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed contract.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference( + self.compensation_review_reference, + "compensation_change_review", + "compensation_review_reference", + ) + _validate_reference(self.person_record_reference, "person_record", "person_record_reference") + _validate_reference( + self.employment_record_reference, + "employment_record", + "employment_record_reference", + ) + _validate_reference( + self.active_assignment_snapshot_reference, + "active_assignment_snapshot", + "active_assignment_snapshot_reference", + ) + _validate_digest(self.active_assignment_snapshot_digest, "active_assignment_snapshot_digest") + _validate_reference( + self.current_compensation_snapshot_reference, + "compensation_snapshot", + "current_compensation_snapshot_reference", + ) + _validate_digest( + self.current_compensation_snapshot_digest, + "current_compensation_snapshot_digest", + ) + _validate_reference( + self.proposed_compensation_plan_reference, + "compensation_plan", + "proposed_compensation_plan_reference", + ) + _validate_digest( + self.proposed_compensation_plan_digest, + "proposed_compensation_plan_digest", + ) + _validate_reference( + self.compensation_policy_reference, + "compensation_policy", + "compensation_policy_reference", + ) + _validate_digest(self.compensation_policy_digest, "compensation_policy_digest") + _validate_reference( + self.pay_equity_review_reference, + "pay_equity_review", + "pay_equity_review_reference", + ) + _validate_digest(self.pay_equity_review_digest, "pay_equity_review_digest") + _validate_reference( + self.budget_authorization_reference, + "budget_authorization", + "budget_authorization_reference", + ) + _validate_digest(self.budget_authorization_digest, "budget_authorization_digest") + _validate_reference( + self.payroll_handoff_plan_reference, + "payroll_handoff_plan", + "payroll_handoff_plan_reference", + ) + _validate_digest(self.payroll_handoff_plan_digest, "payroll_handoff_plan_digest") + _validate_reference(self.requester_reference, "actor", "requester_reference") + _validate_reference(self.reviewer_reference, "actor", "reviewer_reference") + if self.requester_reference == self.reviewer_reference: + raise ValueError("requester and reviewer must be different actor references") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain compensation_change_review") + if self.reason_code not in _ALLOWED_REASON_CODES: + raise ValueError("reason_code must be an approved non-sensitive compensation-review category") + _validate_business_date(self.proposed_effective_on, "proposed_effective_on") + _canonical_timestamp(self.generated_at) + _validate_evidence_version(self.evidence_version) + if self.contains_personal_data is not True: + raise ValueError("compensation review packet must acknowledge sensitive personal-data correlation") + if self.contains_compensation_values is not False: + raise ValueError("compensation review packet must not contain compensation values") + if self.contains_protected_attribute_values is not False: + raise ValueError("compensation review packet must not contain protected-attribute values") + if self.contains_free_form_case_narrative is not False: + raise ValueError("compensation review packet must not contain free-form case narrative") + if self.contains_free_form_model_output is not False: + raise ValueError("compensation review packet must not contain free-form model output") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before a compensation change") + if self.decision_authority != _DECISION_AUTHORITY: + raise ValueError("decision_authority must remain human_review_only") + if self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_review") + if self.scope_verification_state != _SCOPE_VERIFICATION_STATE: + raise ValueError("scope_verification_state must remain requires_authoritative_resolution") + if self.mutation_state != _MUTATION_STATE: + raise ValueError("mutation_state must remain not_authorized_to_apply") + if self.external_execution_state != _EXTERNAL_EXECUTION_STATE: + raise ValueError("external_execution_state must remain not_authorized_to_execute") + if self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed compensation-change instruction") + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + payload = { + "active_assignment_snapshot_digest": self.active_assignment_snapshot_digest, + "active_assignment_snapshot_reference": self.active_assignment_snapshot_reference, + "budget_authorization_digest": self.budget_authorization_digest, + "budget_authorization_reference": self.budget_authorization_reference, + "compensation_policy_digest": self.compensation_policy_digest, + "compensation_policy_reference": self.compensation_policy_reference, + "compensation_review_reference": self.compensation_review_reference, + "contains_compensation_values": self.contains_compensation_values, + "contains_free_form_case_narrative": self.contains_free_form_case_narrative, + "contains_free_form_model_output": self.contains_free_form_model_output, + "contains_personal_data": self.contains_personal_data, + "contains_protected_attribute_values": self.contains_protected_attribute_values, + "current_compensation_snapshot_digest": self.current_compensation_snapshot_digest, + "current_compensation_snapshot_reference": self.current_compensation_snapshot_reference, + "decision_authority": self.decision_authority, + "employment_record_reference": self.employment_record_reference, + "evidence_version": self.evidence_version, + "external_execution_state": self.external_execution_state, + "generated_at": _canonical_timestamp(self.generated_at), + "human_confirmation_required": self.human_confirmation_required, + "mutation_state": self.mutation_state, + "next_action": self.next_action, + "pay_equity_review_digest": self.pay_equity_review_digest, + "pay_equity_review_reference": self.pay_equity_review_reference, + "payroll_handoff_plan_digest": self.payroll_handoff_plan_digest, + "payroll_handoff_plan_reference": self.payroll_handoff_plan_reference, + "person_record_reference": self.person_record_reference, + "proposed_compensation_plan_digest": self.proposed_compensation_plan_digest, + "proposed_compensation_plan_reference": self.proposed_compensation_plan_reference, + "proposed_effective_on": self.proposed_effective_on.isoformat(), + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "requester_reference": self.requester_reference, + "review_state": self.review_state, + "reviewer_reference": self.reviewer_reference, + "scope_verification_state": self.scope_verification_state, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 compensation-review packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_compensation_change_review_packet( + *, + tenant_record_id: str, + compensation_review_reference: str, + person_record_reference: str, + employment_record_reference: str, + active_assignment_snapshot_reference: str, + active_assignment_snapshot_digest: str, + current_compensation_snapshot_reference: str, + current_compensation_snapshot_digest: str, + proposed_compensation_plan_reference: str, + proposed_compensation_plan_digest: str, + compensation_policy_reference: str, + compensation_policy_digest: str, + pay_equity_review_reference: str, + pay_equity_review_digest: str, + budget_authorization_reference: str, + budget_authorization_digest: str, + payroll_handoff_plan_reference: str, + payroll_handoff_plan_digest: str, + requester_reference: str, + reviewer_reference: str, + purpose_code: str, + reason_code: str, + proposed_effective_on: date, + generated_at: datetime, + evidence_version: int = 1, +) -> CompensationChangeReviewPacket: + """Build a value-minimized compensation packet pending authoritative human approval.""" + return CompensationChangeReviewPacket( + tenant_record_id=tenant_record_id, + compensation_review_reference=compensation_review_reference, + person_record_reference=person_record_reference, + employment_record_reference=employment_record_reference, + active_assignment_snapshot_reference=active_assignment_snapshot_reference, + active_assignment_snapshot_digest=active_assignment_snapshot_digest, + current_compensation_snapshot_reference=current_compensation_snapshot_reference, + current_compensation_snapshot_digest=current_compensation_snapshot_digest, + proposed_compensation_plan_reference=proposed_compensation_plan_reference, + proposed_compensation_plan_digest=proposed_compensation_plan_digest, + compensation_policy_reference=compensation_policy_reference, + compensation_policy_digest=compensation_policy_digest, + pay_equity_review_reference=pay_equity_review_reference, + pay_equity_review_digest=pay_equity_review_digest, + budget_authorization_reference=budget_authorization_reference, + budget_authorization_digest=budget_authorization_digest, + payroll_handoff_plan_reference=payroll_handoff_plan_reference, + payroll_handoff_plan_digest=payroll_handoff_plan_digest, + requester_reference=requester_reference, + reviewer_reference=reviewer_reference, + purpose_code=purpose_code, + reason_code=reason_code, + proposed_effective_on=proposed_effective_on, + generated_at=generated_at, + evidence_version=evidence_version, + ) From 2b78f0d3cfca5216b7c13794d296fd7550e9a8d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:17:15 -0700 Subject: [PATCH 10/63] test: cover compensation change review contract --- .../tests/test_packet.py | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_packet.py diff --git a/packages/compensation-change-review/tests/test_packet.py b/packages/compensation-change-review/tests/test_packet.py new file mode 100644 index 000000000..30b2dbce4 --- /dev/null +++ b/packages/compensation-change-review/tests/test_packet.py @@ -0,0 +1,243 @@ +"""Regression coverage for governed compensation-change review evidence.""" +from dataclasses import replace +from datetime import date, datetime, timedelta, timezone, tzinfo +import json +import re + +import pytest + +from orgmetra_compensation_change_review import ( + CompensationChangeReviewPacket, + build_compensation_change_review_packet, +) + + +def valid_kwargs() -> dict[str, object]: + """Return one complete valid packet input set with opaque deterministic identities.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def build_valid() -> CompensationChangeReviewPacket: + """Build one valid packet through the supported public builder.""" + return build_compensation_change_review_packet(**valid_kwargs()) + + +def test_builds_deterministic_value_minimized_packet() -> None: + """A valid packet is canonical, human-only, unresolved, and excludes protected values.""" + packet = build_valid() + canonical = json.loads(packet.canonical_json()) + + assert packet.decision_authority == "human_review_only" + assert packet.review_state == "requires_human_review" + assert packet.scope_verification_state == "requires_authoritative_resolution" + assert packet.mutation_state == "not_authorized_to_apply" + assert packet.external_execution_state == "not_authorized_to_execute" + assert packet.contains_personal_data is True + assert packet.contains_compensation_values is False + assert packet.contains_protected_attribute_values is False + assert canonical["generated_at"] == "2026-08-19T06:12:13.456789Z" + assert canonical["evidence_version"] == 1 + assert len(packet.sha256_digest()) == 64 + assert packet.sha256_digest() == build_valid().sha256_digest() + + +def test_canonical_evidence_changes_with_governed_artifact_or_version() -> None: + """Changing exact reviewed evidence or its version changes the immutable packet digest.""" + packet = build_valid() + changed_plan = replace(packet, proposed_compensation_plan_digest="2" * 64) + changed_policy = replace(packet, compensation_policy_digest="3" * 64) + changed_version = replace(packet, evidence_version=2) + + assert len({packet.sha256_digest(), changed_plan.sha256_digest(), changed_policy.sha256_digest(), changed_version.sha256_digest()}) == 4 + + +def test_repr_redacts_personal_and_compensation_correlations() -> None: + """Normal repr formatting cannot leak worker, actor, or evidence correlation identifiers.""" + packet = build_valid() + rendered = repr(packet) + + assert rendered == "CompensationChangeReviewPacket()" + for sensitive in ( + packet.tenant_record_id, + packet.person_record_reference, + packet.employment_record_reference, + packet.current_compensation_snapshot_reference, + packet.proposed_compensation_plan_reference, + packet.requester_reference, + packet.current_compensation_snapshot_digest, + ): + assert sensitive not in rendered + + +def test_next_action_requires_identity_scope_and_evidence_before_approval() -> None: + """The canonical operator instruction orders tenant, actor, worker, and evidence checks first.""" + action = build_valid().next_action + actor_clause = "verify their resolved actor identities are distinct" + worker_clause = "prove the Person-to-Employment binding" + evidence_clause = "verify the current compensation snapshot" + approval_clause = "record accountable human approval" + + assert action.startswith("Re-resolve every packet reference within tenant_record_id;") + assert action.index(actor_clause) < action.index(worker_clause) < action.index(evidence_clause) < action.index(approval_clause) + assert "without copying compensation or protected-attribute values" in action + assert action.endswith("execute payroll only through its published owner contract.") + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("contains_personal_data", False, "acknowledge sensitive personal-data correlation"), + ("contains_compensation_values", True, "must not contain compensation values"), + ("contains_protected_attribute_values", True, "must not contain protected-attribute values"), + ("contains_free_form_case_narrative", True, "must not contain free-form case narrative"), + ("contains_free_form_model_output", True, "must not contain free-form model output"), + ("human_confirmation_required", False, "human confirmation is mandatory"), + ("decision_authority", "model_authority", "must remain human_review_only"), + ("review_state", "approved", "must remain requires_human_review"), + ("scope_verification_state", "resolved", "must remain requires_authoritative_resolution"), + ("mutation_state", "authorized", "must remain not_authorized_to_apply"), + ("external_execution_state", "executed", "must remain not_authorized_to_execute"), + ("next_action", "Send directly to payroll.", "next_action must remain"), + ], +) +def test_direct_constructor_and_replace_fail_closed(field_name: str, value: object, message: str) -> None: + """Both construction paths reject attempts to weaken immutable high-impact guardrails.""" + direct_kwargs = valid_kwargs() + direct_kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + CompensationChangeReviewPacket(**direct_kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: value}) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("tenant_record_id", "not-a-uuid", "canonical UUID text"), + ("tenant_record_id", 42, "canonical UUID text"), + ("tenant_record_id", "00000000-0000-0000-0000-000000000000", "canonical operational UUID"), + ("tenant_record_id", "ffffffff-ffff-ffff-ffff-ffffffffffff", "canonical operational UUID"), + ("tenant_record_id", "11111111-1111-4111-8111-11111111111A", "canonical operational UUID"), + ("person_record_reference", "person_record:seongho", "opaque person_record"), + ("person_record_reference", 42, "opaque person_record"), + ("person_record_reference", "actor:33333333-3333-4333-8333-333333333333", "opaque person_record"), + ("person_record_reference", "person_record:00000000-0000-0000-0000-000000000000", "opaque person_record"), + ("person_record_reference", "person_record:ffffffff-ffff-ffff-ffff-ffffffffffff", "opaque person_record"), + ("person_record_reference", "person_record:33333333-3333-4333-8333-33333333333A", "opaque person_record"), + ("person_record_reference", "person_record:" + "x" * 200, "opaque person_record"), + ("current_compensation_snapshot_digest", "A" * 64, "lowercase SHA-256"), + ("current_compensation_snapshot_digest", "a" * 63, "lowercase SHA-256"), + ("current_compensation_snapshot_digest", 64, "lowercase SHA-256"), + ("purpose_code", "general_hr", "purpose_code must remain"), + ("reason_code", "medical_leave_adjustment", "approved non-sensitive compensation-review category"), + ("proposed_effective_on", "2026-10-01", "must be a date"), + ("proposed_effective_on", datetime(2026, 10, 1, tzinfo=timezone.utc), "must be a date"), + ("generated_at", datetime(2026, 8, 19, 6, 12), "generated_at must be timezone-aware"), + ("generated_at", "2026-08-19T06:12:00Z", "generated_at must be timezone-aware"), + ("evidence_version", True, "evidence_version must be an integer"), + ("evidence_version", 0, "evidence_version must be an integer"), + ("evidence_version", 2_147_483_648, "evidence_version must be an integer"), + ], +) +def test_invalid_core_inputs_fail_closed(field_name: str, value: object, message: str) -> None: + """Malformed identity, evidence, purpose, time, and version inputs are rejected.""" + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_compensation_change_review_packet(**kwargs) + + +def test_every_reference_and_digest_is_validated() -> None: + """Each trust-bearing reference and digest field is individually bound to its namespace.""" + packet = build_valid() + reference_fields = { + "compensation_review_reference": "compensation_change_review", + "person_record_reference": "person_record", + "employment_record_reference": "employment_record", + "active_assignment_snapshot_reference": "active_assignment_snapshot", + "current_compensation_snapshot_reference": "compensation_snapshot", + "proposed_compensation_plan_reference": "compensation_plan", + "compensation_policy_reference": "compensation_policy", + "pay_equity_review_reference": "pay_equity_review", + "budget_authorization_reference": "budget_authorization", + "payroll_handoff_plan_reference": "payroll_handoff_plan", + "requester_reference": "actor", + "reviewer_reference": "actor", + } + digest_fields = [name for name in valid_kwargs() if name.endswith("_digest")] + + for field_name, prefix in reference_fields.items(): + with pytest.raises(ValueError, match=re.escape(f"opaque {prefix}: reference")): + replace(packet, **{field_name: f"wrong:11111111-1111-4111-8111-111111111111"}) + for field_name in digest_fields: + with pytest.raises(ValueError, match="lowercase SHA-256"): + replace(packet, **{field_name: "z" * 64}) + + +def test_same_opaque_actor_reference_is_rejected_early() -> None: + """Exact requester/reviewer reuse is denied before authoritative identity re-resolution.""" + kwargs = valid_kwargs() + kwargs["reviewer_reference"] = kwargs["requester_reference"] + with pytest.raises(ValueError, match="different actor references"): + build_compensation_change_review_packet(**kwargs) + + +def test_fractional_and_offset_timestamps_preserve_distinct_instants() -> None: + """Canonical evidence preserves microseconds while normalizing equivalent offsets to UTC.""" + packet = build_valid() + later = replace(packet, generated_at=packet.generated_at.replace(microsecond=456790)) + offset = replace( + packet, + generated_at=datetime(2026, 8, 19, 15, 12, 13, 456789, tzinfo=timezone(timedelta(hours=9))), + ) + + assert packet.sha256_digest() != later.sha256_digest() + assert packet.canonical_json() == offset.canonical_json() + + +class UnknownOffset(tzinfo): + """Timezone fixture whose UTC offset is intentionally unknown.""" + + def utcoffset(self, dt: datetime | None) -> None: + """Return no offset so the packet must reject the timestamp.""" + return None + + def dst(self, dt: datetime | None) -> None: + """Return no daylight-saving offset for the invalid fixture.""" + return None + + +def test_timezone_with_unknown_offset_is_rejected() -> None: + """A tzinfo object is insufficient when it cannot establish an actual UTC offset.""" + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime(2026, 8, 19, 6, 12, tzinfo=UnknownOffset()) + with pytest.raises(ValueError, match="generated_at must be timezone-aware"): + build_compensation_change_review_packet(**kwargs) From 5ad430a4fd4fafea4413ae242843d990ed186188 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:46:27 -0700 Subject: [PATCH 11/63] test: reject UUIDv1 compensation trust references --- .../tests/test_reference_privacy.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_reference_privacy.py diff --git a/packages/compensation-change-review/tests/test_reference_privacy.py b/packages/compensation-change-review/tests/test_reference_privacy.py new file mode 100644 index 000000000..f1f25792c --- /dev/null +++ b/packages/compensation-change-review/tests/test_reference_privacy.py @@ -0,0 +1,73 @@ +"""Privacy regressions for opaque compensation-review trust references.""" +from dataclasses import replace +from datetime import date, datetime, timezone + +import pytest + +from orgmetra_compensation_change_review import build_compensation_change_review_packet + +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + + +def valid_kwargs() -> dict[str, object]: + """Return one complete packet whose trust references use opaque UUIDv4 suffixes.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +@pytest.mark.parametrize( + ("field_name", "prefix"), + [ + ("compensation_review_reference", "compensation_change_review"), + ("person_record_reference", "person_record"), + ("employment_record_reference", "employment_record"), + ("active_assignment_snapshot_reference", "active_assignment_snapshot"), + ("current_compensation_snapshot_reference", "compensation_snapshot"), + ("proposed_compensation_plan_reference", "compensation_plan"), + ("compensation_policy_reference", "compensation_policy"), + ("pay_equity_review_reference", "pay_equity_review"), + ("budget_authorization_reference", "budget_authorization"), + ("payroll_handoff_plan_reference", "payroll_handoff_plan"), + ("requester_reference", "actor"), + ("reviewer_reference", "actor"), + ], +) +def test_uuid1_trust_reference_is_rejected_by_builder_and_replace( + field_name: str, + prefix: str, +) -> None: + """UUIDv1 timestamp/node metadata must never enter an opaque trust-reference field.""" + value = f"{prefix}:{UUID1_ID}" + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=field_name): + build_compensation_change_review_packet(**kwargs) + + packet = build_compensation_change_review_packet(**valid_kwargs()) + with pytest.raises(ValueError, match=field_name): + replace(packet, **{field_name: value}) From f6eb5efc327ea8bd790dda81c2fab1718fbd7888 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:47:29 -0700 Subject: [PATCH 12/63] fix: require UUIDv4 compensation trust references --- .../src/orgmetra_compensation_change_review/packet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index 9b9d4d4b7..77678b6b9 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -60,7 +60,7 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require the expected namespace plus a canonical operational UUID suffix.""" + """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" message = f"{field_name} must be an opaque {prefix}: reference" if ( not isinstance(value, str) @@ -74,7 +74,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: parsed = UUID(suffix) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(message) from exc - if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): raise ValueError(message) From 1820a252cbbe23b1a15d28ecd4c23784160a9284 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:48:10 -0700 Subject: [PATCH 13/63] docs: define UUIDv4 compensation reference privacy --- packages/compensation-change-review/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md index 93a8623d6..2651d118b 100644 --- a/packages/compensation-change-review/README.md +++ b/packages/compensation-change-review/README.md @@ -4,7 +4,7 @@ ## What the packet carries -The packet correlates one tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. Trust-bearing artifacts use canonical UUID-backed opaque references; evidence artifacts also carry independent lowercase SHA-256 digests. +The packet correlates one tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. Every namespaced trust-bearing artifact uses a canonical non-sentinel UUIDv4-backed opaque reference; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter an otherwise opaque field. Evidence artifacts also carry independent lowercase SHA-256 digests. The envelope intentionally **does not carry salary, wage, bonus, benefit, equity, protected-attribute, credential, or free-form case/model values**. Opaque Person/Employment and evidence references still create sensitive personal-data correlation, so the packet explicitly reports `contains_personal_data = true` rather than claiming anonymity. @@ -18,7 +18,7 @@ A valid packet is not an approval. It remains: - `mutation_state = not_authorized_to_apply`; and - `external_execution_state = not_authorized_to_execute`. -Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. +Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. UUIDv4 syntax is only an opacity constraint; it does not prove tenant ownership, actor identity, worker scope, or policy applicability. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. ## Example From 3242733c18ba8ed1aee0da412ae1a2acf4390d8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:48:29 -0700 Subject: [PATCH 14/63] docs: record UUIDv4 compensation trust-reference decision --- docs/adr/0022-governed-compensation-change-review.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/adr/0022-governed-compensation-change-review.md b/docs/adr/0022-governed-compensation-change-review.md index f210e0ee2..78cd120fa 100644 --- a/docs/adr/0022-governed-compensation-change-review.md +++ b/docs/adr/0022-governed-compensation-change-review.md @@ -7,7 +7,7 @@ Compensation changes are high-impact employment actions. A useful enterprise review boundary must correlate the proposed change to authoritative worker scope, the exact current/proposed compensation artifacts, the governing policy, pay-equity review, budget authorization, and payroll handoff without copying salary, wage, bonus, benefit, equity, protected-attribute, or free-form case values into portable evidence. -Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. +Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. UUID syntax is itself part of the privacy boundary: UUIDv1 can carry timestamp/node-derived correlation metadata despite appearing opaque. Current primary-source context is recorded in `docs/doctoring/compensation-change-review-references.md`. ISO 30414:2025 provides current human-capital reporting/disclosure context; U.S. EEOC compensation guidance demonstrates why compensation governance must not turn protected-attribute review into uncontrolled evidence copying; U.S. Department of Labor FLSA recordkeeping guidance demonstrates the need for accountable wage/time records. These are governance inputs only, not certification or universal legal-compliance claims. @@ -15,7 +15,7 @@ Current primary-source context is recorded in `docs/doctoring/compensation-chang Orgmetra will expose a value-minimized `CompensationChangeReviewPacket` before any authoritative compensation-related HRIS mutation or payroll execution. -The packet binds one canonical tenant to UUID-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. +The packet binds one canonical tenant to canonical non-sentinel UUIDv4-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. UUIDv1 and every other UUID version fail closed for these namespaced trust references so timestamp/node-derived correlation metadata cannot enter the envelope through identifiers presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. The packet deliberately excludes compensation values, protected-attribute values, free-form case narrative, credentials, and free-form model output. It explicitly acknowledges remaining personal-data correlation with `contains_personal_data = true`. @@ -28,7 +28,7 @@ Direct construction and replacement fail closed unless the packet remains: - `mutation_state = not_authorized_to_apply`; and - `external_execution_state = not_authorized_to_execute`. -Requester and reviewer opaque references must differ, but authoritative separation of duties requires both identities to be re-resolved inside the packet tenant immediately before approval. The host must also re-resolve every packet reference, prove Person-to-Employment and active Assignment/Job/Position scope, and verify the current compensation snapshot, proposed plan, exact compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance without copying compensation or protected-attribute values into the packet. +Requester and reviewer opaque references must differ, but authoritative separation of duties requires both identities to be re-resolved inside the packet tenant immediately before approval. The host must also re-resolve every packet reference, prove Person-to-Employment and active Assignment/Job/Position scope, and verify the current compensation snapshot, proposed plan, exact compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance without copying compensation or protected-attribute values into the packet. UUIDv4 syntax constrains opacity only; it does not prove tenant membership, actor identity, worker scope, policy applicability, or substantive correctness. Any authorized HRIS change must use the authoritative Orgmetra People boundary with its own purpose-bound authorization, idempotency, bitemporal persistence, and immutable audit/outbox evidence. Payroll execution remains behind the payroll owner's published contract. This package performs no foreign mutation and no direct cross-service application-table SQL. @@ -38,16 +38,18 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w - A review envelope cannot masquerade as compensation approval, an applied HRIS change, or completed payroll execution. - Buyers can correlate exact policy/equity/budget/proposed-plan evidence while minimizing duplicated pay and protected-attribute values. +- UUIDv1 timestamp/node correlation cannot enter namespaced trust references. - Changes to governed evidence or `evidence_version` change the canonical packet digest. - Cross-tenant, wrong-worker, stale-policy, actor-separation, and effective-date questions remain explicit authoritative-resolution obligations instead of being inferred from reference syntax. ### Trade-offs - The packet is not anonymous; opaque worker/evidence correlations still require purpose-bound access, retention, export, and audit controls. +- UUIDv4 syntax is not authorization, ownership, or relationship evidence. - A pay-equity evidence reference/digest proves which artifact was reviewed, not that compensation is fair, nondiscriminatory, or legally sufficient. - The packet does not calculate compensation, payroll, taxes, protected-class statistics, pay-equity findings, or legal conclusions. - Hosts must perform authoritative scope and actor resolution at approval time. ## Verification -The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict UUID-backed opaque references; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. +The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict canonical non-sentinel UUIDv4-backed opaque references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. From 9fa44c95ba5c35772afa091c9a36c7e8da2bd7df Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:48:36 -0700 Subject: [PATCH 15/63] docs: record compensation reference privacy hardening --- packages/compensation-change-review/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 29951c6a4..6663ce115 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -6,4 +6,4 @@ - `CompensationChangeReviewPacket`, a value-minimized pre-mutation evidence contract that binds authoritative worker scope, reviewed compensation-plan/policy evidence, pay-equity review, budget authorization, and payroll handoff provenance without copying compensation or protected-attribute values. - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. -- Deterministic canonical JSON/SHA-256 evidence, redacted representations, strict UUID-backed opaque references, and exact 100% owned statement/branch coverage regressions. +- Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4-backed opaque trust references that reject UUIDv1 timestamp/node correlation metadata, and exact 100% owned statement/branch coverage regressions. From a83a8b00d878ab810ffc614d5349e15b5fd2f90a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:48:46 -0700 Subject: [PATCH 16/63] docs: trace UUIDv4 compensation reference regression --- docs/traceability/compensation-change-review.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 1ea13989b..699b25ae5 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -5,7 +5,7 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Buyer or governance need | Contract | Executable evidence | | --- | --- | --- | | Correlate a proposed pay change without copying pay amounts | Opaque Person/Employment/current/proposed compensation references + independent digests; compensation/protected values excluded | `test_builds_deterministic_value_minimized_packet`, `test_every_reference_and_digest_is_validated` | -| Prevent reference fields from becoming a PII/value smuggling channel | Every trust-bearing reference is the expected namespace plus canonical non-sentinel UUID | `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated` | +| Prevent reference fields from becoming a PII/value/correlation smuggling channel | Every namespaced trust-bearing reference is the expected namespace plus canonical non-sentinel UUIDv4; UUIDv1 and other UUID versions fail closed | `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated`, `test_uuid1_trust_reference_is_rejected_by_builder_and_replace` | | Preserve accountable separation of duties | Requester/reviewer references differ; canonical next action requires tenant-scoped identity re-resolution and distinct resolved actors | `test_same_opaque_actor_reference_is_rejected_early`, `test_next_action_requires_identity_scope_and_evidence_before_approval` | | Prevent review evidence from authorizing a high-impact action | Human-only review, unresolved authoritative scope, no HRIS mutation, no external execution are immutable | `test_direct_constructor_and_replace_fail_closed` | | Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | @@ -18,4 +18,4 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt ## Ownership boundary -This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. +This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. UUIDv4 is an opacity/privacy constraint only and does not establish tenant membership, actor identity, worker relationship, or policy applicability. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. From f551fd3e8f89c81d38b044a9b80abd43ee71293c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:10:57 -0700 Subject: [PATCH 17/63] test: reject UUIDv1 tenant identity in compensation review --- .../tests/test_tenant_uuid_privacy.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_tenant_uuid_privacy.py diff --git a/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py new file mode 100644 index 000000000..5bc613333 --- /dev/null +++ b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py @@ -0,0 +1,44 @@ +"""Tenant identity privacy regressions for compensation-change review evidence.""" + +from datetime import date, datetime, timezone + +import pytest + +from orgmetra_compensation_change_review import build_compensation_change_review_packet + + +def _build_with_tenant(tenant_record_id: str): + """Build a valid value-minimized compensation packet around one tenant identity.""" + return build_compensation_change_review_packet( + tenant_record_id=tenant_record_id, + compensation_review_reference="compensation_change_review:22222222-2222-4222-8222-222222222222", + person_record_reference="person_record:33333333-3333-4333-8333-333333333333", + employment_record_reference="employment_record:44444444-4444-4444-8444-444444444444", + active_assignment_snapshot_reference="active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + active_assignment_snapshot_digest="a" * 64, + current_compensation_snapshot_reference="compensation_snapshot:66666666-6666-4666-8666-666666666666", + current_compensation_snapshot_digest="b" * 64, + proposed_compensation_plan_reference="compensation_plan:77777777-7777-4777-8777-777777777777", + proposed_compensation_plan_digest="c" * 64, + compensation_policy_reference="compensation_policy:88888888-8888-4888-8888-888888888888", + compensation_policy_digest="d" * 64, + pay_equity_review_reference="pay_equity_review:99999999-9999-4999-8999-999999999999", + pay_equity_review_digest="e" * 64, + budget_authorization_reference="budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + budget_authorization_digest="f" * 64, + payroll_handoff_plan_reference="payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + payroll_handoff_plan_digest="1" * 64, + requester_reference="actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + reviewer_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + purpose_code="compensation_change_review", + reason_code="annual_compensation_review", + proposed_effective_on=date(2026, 10, 1), + generated_at=datetime(2026, 8, 20, 8, 15, tzinfo=timezone.utc), + evidence_version=1, + ) + + +def test_rejects_uuid1_tenant_identity() -> None: + """Reject timestamp/node-correlating UUIDv1 from opaque tenant identity.""" + with pytest.raises(ValueError, match="tenant_record_id"): + _build_with_tenant("6ba7b810-9dad-11d1-80b4-00c04fd430c8") From 79ab792f430635d6a22240d8e05be0fe22943c58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:11:43 -0700 Subject: [PATCH 18/63] fix: require opaque UUIDv4 tenant identity --- .../src/orgmetra_compensation_change_review/packet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index 77678b6b9..aed794461 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -50,13 +50,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical non-sentinel UUID text for a governance identity.""" + """Require canonical UUIDv4 text for an opaque governance identity.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUID") + if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUIDv4") def _validate_reference(value: str, prefix: str, field_name: str) -> None: From cf713cdea8e10b63e2ee233bc2b9c6134a436e61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:12:13 -0700 Subject: [PATCH 19/63] docs: bind compensation tenant identity to UUIDv4 --- packages/compensation-change-review/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md index 2651d118b..71b7f94b5 100644 --- a/packages/compensation-change-review/README.md +++ b/packages/compensation-change-review/README.md @@ -4,7 +4,7 @@ ## What the packet carries -The packet correlates one tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. Every namespaced trust-bearing artifact uses a canonical non-sentinel UUIDv4-backed opaque reference; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter an otherwise opaque field. Evidence artifacts also carry independent lowercase SHA-256 digests. +The packet correlates one canonical non-sentinel UUIDv4 tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. The tenant identity and every namespaced trust-bearing artifact use canonical non-sentinel UUIDv4 identity; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter values presented as opaque governance identity. Evidence artifacts also carry independent lowercase SHA-256 digests. The envelope intentionally **does not carry salary, wage, bonus, benefit, equity, protected-attribute, credential, or free-form case/model values**. Opaque Person/Employment and evidence references still create sensitive personal-data correlation, so the packet explicitly reports `contains_personal_data = true` rather than claiming anonymity. From 24377283e041180dccbd2a42dd51f4e8a5bc799c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:12:22 -0700 Subject: [PATCH 20/63] docs: record compensation tenant UUIDv4 privacy repair --- packages/compensation-change-review/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 6663ce115..67882ec7c 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -6,4 +6,4 @@ - `CompensationChangeReviewPacket`, a value-minimized pre-mutation evidence contract that binds authoritative worker scope, reviewed compensation-plan/policy evidence, pay-equity review, budget authorization, and payroll handoff provenance without copying compensation or protected-attribute values. - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. -- Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4-backed opaque trust references that reject UUIDv1 timestamp/node correlation metadata, and exact 100% owned statement/branch coverage regressions. +- Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identity for `tenant_record_id` and namespaced opaque trust references that reject UUIDv1 timestamp/node correlation metadata, and exact 100% owned statement/branch coverage regressions. From ccfcde58d776eb163cbfaf72824679eea91c3fda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:12:32 -0700 Subject: [PATCH 21/63] docs: trace compensation tenant UUIDv4 regression --- docs/traceability/compensation-change-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 699b25ae5..bc4b3c438 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -5,7 +5,7 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Buyer or governance need | Contract | Executable evidence | | --- | --- | --- | | Correlate a proposed pay change without copying pay amounts | Opaque Person/Employment/current/proposed compensation references + independent digests; compensation/protected values excluded | `test_builds_deterministic_value_minimized_packet`, `test_every_reference_and_digest_is_validated` | -| Prevent reference fields from becoming a PII/value/correlation smuggling channel | Every namespaced trust-bearing reference is the expected namespace plus canonical non-sentinel UUIDv4; UUIDv1 and other UUID versions fail closed | `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated`, `test_uuid1_trust_reference_is_rejected_by_builder_and_replace` | +| Prevent tenant/reference identity from becoming a correlation smuggling channel | `tenant_record_id` and every namespaced trust-bearing reference require canonical non-sentinel UUIDv4 identity; UUIDv1 and other UUID versions fail closed | `test_rejects_uuid1_tenant_identity`, `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated`, `test_uuid1_trust_reference_is_rejected_by_builder_and_replace` | | Preserve accountable separation of duties | Requester/reviewer references differ; canonical next action requires tenant-scoped identity re-resolution and distinct resolved actors | `test_same_opaque_actor_reference_is_rejected_early`, `test_next_action_requires_identity_scope_and_evidence_before_approval` | | Prevent review evidence from authorizing a high-impact action | Human-only review, unresolved authoritative scope, no HRIS mutation, no external execution are immutable | `test_direct_constructor_and_replace_fail_closed` | | Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | From e57369fefda046acb2bf2df108316c1b303f99af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 01:12:50 -0700 Subject: [PATCH 22/63] docs: make compensation tenant UUIDv4 part of privacy decision --- docs/adr/0022-governed-compensation-change-review.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/adr/0022-governed-compensation-change-review.md b/docs/adr/0022-governed-compensation-change-review.md index 78cd120fa..932167e27 100644 --- a/docs/adr/0022-governed-compensation-change-review.md +++ b/docs/adr/0022-governed-compensation-change-review.md @@ -7,7 +7,7 @@ Compensation changes are high-impact employment actions. A useful enterprise review boundary must correlate the proposed change to authoritative worker scope, the exact current/proposed compensation artifacts, the governing policy, pay-equity review, budget authorization, and payroll handoff without copying salary, wage, bonus, benefit, equity, protected-attribute, or free-form case values into portable evidence. -Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. UUID syntax is itself part of the privacy boundary: UUIDv1 can carry timestamp/node-derived correlation metadata despite appearing opaque. +Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. UUID syntax is itself part of the privacy boundary: UUIDv1 can carry timestamp/node-derived correlation metadata despite appearing opaque, including when used as the top-level tenant identity. Current primary-source context is recorded in `docs/doctoring/compensation-change-review-references.md`. ISO 30414:2025 provides current human-capital reporting/disclosure context; U.S. EEOC compensation guidance demonstrates why compensation governance must not turn protected-attribute review into uncontrolled evidence copying; U.S. Department of Labor FLSA recordkeeping guidance demonstrates the need for accountable wage/time records. These are governance inputs only, not certification or universal legal-compliance claims. @@ -15,7 +15,7 @@ Current primary-source context is recorded in `docs/doctoring/compensation-chang Orgmetra will expose a value-minimized `CompensationChangeReviewPacket` before any authoritative compensation-related HRIS mutation or payroll execution. -The packet binds one canonical tenant to canonical non-sentinel UUIDv4-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. UUIDv1 and every other UUID version fail closed for these namespaced trust references so timestamp/node-derived correlation metadata cannot enter the envelope through identifiers presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. +The packet binds one canonical non-sentinel UUIDv4 tenant to canonical non-sentinel UUIDv4-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. UUIDv1 and every other UUID version fail closed for tenant identity and namespaced trust references so timestamp/node-derived correlation metadata cannot enter the envelope through identifiers presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. The packet deliberately excludes compensation values, protected-attribute values, free-form case narrative, credentials, and free-form model output. It explicitly acknowledges remaining personal-data correlation with `contains_personal_data = true`. @@ -38,7 +38,7 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w - A review envelope cannot masquerade as compensation approval, an applied HRIS change, or completed payroll execution. - Buyers can correlate exact policy/equity/budget/proposed-plan evidence while minimizing duplicated pay and protected-attribute values. -- UUIDv1 timestamp/node correlation cannot enter namespaced trust references. +- UUIDv1 timestamp/node correlation cannot enter tenant identity or namespaced trust references. - Changes to governed evidence or `evidence_version` change the canonical packet digest. - Cross-tenant, wrong-worker, stale-policy, actor-separation, and effective-date questions remain explicit authoritative-resolution obligations instead of being inferred from reference syntax. @@ -52,4 +52,4 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w ## Verification -The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict canonical non-sentinel UUIDv4-backed opaque references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. +The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict canonical non-sentinel UUIDv4 tenant and namespaced opaque references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. From 73c778bc409a36417f73a38d9b517667383813fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:33:35 -0700 Subject: [PATCH 23/63] test: accept canonical HRIS tenant UUID contract --- .../tests/test_tenant_uuid_privacy.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py index 5bc613333..3538a770f 100644 --- a/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py +++ b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py @@ -1,4 +1,4 @@ -"""Tenant identity privacy regressions for compensation-change review evidence.""" +"""Tenant identity interoperability regressions for compensation-change review evidence.""" from datetime import date, datetime, timezone @@ -7,6 +7,9 @@ from orgmetra_compensation_change_review import build_compensation_change_review_packet +_AUTHORITATIVE_UUIDV7_TENANT = "10000000-0000-7000-8000-000000000001" + + def _build_with_tenant(tenant_record_id: str): """Build a valid value-minimized compensation packet around one tenant identity.""" return build_compensation_change_review_packet( @@ -38,7 +41,20 @@ def _build_with_tenant(tenant_record_id: str): ) -def test_rejects_uuid1_tenant_identity() -> None: - """Reject timestamp/node-correlating UUIDv1 from opaque tenant identity.""" +def test_accepts_authoritative_operational_uuidv7_tenant_identity() -> None: + """Accept the canonical UUIDv7 tenant form already accepted by protected HRIS core.""" + packet = _build_with_tenant(_AUTHORITATIVE_UUIDV7_TENANT) + assert packet.tenant_record_id == _AUTHORITATIVE_UUIDV7_TENANT + + +@pytest.mark.parametrize( + "tenant_record_id", + [ + "00000000-0000-0000-0000-000000000000", + "ffffffff-ffff-ffff-ffff-ffffffffffff", + ], +) +def test_rejects_reserved_sentinel_tenant_identity(tenant_record_id: str) -> None: + """Reject RFC 9562 Nil/Max sentinels while deferring UUID version policy to HRIS core.""" with pytest.raises(ValueError, match="tenant_record_id"): - _build_with_tenant("6ba7b810-9dad-11d1-80b4-00c04fd430c8") + _build_with_tenant(tenant_record_id) From 3551dfec2d5ec4c831a008975580aaffc63df7ec Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:35:50 -0700 Subject: [PATCH 24/63] fix: honor authoritative tenant UUID contract --- .../src/orgmetra_compensation_change_review/packet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index aed794461..480ec8541 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -50,13 +50,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical UUIDv4 text for an opaque governance identity.""" + """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUIDv4") + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") def _validate_reference(value: str, prefix: str, field_name: str) -> None: From 04ddc6540c844d4c3f3bd056f3a5cbf7b8e02000 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:37:59 -0700 Subject: [PATCH 25/63] docs: align compensation tenant identity boundary --- .../adr/0022-governed-compensation-change-review.md | 13 +++++++------ docs/traceability/compensation-change-review.md | 4 ++-- packages/compensation-change-review/CHANGELOG.md | 6 +++++- packages/compensation-change-review/README.md | 6 +++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/adr/0022-governed-compensation-change-review.md b/docs/adr/0022-governed-compensation-change-review.md index 932167e27..da7d53c68 100644 --- a/docs/adr/0022-governed-compensation-change-review.md +++ b/docs/adr/0022-governed-compensation-change-review.md @@ -7,7 +7,7 @@ Compensation changes are high-impact employment actions. A useful enterprise review boundary must correlate the proposed change to authoritative worker scope, the exact current/proposed compensation artifacts, the governing policy, pay-equity review, budget authorization, and payroll handoff without copying salary, wage, bonus, benefit, equity, protected-attribute, or free-form case values into portable evidence. -Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. UUID syntax is itself part of the privacy boundary: UUIDv1 can carry timestamp/node-derived correlation metadata despite appearing opaque, including when used as the top-level tenant identity. +Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. The authoritative HRIS already owns tenant identity semantics: protected Orgmetra core accepts canonical non-sentinel operational UUIDs, including UUIDv7. This leaf package therefore must not narrow `tenant_record_id` to UUIDv4. UUIDv4 remains appropriate for packet-owned opaque references where this package owns the correlation-privacy contract. Current primary-source context is recorded in `docs/doctoring/compensation-change-review-references.md`. ISO 30414:2025 provides current human-capital reporting/disclosure context; U.S. EEOC compensation guidance demonstrates why compensation governance must not turn protected-attribute review into uncontrolled evidence copying; U.S. Department of Labor FLSA recordkeeping guidance demonstrates the need for accountable wage/time records. These are governance inputs only, not certification or universal legal-compliance claims. @@ -15,7 +15,7 @@ Current primary-source context is recorded in `docs/doctoring/compensation-chang Orgmetra will expose a value-minimized `CompensationChangeReviewPacket` before any authoritative compensation-related HRIS mutation or payroll execution. -The packet binds one canonical non-sentinel UUIDv4 tenant to canonical non-sentinel UUIDv4-backed opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer. UUIDv1 and every other UUID version fail closed for tenant identity and namespaced trust references so timestamp/node-derived correlation metadata cannot enter the envelope through identifiers presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. +The packet binds one authoritative Orgmetra `tenant_record_id` that satisfies the protected-core canonical non-sentinel operational-UUID contract. Packet-owned opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer remain canonical non-sentinel UUIDv4-backed namespaced references. This preserves interoperability with authoritative UUIDv7 tenant identities while preventing timestamp/node-derived correlation metadata from entering packet-owned references presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. The packet deliberately excludes compensation values, protected-attribute values, free-form case narrative, credentials, and free-form model output. It explicitly acknowledges remaining personal-data correlation with `contains_personal_data = true`. @@ -28,7 +28,7 @@ Direct construction and replacement fail closed unless the packet remains: - `mutation_state = not_authorized_to_apply`; and - `external_execution_state = not_authorized_to_execute`. -Requester and reviewer opaque references must differ, but authoritative separation of duties requires both identities to be re-resolved inside the packet tenant immediately before approval. The host must also re-resolve every packet reference, prove Person-to-Employment and active Assignment/Job/Position scope, and verify the current compensation snapshot, proposed plan, exact compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance without copying compensation or protected-attribute values into the packet. UUIDv4 syntax constrains opacity only; it does not prove tenant membership, actor identity, worker scope, policy applicability, or substantive correctness. +Requester and reviewer opaque references must differ, but authoritative separation of duties requires both identities to be re-resolved inside the packet tenant immediately before approval. The host must also re-resolve every packet reference, prove Person-to-Employment and active Assignment/Job/Position scope, and verify the current compensation snapshot, proposed plan, exact compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance without copying compensation or protected-attribute values into the packet. Neither authoritative tenant UUID syntax nor packet-owned UUIDv4 syntax proves tenant membership, actor identity, worker scope, policy applicability, or substantive correctness. Any authorized HRIS change must use the authoritative Orgmetra People boundary with its own purpose-bound authorization, idempotency, bitemporal persistence, and immutable audit/outbox evidence. Payroll execution remains behind the payroll owner's published contract. This package performs no foreign mutation and no direct cross-service application-table SQL. @@ -38,18 +38,19 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w - A review envelope cannot masquerade as compensation approval, an applied HRIS change, or completed payroll execution. - Buyers can correlate exact policy/equity/budget/proposed-plan evidence while minimizing duplicated pay and protected-attribute values. -- UUIDv1 timestamp/node correlation cannot enter tenant identity or namespaced trust references. +- Authoritative UUIDv7 tenant identities remain interoperable instead of being rejected by a leaf-only UUIDv4 policy. +- UUIDv1 timestamp/node correlation remains excluded from packet-owned namespaced trust references. - Changes to governed evidence or `evidence_version` change the canonical packet digest. - Cross-tenant, wrong-worker, stale-policy, actor-separation, and effective-date questions remain explicit authoritative-resolution obligations instead of being inferred from reference syntax. ### Trade-offs - The packet is not anonymous; opaque worker/evidence correlations still require purpose-bound access, retention, export, and audit controls. -- UUIDv4 syntax is not authorization, ownership, or relationship evidence. +- UUID syntax is not authorization, ownership, or relationship evidence; authoritative tenant identity policy and packet-owned reference privacy are intentionally separate ownership boundaries. - A pay-equity evidence reference/digest proves which artifact was reviewed, not that compensation is fair, nondiscriminatory, or legally sufficient. - The packet does not calculate compensation, payroll, taxes, protected-class statistics, pay-equity findings, or legal conclusions. - Hosts must perform authoritative scope and actor resolution at approval time. ## Verification -The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; strict canonical non-sentinel UUIDv4 tenant and namespaced opaque references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. +The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; authoritative tenant interoperability with the protected-core canonical non-sentinel operational-UUID contract, including the canonical UUIDv7 tenant used by the core PostgreSQL regression and rejection of RFC 9562 Nil/Max sentinels; strict UUIDv4 packet-owned namespaced references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index bc4b3c438..7519ba0a8 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -5,7 +5,7 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Buyer or governance need | Contract | Executable evidence | | --- | --- | --- | | Correlate a proposed pay change without copying pay amounts | Opaque Person/Employment/current/proposed compensation references + independent digests; compensation/protected values excluded | `test_builds_deterministic_value_minimized_packet`, `test_every_reference_and_digest_is_validated` | -| Prevent tenant/reference identity from becoming a correlation smuggling channel | `tenant_record_id` and every namespaced trust-bearing reference require canonical non-sentinel UUIDv4 identity; UUIDv1 and other UUID versions fail closed | `test_rejects_uuid1_tenant_identity`, `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated`, `test_uuid1_trust_reference_is_rejected_by_builder_and_replace` | +| Preserve authoritative tenant identity while preventing leaf-owned reference correlation smuggling | `tenant_record_id` follows protected HRIS canonical non-sentinel operational UUID semantics; packet-owned namespaced trust references require canonical UUIDv4 | `test_accepts_authoritative_operational_uuidv7_tenant_identity`, `test_rejects_reserved_sentinel_tenant_identity`, `test_invalid_core_inputs_fail_closed`, `test_every_reference_and_digest_is_validated`, `test_uuid1_trust_reference_is_rejected_by_builder_and_replace` | | Preserve accountable separation of duties | Requester/reviewer references differ; canonical next action requires tenant-scoped identity re-resolution and distinct resolved actors | `test_same_opaque_actor_reference_is_rejected_early`, `test_next_action_requires_identity_scope_and_evidence_before_approval` | | Prevent review evidence from authorizing a high-impact action | Human-only review, unresolved authoritative scope, no HRIS mutation, no external execution are immutable | `test_direct_constructor_and_replace_fail_closed` | | Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | @@ -18,4 +18,4 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt ## Ownership boundary -This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. UUIDv4 is an opacity/privacy constraint only and does not establish tenant membership, actor identity, worker relationship, or policy applicability. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. +This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative tenant identity plus Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. UUIDv4 is an opacity/privacy constraint for packet-owned namespaced references only and does not establish tenant membership, actor identity, worker relationship, or policy applicability. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 67882ec7c..3500ee4bd 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -6,4 +6,8 @@ - `CompensationChangeReviewPacket`, a value-minimized pre-mutation evidence contract that binds authoritative worker scope, reviewed compensation-plan/policy evidence, pay-equity review, budget authorization, and payroll handoff provenance without copying compensation or protected-attribute values. - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. -- Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identity for `tenant_record_id` and namespaced opaque trust references that reject UUIDv1 timestamp/node correlation metadata, and exact 100% owned statement/branch coverage regressions. +- Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identities for packet-owned namespaced opaque trust references, and exact 100% owned statement/branch coverage regressions. + +### Changed + +- `tenant_record_id` now follows the authoritative Orgmetra canonical non-sentinel operational-UUID contract instead of imposing a duplicate UUIDv4-only leaf policy; the protected-core UUIDv7 tenant form is covered explicitly while RFC 9562 Nil/Max sentinels remain rejected. diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md index 71b7f94b5..3a3aad074 100644 --- a/packages/compensation-change-review/README.md +++ b/packages/compensation-change-review/README.md @@ -4,7 +4,7 @@ ## What the packet carries -The packet correlates one canonical non-sentinel UUIDv4 tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. The tenant identity and every namespaced trust-bearing artifact use canonical non-sentinel UUIDv4 identity; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter values presented as opaque governance identity. Evidence artifacts also carry independent lowercase SHA-256 digests. +The packet correlates one authoritative Orgmetra tenant, Person, Employment, active Assignment/Job/Position scope snapshot, current compensation snapshot, proposed compensation plan, exact compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, reviewer, business effective date, and evidence version. `tenant_record_id` follows the authoritative HRIS canonical non-sentinel operational-UUID contract, so valid tenant UUID versions accepted by protected Orgmetra core remain interoperable. Packet-owned namespaced trust-bearing references use canonical non-sentinel UUIDv4 identities so timestamp/node-derived correlation metadata cannot enter leaf-owned opaque references. Evidence artifacts also carry independent lowercase SHA-256 digests. The envelope intentionally **does not carry salary, wage, bonus, benefit, equity, protected-attribute, credential, or free-form case/model values**. Opaque Person/Employment and evidence references still create sensitive personal-data correlation, so the packet explicitly reports `contains_personal_data = true` rather than claiming anonymity. @@ -18,7 +18,7 @@ A valid packet is not an approval. It remains: - `mutation_state = not_authorized_to_apply`; and - `external_execution_state = not_authorized_to_execute`. -Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. UUIDv4 syntax is only an opacity constraint; it does not prove tenant ownership, actor identity, worker scope, or policy applicability. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. +Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. UUID syntax is not authorization or relationship evidence: the tenant UUID is governed by authoritative Orgmetra core, while UUIDv4 on packet-owned references is only an opacity/privacy constraint. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. ## Example @@ -27,7 +27,7 @@ from datetime import date, datetime, timezone from orgmetra_compensation_change_review import build_compensation_change_review_packet packet = build_compensation_change_review_packet( - tenant_record_id="11111111-1111-4111-8111-111111111111", + tenant_record_id="10000000-0000-7000-8000-000000000001", compensation_review_reference="compensation_change_review:22222222-2222-4222-8222-222222222222", person_record_reference="person_record:33333333-3333-4333-8333-333333333333", employment_record_reference="employment_record:44444444-4444-4444-8444-444444444444", From 76db3c38f20b3fa8462b58bb455349a4eec13fab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:11:25 -0700 Subject: [PATCH 26/63] test(compensation-review): reject recorded-time subclasses --- .../tests/test_temporal_evidence_integrity.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_temporal_evidence_integrity.py diff --git a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py new file mode 100644 index 000000000..c4f622a75 --- /dev/null +++ b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py @@ -0,0 +1,61 @@ +"""Regression coverage for compensation-review recorded-time evidence integrity.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone + +import pytest + +from orgmetra_compensation_change_review import build_compensation_change_review_packet + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical recorded-time evidence.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the hostile subclass alive across UTC normalization.""" + return self + + def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Return an instant different from the underlying review evidence.""" + return "2099-12-31T23:59:59+00:00" + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid compensation-change review packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 21, 4, 25, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence() -> None: + """Canonical audit evidence must not call caller-overridable datetime methods.""" + kwargs = valid_kwargs() + kwargs["generated_at"] = ForgedDateTime(2026, 8, 21, 4, 25, tzinfo=timezone.utc) + + with pytest.raises(ValueError, match="generated_at"): + build_compensation_change_review_packet(**kwargs) From 18716ae36a7c95b80284998d3f457034ab7e6582 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:12:06 -0700 Subject: [PATCH 27/63] fix(compensation-review): require exact recorded-time type --- .../src/orgmetra_compensation_change_review/packet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index 480ec8541..3591beacd 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -86,8 +86,8 @@ def _validate_digest(value: str, field_name: str) -> None: def _canonical_timestamp(value: datetime) -> str: """Render an aware instant as precision-preserving UTC RFC 3339 text.""" - if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: - raise ValueError("generated_at must be timezone-aware") + if type(value) is not datetime or value.tzinfo is None or value.utcoffset() is None: + raise ValueError("generated_at must be an exact timezone-aware datetime") return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") From 16fa40b67bdb98722fa1cfbd4e1bf38069e8e646 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:32:41 -0700 Subject: [PATCH 28/63] fix(compensation-review): align timestamp validation contract --- .../packet.py | 313 ++++++------------ 1 file changed, 95 insertions(+), 218 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index 3591beacd..e23ff4230 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -1,72 +1,57 @@ -"""Governed, value-minimized pre-mutation compensation-change review evidence. +"""Governed pre-mutation compensation-change review evidence.""" -The packet correlates one proposed compensation change to authoritative worker scope and -exact reviewed policy/evidence artifacts without copying pay amounts, protected-attribute -values, narrative case material, credentials, or model output into the review envelope. -Opaque worker and evidence references remain sensitive correlating metadata. Authoritative -scope resolution, human approval, HRIS mutation, and payroll execution remain outside this -package and must occur through their published owner boundaries. -""" from __future__ import annotations from dataclasses import dataclass from datetime import date, datetime, timezone -from hashlib import sha256 +import hashlib import json import re +from typing import Final from uuid import UUID -_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") -_REFERENCE_PATTERN = re.compile( - r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" -) -_PURPOSE_CODE = "compensation_change_review" -_ALLOWED_REASON_CODES = frozenset( +_DIGEST_PATTERN: Final = re.compile(r"[0-9a-f]{64}") +_CODE_PATTERN: Final = re.compile(r"[a-z][a-z0-9_]{0,63}") +_MAX_REFERENCE_LENGTH: Final = 160 +_SENTINEL_UUIDS: Final = {0, (1 << 128) - 1} +_ALLOWED_REASON_CODES: Final = frozenset( { "annual_compensation_review", - "promotion_compensation_review", - "market_adjustment_review", - "retention_adjustment_review", - "role_change_compensation_review", + "promotion_adjustment", + "market_adjustment", + "internal_equity_adjustment", + "approved_retention_adjustment", } ) -_DECISION_AUTHORITY = "human_review_only" -_REVIEW_STATE = "requires_human_review" -_SCOPE_VERIFICATION_STATE = "requires_authoritative_resolution" -_MUTATION_STATE = "not_authorized_to_apply" -_EXTERNAL_EXECUTION_STATE = "not_authorized_to_execute" -_NEXT_ACTION = ( - "Re-resolve every packet reference within tenant_record_id; specifically re-resolve " - "requester_reference and reviewer_reference within tenant_record_id and verify their " - "resolved actor identities are distinct, then prove the Person-to-Employment binding " - "and active Assignment/Job/Position scope represented by the snapshot, then verify the " - "current compensation snapshot, proposed compensation plan, exact compensation policy, " - "pay-equity review, budget authorization, proposed effective date, and payroll-handoff " - "provenance without copying compensation or protected-attribute values into this packet; " - "then record accountable human approval, apply any authorized HRIS change only through " - "the authoritative Orgmetra People boundary, and execute payroll only through its " - "published owner contract." +_NEXT_ACTION: Final = ( + "Re-resolve every packet reference in tenant_record_id, prove requester and reviewer " + "resolve to distinct authoritative actor identities, prove the Person-to-Employment and " + "active Assignment/Job/Position scope, then verify exact current/proposed compensation, " + "policy, pay-equity review, budget authorization, effective date, and payroll handoff " + "provenance before human approval or any People mutation." ) def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" + """Require a canonical non-sentinel operational UUID accepted by core HRIS.""" + if not isinstance(value, str): + raise ValueError(f"{field_name} must be canonical UUID text") try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + if str(parsed) != value or parsed.int in _SENTINEL_UUIDS: raise ValueError(f"{field_name} must be a canonical operational UUID") def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" + """Require a value-minimized canonical non-sentinel UUIDv4 namespaced reference.""" message = f"{field_name} must be an opaque {prefix}: reference" + namespace = f"{prefix}:" if ( not isinstance(value, str) - or len(value) > 160 - or not _REFERENCE_PATTERN.fullmatch(value) - or not value.startswith(f"{prefix}:") + or len(value) > _MAX_REFERENCE_LENGTH + or not value.startswith(namespace) ): raise ValueError(message) suffix = value.split(":", 1)[1] @@ -74,7 +59,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: parsed = UUID(suffix) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(message) from exc - if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + if str(parsed) != suffix or parsed.version != 4 or parsed.int in _SENTINEL_UUIDS: raise ValueError(message) @@ -87,7 +72,7 @@ def _validate_digest(value: str, field_name: str) -> None: def _canonical_timestamp(value: datetime) -> str: """Render an aware instant as precision-preserving UTC RFC 3339 text.""" if type(value) is not datetime or value.tzinfo is None or value.utcoffset() is None: - raise ValueError("generated_at must be an exact timezone-aware datetime") + raise ValueError("generated_at must be timezone-aware exact datetime") return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") @@ -103,9 +88,9 @@ def _validate_evidence_version(value: int) -> None: raise ValueError("evidence_version must be an integer from 1 through 2147483647") -@dataclass(frozen=True, slots=True, repr=False) +@dataclass(frozen=True, slots=True) class CompensationChangeReviewPacket: - """Immutable compensation-review evidence that cannot authorize mutation or payroll.""" + """Value-minimized evidence envelope for human compensation-change review.""" tenant_record_id: str compensation_review_reference: str @@ -127,129 +112,70 @@ class CompensationChangeReviewPacket: payroll_handoff_plan_digest: str requester_reference: str reviewer_reference: str + proposed_effective_on: date purpose_code: str reason_code: str - proposed_effective_on: date + evidence_version: int generated_at: datetime - evidence_version: int = 1 - contains_personal_data: bool = True - contains_compensation_values: bool = False - contains_protected_attribute_values: bool = False - contains_free_form_case_narrative: bool = False - contains_free_form_model_output: bool = False - human_confirmation_required: bool = True - decision_authority: str = _DECISION_AUTHORITY - review_state: str = _REVIEW_STATE - scope_verification_state: str = _SCOPE_VERIFICATION_STATE - mutation_state: str = _MUTATION_STATE - external_execution_state: str = _EXTERNAL_EXECUTION_STATE + review_state: str = "requires_human_review" + mutation_authorized: bool = False + scope_verification_state: str = "requires_authoritative_resolution" next_action: str = _NEXT_ACTION - def __repr__(self) -> str: - """Return a representation that never emits sensitive correlation evidence.""" - return "CompensationChangeReviewPacket()" - def __post_init__(self) -> None: - """Fail closed when direct construction drifts from the governed contract.""" + """Validate privacy, scope-correlation, timing, and high-impact review invariants.""" _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") - _validate_reference( - self.compensation_review_reference, - "compensation_change_review", - "compensation_review_reference", - ) - _validate_reference(self.person_record_reference, "person_record", "person_record_reference") - _validate_reference( - self.employment_record_reference, - "employment_record", - "employment_record_reference", - ) - _validate_reference( - self.active_assignment_snapshot_reference, - "active_assignment_snapshot", - "active_assignment_snapshot_reference", - ) - _validate_digest(self.active_assignment_snapshot_digest, "active_assignment_snapshot_digest") - _validate_reference( - self.current_compensation_snapshot_reference, - "compensation_snapshot", - "current_compensation_snapshot_reference", - ) - _validate_digest( - self.current_compensation_snapshot_digest, + reference_prefixes = { + "compensation_review_reference": "compensation_change_review", + "person_record_reference": "person_record", + "employment_record_reference": "employment_record", + "active_assignment_snapshot_reference": "active_assignment_snapshot", + "current_compensation_snapshot_reference": "compensation_snapshot", + "proposed_compensation_plan_reference": "compensation_plan", + "compensation_policy_reference": "compensation_policy", + "pay_equity_review_reference": "pay_equity_review", + "budget_authorization_reference": "budget_authorization", + "payroll_handoff_plan_reference": "payroll_handoff_plan", + "requester_reference": "actor", + "reviewer_reference": "actor", + } + for field_name, prefix in reference_prefixes.items(): + _validate_reference(getattr(self, field_name), prefix, field_name) + + if self.requester_reference == self.reviewer_reference: + raise ValueError("requester_reference and reviewer_reference must be different actor references") + + digest_fields = ( + "active_assignment_snapshot_digest", "current_compensation_snapshot_digest", - ) - _validate_reference( - self.proposed_compensation_plan_reference, - "compensation_plan", - "proposed_compensation_plan_reference", - ) - _validate_digest( - self.proposed_compensation_plan_digest, "proposed_compensation_plan_digest", + "compensation_policy_digest", + "pay_equity_review_digest", + "budget_authorization_digest", + "payroll_handoff_plan_digest", ) - _validate_reference( - self.compensation_policy_reference, - "compensation_policy", - "compensation_policy_reference", - ) - _validate_digest(self.compensation_policy_digest, "compensation_policy_digest") - _validate_reference( - self.pay_equity_review_reference, - "pay_equity_review", - "pay_equity_review_reference", - ) - _validate_digest(self.pay_equity_review_digest, "pay_equity_review_digest") - _validate_reference( - self.budget_authorization_reference, - "budget_authorization", - "budget_authorization_reference", - ) - _validate_digest(self.budget_authorization_digest, "budget_authorization_digest") - _validate_reference( - self.payroll_handoff_plan_reference, - "payroll_handoff_plan", - "payroll_handoff_plan_reference", - ) - _validate_digest(self.payroll_handoff_plan_digest, "payroll_handoff_plan_digest") - _validate_reference(self.requester_reference, "actor", "requester_reference") - _validate_reference(self.reviewer_reference, "actor", "reviewer_reference") - if self.requester_reference == self.reviewer_reference: - raise ValueError("requester and reviewer must be different actor references") - if self.purpose_code != _PURPOSE_CODE: + for field_name in digest_fields: + _validate_digest(getattr(self, field_name), field_name) + + _validate_business_date(self.proposed_effective_on, "proposed_effective_on") + if self.purpose_code != "compensation_change_review": raise ValueError("purpose_code must remain compensation_change_review") if self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must be an approved non-sensitive compensation-review category") - _validate_business_date(self.proposed_effective_on, "proposed_effective_on") - _canonical_timestamp(self.generated_at) _validate_evidence_version(self.evidence_version) - if self.contains_personal_data is not True: - raise ValueError("compensation review packet must acknowledge sensitive personal-data correlation") - if self.contains_compensation_values is not False: - raise ValueError("compensation review packet must not contain compensation values") - if self.contains_protected_attribute_values is not False: - raise ValueError("compensation review packet must not contain protected-attribute values") - if self.contains_free_form_case_narrative is not False: - raise ValueError("compensation review packet must not contain free-form case narrative") - if self.contains_free_form_model_output is not False: - raise ValueError("compensation review packet must not contain free-form model output") - if self.human_confirmation_required is not True: - raise ValueError("human confirmation is mandatory before a compensation change") - if self.decision_authority != _DECISION_AUTHORITY: - raise ValueError("decision_authority must remain human_review_only") - if self.review_state != _REVIEW_STATE: + _canonical_timestamp(self.generated_at) + if self.review_state != "requires_human_review": raise ValueError("review_state must remain requires_human_review") - if self.scope_verification_state != _SCOPE_VERIFICATION_STATE: + if type(self.mutation_authorized) is not bool or self.mutation_authorized: + raise ValueError("mutation_authorized must remain false") + if self.scope_verification_state != "requires_authoritative_resolution": raise ValueError("scope_verification_state must remain requires_authoritative_resolution") - if self.mutation_state != _MUTATION_STATE: - raise ValueError("mutation_state must remain not_authorized_to_apply") - if self.external_execution_state != _EXTERNAL_EXECUTION_STATE: - raise ValueError("external_execution_state must remain not_authorized_to_execute") if self.next_action != _NEXT_ACTION: - raise ValueError("next_action must remain the governed compensation-change instruction") + raise ValueError("next_action must remain the governed approval instruction") - def canonical_json(self) -> str: - """Return deterministic canonical JSON for immutable audit correlation.""" - payload = { + def to_dict(self) -> dict[str, object]: + """Return canonical value-minimized audit evidence without compensation values.""" + return { "active_assignment_snapshot_digest": self.active_assignment_snapshot_digest, "active_assignment_snapshot_reference": self.active_assignment_snapshot_reference, "budget_authorization_digest": self.budget_authorization_digest, @@ -257,20 +183,12 @@ def canonical_json(self) -> str: "compensation_policy_digest": self.compensation_policy_digest, "compensation_policy_reference": self.compensation_policy_reference, "compensation_review_reference": self.compensation_review_reference, - "contains_compensation_values": self.contains_compensation_values, - "contains_free_form_case_narrative": self.contains_free_form_case_narrative, - "contains_free_form_model_output": self.contains_free_form_model_output, - "contains_personal_data": self.contains_personal_data, - "contains_protected_attribute_values": self.contains_protected_attribute_values, "current_compensation_snapshot_digest": self.current_compensation_snapshot_digest, "current_compensation_snapshot_reference": self.current_compensation_snapshot_reference, - "decision_authority": self.decision_authority, "employment_record_reference": self.employment_record_reference, "evidence_version": self.evidence_version, - "external_execution_state": self.external_execution_state, "generated_at": _canonical_timestamp(self.generated_at), - "human_confirmation_required": self.human_confirmation_required, - "mutation_state": self.mutation_state, + "mutation_authorized": self.mutation_authorized, "next_action": self.next_action, "pay_equity_review_digest": self.pay_equity_review_digest, "pay_equity_review_reference": self.pay_equity_review_reference, @@ -288,66 +206,25 @@ def canonical_json(self) -> str: "scope_verification_state": self.scope_verification_state, "tenant_record_id": self.tenant_record_id, } - return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def canonical_json(self) -> str: + """Return deterministic JSON suitable for immutable audit correlation.""" + return json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True) def sha256_digest(self) -> str: - """Return SHA-256 over the exact canonical UTF-8 compensation-review packet.""" - return sha256(self.canonical_json().encode("utf-8")).hexdigest() + """Return SHA-256 over the exact canonical JSON evidence.""" + return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest() + + def __repr__(self) -> str: + """Return a correlation-free representation suitable for routine logs.""" + return ( + "CompensationChangeReviewPacket(" + f"review_state={self.review_state!r}, " + f"mutation_authorized={self.mutation_authorized!r}, " + f"scope_verification_state={self.scope_verification_state!r})" + ) -def build_compensation_change_review_packet( - *, - tenant_record_id: str, - compensation_review_reference: str, - person_record_reference: str, - employment_record_reference: str, - active_assignment_snapshot_reference: str, - active_assignment_snapshot_digest: str, - current_compensation_snapshot_reference: str, - current_compensation_snapshot_digest: str, - proposed_compensation_plan_reference: str, - proposed_compensation_plan_digest: str, - compensation_policy_reference: str, - compensation_policy_digest: str, - pay_equity_review_reference: str, - pay_equity_review_digest: str, - budget_authorization_reference: str, - budget_authorization_digest: str, - payroll_handoff_plan_reference: str, - payroll_handoff_plan_digest: str, - requester_reference: str, - reviewer_reference: str, - purpose_code: str, - reason_code: str, - proposed_effective_on: date, - generated_at: datetime, - evidence_version: int = 1, -) -> CompensationChangeReviewPacket: - """Build a value-minimized compensation packet pending authoritative human approval.""" - return CompensationChangeReviewPacket( - tenant_record_id=tenant_record_id, - compensation_review_reference=compensation_review_reference, - person_record_reference=person_record_reference, - employment_record_reference=employment_record_reference, - active_assignment_snapshot_reference=active_assignment_snapshot_reference, - active_assignment_snapshot_digest=active_assignment_snapshot_digest, - current_compensation_snapshot_reference=current_compensation_snapshot_reference, - current_compensation_snapshot_digest=current_compensation_snapshot_digest, - proposed_compensation_plan_reference=proposed_compensation_plan_reference, - proposed_compensation_plan_digest=proposed_compensation_plan_digest, - compensation_policy_reference=compensation_policy_reference, - compensation_policy_digest=compensation_policy_digest, - pay_equity_review_reference=pay_equity_review_reference, - pay_equity_review_digest=pay_equity_review_digest, - budget_authorization_reference=budget_authorization_reference, - budget_authorization_digest=budget_authorization_digest, - payroll_handoff_plan_reference=payroll_handoff_plan_reference, - payroll_handoff_plan_digest=payroll_handoff_plan_digest, - requester_reference=requester_reference, - reviewer_reference=reviewer_reference, - purpose_code=purpose_code, - reason_code=reason_code, - proposed_effective_on=proposed_effective_on, - generated_at=generated_at, - evidence_version=evidence_version, - ) +def build_compensation_change_review_packet(**values: object) -> CompensationChangeReviewPacket: + """Build a validated compensation-change review packet from explicit governed fields.""" + return CompensationChangeReviewPacket(**values) # type: ignore[arg-type] From a3294b038cf4386367a7cf92f2a6da7fa0ecf9b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:34:18 -0700 Subject: [PATCH 29/63] fix(compensation-review): restore packet and align timestamp message --- .../packet.py | 311 ++++++++++++------ 1 file changed, 217 insertions(+), 94 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index e23ff4230..bd56bed7f 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -1,57 +1,72 @@ -"""Governed pre-mutation compensation-change review evidence.""" +"""Governed, value-minimized pre-mutation compensation-change review evidence. +The packet correlates one proposed compensation change to authoritative worker scope and +exact reviewed policy/evidence artifacts without copying pay amounts, protected-attribute +values, narrative case material, credentials, or model output into the review envelope. +Opaque worker and evidence references remain sensitive correlating metadata. Authoritative +scope resolution, human approval, HRIS mutation, and payroll execution remain outside this +package and must occur through their published owner boundaries. +""" from __future__ import annotations from dataclasses import dataclass from datetime import date, datetime, timezone -import hashlib +from hashlib import sha256 import json import re -from typing import Final from uuid import UUID -_DIGEST_PATTERN: Final = re.compile(r"[0-9a-f]{64}") -_CODE_PATTERN: Final = re.compile(r"[a-z][a-z0-9_]{0,63}") -_MAX_REFERENCE_LENGTH: Final = 160 -_SENTINEL_UUIDS: Final = {0, (1 << 128) - 1} -_ALLOWED_REASON_CODES: Final = frozenset( +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) +_PURPOSE_CODE = "compensation_change_review" +_ALLOWED_REASON_CODES = frozenset( { "annual_compensation_review", - "promotion_adjustment", - "market_adjustment", - "internal_equity_adjustment", - "approved_retention_adjustment", + "promotion_compensation_review", + "market_adjustment_review", + "retention_adjustment_review", + "role_change_compensation_review", } ) -_NEXT_ACTION: Final = ( - "Re-resolve every packet reference in tenant_record_id, prove requester and reviewer " - "resolve to distinct authoritative actor identities, prove the Person-to-Employment and " - "active Assignment/Job/Position scope, then verify exact current/proposed compensation, " - "policy, pay-equity review, budget authorization, effective date, and payroll handoff " - "provenance before human approval or any People mutation." +_DECISION_AUTHORITY = "human_review_only" +_REVIEW_STATE = "requires_human_review" +_SCOPE_VERIFICATION_STATE = "requires_authoritative_resolution" +_MUTATION_STATE = "not_authorized_to_apply" +_EXTERNAL_EXECUTION_STATE = "not_authorized_to_execute" +_NEXT_ACTION = ( + "Re-resolve every packet reference within tenant_record_id; specifically re-resolve " + "requester_reference and reviewer_reference within tenant_record_id and verify their " + "resolved actor identities are distinct, then prove the Person-to-Employment binding " + "and active Assignment/Job/Position scope represented by the snapshot, then verify the " + "current compensation snapshot, proposed compensation plan, exact compensation policy, " + "pay-equity review, budget authorization, proposed effective date, and payroll-handoff " + "provenance without copying compensation or protected-attribute values into this packet; " + "then record accountable human approval, apply any authorized HRIS change only through " + "the authoritative Orgmetra People boundary, and execute payroll only through its " + "published owner contract." ) def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require a canonical non-sentinel operational UUID accepted by core HRIS.""" - if not isinstance(value, str): - raise ValueError(f"{field_name} must be canonical UUID text") + """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.int in _SENTINEL_UUIDS: + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): raise ValueError(f"{field_name} must be a canonical operational UUID") def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require a value-minimized canonical non-sentinel UUIDv4 namespaced reference.""" + """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" message = f"{field_name} must be an opaque {prefix}: reference" - namespace = f"{prefix}:" if ( not isinstance(value, str) - or len(value) > _MAX_REFERENCE_LENGTH - or not value.startswith(namespace) + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") ): raise ValueError(message) suffix = value.split(":", 1)[1] @@ -59,7 +74,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: parsed = UUID(suffix) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(message) from exc - if str(parsed) != suffix or parsed.version != 4 or parsed.int in _SENTINEL_UUIDS: + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): raise ValueError(message) @@ -88,9 +103,9 @@ def _validate_evidence_version(value: int) -> None: raise ValueError("evidence_version must be an integer from 1 through 2147483647") -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True, slots=True, repr=False) class CompensationChangeReviewPacket: - """Value-minimized evidence envelope for human compensation-change review.""" + """Immutable compensation-review evidence that cannot authorize mutation or payroll.""" tenant_record_id: str compensation_review_reference: str @@ -112,70 +127,129 @@ class CompensationChangeReviewPacket: payroll_handoff_plan_digest: str requester_reference: str reviewer_reference: str - proposed_effective_on: date purpose_code: str reason_code: str - evidence_version: int + proposed_effective_on: date generated_at: datetime - review_state: str = "requires_human_review" - mutation_authorized: bool = False - scope_verification_state: str = "requires_authoritative_resolution" + evidence_version: int = 1 + contains_personal_data: bool = True + contains_compensation_values: bool = False + contains_protected_attribute_values: bool = False + contains_free_form_case_narrative: bool = False + contains_free_form_model_output: bool = False + human_confirmation_required: bool = True + decision_authority: str = _DECISION_AUTHORITY + review_state: str = _REVIEW_STATE + scope_verification_state: str = _SCOPE_VERIFICATION_STATE + mutation_state: str = _MUTATION_STATE + external_execution_state: str = _EXTERNAL_EXECUTION_STATE next_action: str = _NEXT_ACTION + def __repr__(self) -> str: + """Return a representation that never emits sensitive correlation evidence.""" + return "CompensationChangeReviewPacket()" + def __post_init__(self) -> None: - """Validate privacy, scope-correlation, timing, and high-impact review invariants.""" + """Fail closed when direct construction drifts from the governed contract.""" _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") - reference_prefixes = { - "compensation_review_reference": "compensation_change_review", - "person_record_reference": "person_record", - "employment_record_reference": "employment_record", - "active_assignment_snapshot_reference": "active_assignment_snapshot", - "current_compensation_snapshot_reference": "compensation_snapshot", - "proposed_compensation_plan_reference": "compensation_plan", - "compensation_policy_reference": "compensation_policy", - "pay_equity_review_reference": "pay_equity_review", - "budget_authorization_reference": "budget_authorization", - "payroll_handoff_plan_reference": "payroll_handoff_plan", - "requester_reference": "actor", - "reviewer_reference": "actor", - } - for field_name, prefix in reference_prefixes.items(): - _validate_reference(getattr(self, field_name), prefix, field_name) - - if self.requester_reference == self.reviewer_reference: - raise ValueError("requester_reference and reviewer_reference must be different actor references") - - digest_fields = ( - "active_assignment_snapshot_digest", + _validate_reference( + self.compensation_review_reference, + "compensation_change_review", + "compensation_review_reference", + ) + _validate_reference(self.person_record_reference, "person_record", "person_record_reference") + _validate_reference( + self.employment_record_reference, + "employment_record", + "employment_record_reference", + ) + _validate_reference( + self.active_assignment_snapshot_reference, + "active_assignment_snapshot", + "active_assignment_snapshot_reference", + ) + _validate_digest(self.active_assignment_snapshot_digest, "active_assignment_snapshot_digest") + _validate_reference( + self.current_compensation_snapshot_reference, + "compensation_snapshot", + "current_compensation_snapshot_reference", + ) + _validate_digest( + self.current_compensation_snapshot_digest, "current_compensation_snapshot_digest", + ) + _validate_reference( + self.proposed_compensation_plan_reference, + "compensation_plan", + "proposed_compensation_plan_reference", + ) + _validate_digest( + self.proposed_compensation_plan_digest, "proposed_compensation_plan_digest", - "compensation_policy_digest", - "pay_equity_review_digest", - "budget_authorization_digest", - "payroll_handoff_plan_digest", ) - for field_name in digest_fields: - _validate_digest(getattr(self, field_name), field_name) - - _validate_business_date(self.proposed_effective_on, "proposed_effective_on") - if self.purpose_code != "compensation_change_review": + _validate_reference( + self.compensation_policy_reference, + "compensation_policy", + "compensation_policy_reference", + ) + _validate_digest(self.compensation_policy_digest, "compensation_policy_digest") + _validate_reference( + self.pay_equity_review_reference, + "pay_equity_review", + "pay_equity_review_reference", + ) + _validate_digest(self.pay_equity_review_digest, "pay_equity_review_digest") + _validate_reference( + self.budget_authorization_reference, + "budget_authorization", + "budget_authorization_reference", + ) + _validate_digest(self.budget_authorization_digest, "budget_authorization_digest") + _validate_reference( + self.payroll_handoff_plan_reference, + "payroll_handoff_plan", + "payroll_handoff_plan_reference", + ) + _validate_digest(self.payroll_handoff_plan_digest, "payroll_handoff_plan_digest") + _validate_reference(self.requester_reference, "actor", "requester_reference") + _validate_reference(self.reviewer_reference, "actor", "reviewer_reference") + if self.requester_reference == self.reviewer_reference: + raise ValueError("requester and reviewer must be different actor references") + if self.purpose_code != _PURPOSE_CODE: raise ValueError("purpose_code must remain compensation_change_review") if self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must be an approved non-sensitive compensation-review category") - _validate_evidence_version(self.evidence_version) + _validate_business_date(self.proposed_effective_on, "proposed_effective_on") _canonical_timestamp(self.generated_at) - if self.review_state != "requires_human_review": + _validate_evidence_version(self.evidence_version) + if self.contains_personal_data is not True: + raise ValueError("compensation review packet must acknowledge sensitive personal-data correlation") + if self.contains_compensation_values is not False: + raise ValueError("compensation review packet must not contain compensation values") + if self.contains_protected_attribute_values is not False: + raise ValueError("compensation review packet must not contain protected-attribute values") + if self.contains_free_form_case_narrative is not False: + raise ValueError("compensation review packet must not contain free-form case narrative") + if self.contains_free_form_model_output is not False: + raise ValueError("compensation review packet must not contain free-form model output") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before a compensation change") + if self.decision_authority != _DECISION_AUTHORITY: + raise ValueError("decision_authority must remain human_review_only") + if self.review_state != _REVIEW_STATE: raise ValueError("review_state must remain requires_human_review") - if type(self.mutation_authorized) is not bool or self.mutation_authorized: - raise ValueError("mutation_authorized must remain false") - if self.scope_verification_state != "requires_authoritative_resolution": + if self.scope_verification_state != _SCOPE_VERIFICATION_STATE: raise ValueError("scope_verification_state must remain requires_authoritative_resolution") + if self.mutation_state != _MUTATION_STATE: + raise ValueError("mutation_state must remain not_authorized_to_apply") + if self.external_execution_state != _EXTERNAL_EXECUTION_STATE: + raise ValueError("external_execution_state must remain not_authorized_to_execute") if self.next_action != _NEXT_ACTION: - raise ValueError("next_action must remain the governed approval instruction") + raise ValueError("next_action must remain the governed compensation-change instruction") - def to_dict(self) -> dict[str, object]: - """Return canonical value-minimized audit evidence without compensation values.""" - return { + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + payload = { "active_assignment_snapshot_digest": self.active_assignment_snapshot_digest, "active_assignment_snapshot_reference": self.active_assignment_snapshot_reference, "budget_authorization_digest": self.budget_authorization_digest, @@ -183,12 +257,20 @@ def to_dict(self) -> dict[str, object]: "compensation_policy_digest": self.compensation_policy_digest, "compensation_policy_reference": self.compensation_policy_reference, "compensation_review_reference": self.compensation_review_reference, + "contains_compensation_values": self.contains_compensation_values, + "contains_free_form_case_narrative": self.contains_free_form_case_narrative, + "contains_free_form_model_output": self.contains_free_form_model_output, + "contains_personal_data": self.contains_personal_data, + "contains_protected_attribute_values": self.contains_protected_attribute_values, "current_compensation_snapshot_digest": self.current_compensation_snapshot_digest, "current_compensation_snapshot_reference": self.current_compensation_snapshot_reference, + "decision_authority": self.decision_authority, "employment_record_reference": self.employment_record_reference, "evidence_version": self.evidence_version, + "external_execution_state": self.external_execution_state, "generated_at": _canonical_timestamp(self.generated_at), - "mutation_authorized": self.mutation_authorized, + "human_confirmation_required": self.human_confirmation_required, + "mutation_state": self.mutation_state, "next_action": self.next_action, "pay_equity_review_digest": self.pay_equity_review_digest, "pay_equity_review_reference": self.pay_equity_review_reference, @@ -206,25 +288,66 @@ def to_dict(self) -> dict[str, object]: "scope_verification_state": self.scope_verification_state, "tenant_record_id": self.tenant_record_id, } - - def canonical_json(self) -> str: - """Return deterministic JSON suitable for immutable audit correlation.""" - return json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) def sha256_digest(self) -> str: - """Return SHA-256 over the exact canonical JSON evidence.""" - return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest() - - def __repr__(self) -> str: - """Return a correlation-free representation suitable for routine logs.""" - return ( - "CompensationChangeReviewPacket(" - f"review_state={self.review_state!r}, " - f"mutation_authorized={self.mutation_authorized!r}, " - f"scope_verification_state={self.scope_verification_state!r})" - ) + """Return SHA-256 over the exact canonical UTF-8 compensation-review packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() -def build_compensation_change_review_packet(**values: object) -> CompensationChangeReviewPacket: - """Build a validated compensation-change review packet from explicit governed fields.""" - return CompensationChangeReviewPacket(**values) # type: ignore[arg-type] +def build_compensation_change_review_packet( + *, + tenant_record_id: str, + compensation_review_reference: str, + person_record_reference: str, + employment_record_reference: str, + active_assignment_snapshot_reference: str, + active_assignment_snapshot_digest: str, + current_compensation_snapshot_reference: str, + current_compensation_snapshot_digest: str, + proposed_compensation_plan_reference: str, + proposed_compensation_plan_digest: str, + compensation_policy_reference: str, + compensation_policy_digest: str, + pay_equity_review_reference: str, + pay_equity_review_digest: str, + budget_authorization_reference: str, + budget_authorization_digest: str, + payroll_handoff_plan_reference: str, + payroll_handoff_plan_digest: str, + requester_reference: str, + reviewer_reference: str, + purpose_code: str, + reason_code: str, + proposed_effective_on: date, + generated_at: datetime, + evidence_version: int = 1, +) -> CompensationChangeReviewPacket: + """Build a value-minimized compensation packet pending authoritative human approval.""" + return CompensationChangeReviewPacket( + tenant_record_id=tenant_record_id, + compensation_review_reference=compensation_review_reference, + person_record_reference=person_record_reference, + employment_record_reference=employment_record_reference, + active_assignment_snapshot_reference=active_assignment_snapshot_reference, + active_assignment_snapshot_digest=active_assignment_snapshot_digest, + current_compensation_snapshot_reference=current_compensation_snapshot_reference, + current_compensation_snapshot_digest=current_compensation_snapshot_digest, + proposed_compensation_plan_reference=proposed_compensation_plan_reference, + proposed_compensation_plan_digest=proposed_compensation_plan_digest, + compensation_policy_reference=compensation_policy_reference, + compensation_policy_digest=compensation_policy_digest, + pay_equity_review_reference=pay_equity_review_reference, + pay_equity_review_digest=pay_equity_review_digest, + budget_authorization_reference=budget_authorization_reference, + budget_authorization_digest=budget_authorization_digest, + payroll_handoff_plan_reference=payroll_handoff_plan_reference, + payroll_handoff_plan_digest=payroll_handoff_plan_digest, + requester_reference=requester_reference, + reviewer_reference=reviewer_reference, + purpose_code=purpose_code, + reason_code=reason_code, + proposed_effective_on=proposed_effective_on, + generated_at=generated_at, + evidence_version=evidence_version, + ) \ No newline at end of file From 3826dcf284e2771a26e828f7ec4bfe943fa5dab1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:12:05 -0700 Subject: [PATCH 30/63] test(comp-review): reject forged string evidence types --- .../test_string_runtime_evidence_integrity.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py diff --git a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py new file mode 100644 index 000000000..b0cd8e9c0 --- /dev/null +++ b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py @@ -0,0 +1,82 @@ +"""Regression coverage for string-subclass evidence-boundary integrity.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone + +import pytest + +from orgmetra_compensation_change_review import build_compensation_change_review_packet + + +class ForgedReference(str): + """String subclass that forges namespace and UUID suffix validation.""" + + def startswith(self, prefix, *args): # type: ignore[no-untyped-def] + return True + + def split(self, sep=None, maxsplit=-1): # type: ignore[no-untyped-def] + return ["evil", "22222222-2222-4222-8222-222222222222"] + + +class ForgedTenantUUIDText(str): + """String subclass that forges UUID parsing and canonical-equality checks.""" + + def replace(self, old, new, *args): # type: ignore[no-untyped-def] + canonical = "11111111-1111-4111-8111-111111111111" + return canonical.replace(old, new, *args) + + def __eq__(self, other): # type: ignore[no-untyped-def] + if other is None: + return False + return True + + def __ne__(self, other): # type: ignore[no-untyped-def] + if other is None: + return True + return False + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid compensation-change review packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def test_rejects_reference_string_subclass_that_can_forge_namespace_validation() -> None: + kwargs = valid_kwargs() + kwargs["compensation_review_reference"] = ForgedReference("evil:payload") + with pytest.raises(ValueError, match="compensation_review_reference"): + build_compensation_change_review_packet(**kwargs) + + +def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation() -> None: + kwargs = valid_kwargs() + kwargs["tenant_record_id"] = ForgedTenantUUIDText("not-a-tenant-uuid") + with pytest.raises(ValueError, match="tenant_record_id"): + build_compensation_change_review_packet(**kwargs) From 9901e9956380e39b0ef34cb5bd0975e75ca677e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:12:50 -0700 Subject: [PATCH 31/63] fix(comp-review): require exact string evidence types --- .../src/orgmetra_compensation_change_review/packet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index bd56bed7f..cfb359d70 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -51,6 +51,8 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" + if type(value) is not str: + raise ValueError(f"{field_name} must be canonical UUID text") try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: @@ -63,7 +65,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" message = f"{field_name} must be an opaque {prefix}: reference" if ( - not isinstance(value, str) + type(value) is not str or len(value) > 160 or not _REFERENCE_PATTERN.fullmatch(value) or not value.startswith(f"{prefix}:") From 684b4393762daa47ae90158a53b4c8bb3f42a20a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:26:33 -0700 Subject: [PATCH 32/63] test(comp-review): reject forged governance text --- .../test_string_runtime_evidence_integrity.py | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py index b0cd8e9c0..f2738974d 100644 --- a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import replace from datetime import date, datetime, timezone import pytest @@ -27,15 +28,24 @@ def replace(self, old, new, *args): # type: ignore[no-untyped-def] return canonical.replace(old, new, *args) def __eq__(self, other): # type: ignore[no-untyped-def] - if other is None: - return False + return other is not None + + def __ne__(self, other): # type: ignore[no-untyped-def] + return other is None + + +class ForgedGovernanceText(str): + """String subclass that forges fixed equality and allow-list membership.""" + + def __eq__(self, other): # type: ignore[no-untyped-def] return True def __ne__(self, other): # type: ignore[no-untyped-def] - if other is None: - return True return False + def __hash__(self) -> int: + return hash("annual_compensation_review") + def valid_kwargs() -> dict[str, object]: """Return one otherwise valid compensation-change review packet input.""" @@ -80,3 +90,32 @@ def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation() -> None kwargs["tenant_record_id"] = ForgedTenantUUIDText("not-a-tenant-uuid") with pytest.raises(ValueError, match="tenant_record_id"): build_compensation_change_review_packet(**kwargs) + + +def test_rejects_forged_purpose_and_reason_codes() -> None: + purpose_kwargs = valid_kwargs() + purpose_kwargs["purpose_code"] = ForgedGovernanceText("attacker_controlled_purpose") + with pytest.raises(ValueError, match="purpose_code"): + build_compensation_change_review_packet(**purpose_kwargs) + + reason_kwargs = valid_kwargs() + reason_kwargs["reason_code"] = ForgedGovernanceText("attacker_controlled_reason") + with pytest.raises(ValueError, match="reason_code"): + build_compensation_change_review_packet(**reason_kwargs) + + +@pytest.mark.parametrize( + "field_name", + ( + "decision_authority", + "review_state", + "scope_verification_state", + "mutation_state", + "external_execution_state", + "next_action", + ), +) +def test_rejects_forged_direct_construction_constant_text(field_name: str) -> None: + packet = build_compensation_change_review_packet(**valid_kwargs()) + with pytest.raises(ValueError, match=field_name): + replace(packet, **{field_name: ForgedGovernanceText("attacker_controlled_text")}) From e83f71c24c3a2b4226bee3dd4988e2f8fc8012ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:37:08 -0700 Subject: [PATCH 33/63] fix(compensation-review): reject forged governance text --- .../packet.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index cfb359d70..34e7e0614 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -217,9 +217,9 @@ def __post_init__(self) -> None: _validate_reference(self.reviewer_reference, "actor", "reviewer_reference") if self.requester_reference == self.reviewer_reference: raise ValueError("requester and reviewer must be different actor references") - if self.purpose_code != _PURPOSE_CODE: + if type(self.purpose_code) is not str or self.purpose_code != _PURPOSE_CODE: raise ValueError("purpose_code must remain compensation_change_review") - if self.reason_code not in _ALLOWED_REASON_CODES: + if type(self.reason_code) is not str or self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must be an approved non-sensitive compensation-review category") _validate_business_date(self.proposed_effective_on, "proposed_effective_on") _canonical_timestamp(self.generated_at) @@ -236,17 +236,17 @@ def __post_init__(self) -> None: raise ValueError("compensation review packet must not contain free-form model output") if self.human_confirmation_required is not True: raise ValueError("human confirmation is mandatory before a compensation change") - if self.decision_authority != _DECISION_AUTHORITY: + if type(self.decision_authority) is not str or self.decision_authority != _DECISION_AUTHORITY: raise ValueError("decision_authority must remain human_review_only") - if self.review_state != _REVIEW_STATE: + if type(self.review_state) is not str or self.review_state != _REVIEW_STATE: raise ValueError("review_state must remain requires_human_review") - if self.scope_verification_state != _SCOPE_VERIFICATION_STATE: + if type(self.scope_verification_state) is not str or self.scope_verification_state != _SCOPE_VERIFICATION_STATE: raise ValueError("scope_verification_state must remain requires_authoritative_resolution") - if self.mutation_state != _MUTATION_STATE: + if type(self.mutation_state) is not str or self.mutation_state != _MUTATION_STATE: raise ValueError("mutation_state must remain not_authorized_to_apply") - if self.external_execution_state != _EXTERNAL_EXECUTION_STATE: + if type(self.external_execution_state) is not str or self.external_execution_state != _EXTERNAL_EXECUTION_STATE: raise ValueError("external_execution_state must remain not_authorized_to_execute") - if self.next_action != _NEXT_ACTION: + if type(self.next_action) is not str or self.next_action != _NEXT_ACTION: raise ValueError("next_action must remain the governed compensation-change instruction") def canonical_json(self) -> str: From 29c9fcabc3d528df7e85bed26f68c9dc3224882d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:13:20 -0700 Subject: [PATCH 34/63] test(compensation-review): expose post-issuance evidence rewrite --- .../test_post_issuance_evidence_integrity.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py diff --git a/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py new file mode 100644 index 000000000..cfef492a7 --- /dev/null +++ b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py @@ -0,0 +1,72 @@ +"""Regression coverage for post-issuance compensation-review evidence integrity.""" + +from copy import copy +from datetime import date, datetime, timezone + +import pytest + +from orgmetra_compensation_change_review import ( + CompensationChangeReviewPacket, + build_compensation_change_review_packet, +) + + +def _packet_args() -> dict[str, object]: + """Return one complete valid governed compensation-review fixture.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def _packet() -> CompensationChangeReviewPacket: + """Build one governed packet through the supported public constructor.""" + return build_compensation_change_review_packet(**_packet_args()) + + +def test_valid_value_mutation_cannot_rewrite_emitted_evidence() -> None: + """Low-level replacement with another valid value cannot emit a second audit truth.""" + packet = _packet() + original = packet.canonical_json() + object.__setattr__( + packet, + "compensation_policy_digest", + "2" * 64, + ) + + with pytest.raises(ValueError, match="integrity"): + packet.canonical_json() + assert '"compensation_policy_digest":"' + ("d" * 64) + '"' in original + + +def test_shallow_copy_does_not_inherit_process_local_issuance_evidence() -> None: + """Unsupported object copies must fail closed rather than inheriting issuance trust.""" + packet = _packet() + copied = copy(packet) + + assert copied is not packet + with pytest.raises(ValueError, match="integrity"): + copied.canonical_json() From 736b70b5c3fcef311ed0477d717356f9929eb8c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:14:50 -0700 Subject: [PATCH 35/63] fix(compensation-review): bind emitted evidence to issuance --- .../packet.py | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index 34e7e0614..b09afa136 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -14,7 +14,9 @@ from hashlib import sha256 import json import re +from threading import RLock from uuid import UUID +from weakref import ref _DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") _REFERENCE_PATTERN = re.compile( @@ -47,6 +49,8 @@ "the authoritative Orgmetra People boundary, and execute payroll only through its " "published owner contract." ) +_ISSUANCE_LOCK = RLock() +_ISSUANCE_DIGESTS: dict[int, tuple[object, str]] = {} def _validate_operational_uuid(value: str, field_name: str) -> None: @@ -105,7 +109,38 @@ def _validate_evidence_version(value: int) -> None: raise ValueError("evidence_version must be an integer from 1 through 2147483647") -@dataclass(frozen=True, slots=True, repr=False) +def _canonical_json(payload: dict[str, object]) -> str: + """Serialize one already-snapshotted evidence payload deterministically.""" + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + +def _seal_issuance(packet: object, canonical: str) -> None: + """Bind one live packet identity to its creation-time canonical evidence digest.""" + digest = sha256(canonical.encode("utf-8")).hexdigest() + packet_id = id(packet) + + def release_issuance(_packet_reference: object) -> None: + """Release the process-local seal when its packet is collected.""" + with _ISSUANCE_LOCK: + _ISSUANCE_DIGESTS.pop(packet_id, None) + + packet_reference = ref(packet, release_issuance) + with _ISSUANCE_LOCK: + _ISSUANCE_DIGESTS[packet_id] = (packet_reference, digest) + + +def _assert_issuance_integrity(packet: object, canonical: str) -> None: + """Fail closed when a live or copied packet differs from issued evidence.""" + digest = sha256(canonical.encode("utf-8")).hexdigest() + with _ISSUANCE_LOCK: + sealed = _ISSUANCE_DIGESTS.get(id(packet)) + if sealed is None: + raise ValueError("compensation review evidence integrity check failed") + if sealed[1] != digest: + raise ValueError("compensation review evidence integrity check failed") + + +@dataclass(frozen=True, slots=True, repr=False, weakref_slot=True) class CompensationChangeReviewPacket: """Immutable compensation-review evidence that cannot authorize mutation or payroll.""" @@ -248,10 +283,11 @@ def __post_init__(self) -> None: raise ValueError("external_execution_state must remain not_authorized_to_execute") if type(self.next_action) is not str or self.next_action != _NEXT_ACTION: raise ValueError("next_action must remain the governed compensation-change instruction") + _seal_issuance(self, _canonical_json(self._canonical_payload())) - def canonical_json(self) -> str: - """Return deterministic canonical JSON for immutable audit correlation.""" - payload = { + def _canonical_payload(self) -> dict[str, object]: + """Snapshot every trust-bearing field once for integrity verification and export.""" + return { "active_assignment_snapshot_digest": self.active_assignment_snapshot_digest, "active_assignment_snapshot_reference": self.active_assignment_snapshot_reference, "budget_authorization_digest": self.budget_authorization_digest, @@ -290,7 +326,12 @@ def canonical_json(self) -> str: "scope_verification_state": self.scope_verification_state, "tenant_record_id": self.tenant_record_id, } - return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def canonical_json(self) -> str: + """Return creation-bound deterministic canonical JSON for immutable audit correlation.""" + canonical = _canonical_json(self._canonical_payload()) + _assert_issuance_integrity(self, canonical) + return canonical def sha256_digest(self) -> str: """Return SHA-256 over the exact canonical UTF-8 compensation-review packet.""" From 0a956f72978f36bd4d9c458a7aac811721a8de3a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:16:17 -0700 Subject: [PATCH 36/63] test(compensation-review): exercise issuance cleanup --- .../tests/test_post_issuance_evidence_integrity.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py index cfef492a7..df4a76c4e 100644 --- a/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py @@ -2,6 +2,7 @@ from copy import copy from datetime import date, datetime, timezone +from gc import collect import pytest @@ -70,3 +71,14 @@ def test_shallow_copy_does_not_inherit_process_local_issuance_evidence() -> None assert copied is not packet with pytest.raises(ValueError, match="integrity"): copied.canonical_json() + + +def test_collected_packet_releases_process_local_issuance_binding() -> None: + """Weak cleanup must not leave stale process-local issuance state behind.""" + packet = _packet() + assert packet.canonical_json() + del packet + collect() + + replacement = _packet() + assert replacement.canonical_json() From 4a3035c98c374cba44697960261cac3ce3fd3044 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:16:42 -0700 Subject: [PATCH 37/63] docs(compensation-review): record issuance integrity repair --- packages/compensation-change-review/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 3500ee4bd..c6e46b46b 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -7,7 +7,9 @@ - `CompensationChangeReviewPacket`, a value-minimized pre-mutation evidence contract that binds authoritative worker scope, reviewed compensation-plan/policy evidence, pay-equity review, budget authorization, and payroll handoff provenance without copying compensation or protected-attribute values. - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. - Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identities for packet-owned namespaced opaque trust references, and exact 100% owned statement/branch coverage regressions. +- Creation-bound process-local evidence integrity: low-level valid-value field rewrites and unsupported object copies cannot emit a second valid-looking canonical compensation-review truth. ### Changed - `tenant_record_id` now follows the authoritative Orgmetra canonical non-sentinel operational-UUID contract instead of imposing a duplicate UUIDv4-only leaf policy; the protected-core UUIDv7 tenant form is covered explicitly while RFC 9562 Nil/Max sentinels remain rejected. +- Canonical export now snapshots all trust-bearing fields once and verifies the exact snapshot against the packet's construction-time SHA-256 seal before returning evidence. This process-local seal is defense in depth only and does not replace durable authorization, signatures, audit/outbox persistence, or correlation uniqueness. From e2bcb85706b824bca8b355247d505dd41fe25954 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:16:58 -0700 Subject: [PATCH 38/63] docs(compensation-review): explain creation-bound evidence --- packages/compensation-change-review/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md index 3a3aad074..d053eb9f2 100644 --- a/packages/compensation-change-review/README.md +++ b/packages/compensation-change-review/README.md @@ -20,6 +20,8 @@ A valid packet is not an approval. It remains: Immediately before approval, the host must re-resolve every reference in the packet tenant, prove requester/reviewer resolve to distinct authoritative actor identities, prove the Person-to-Employment and active Assignment/Job/Position scope, and verify the exact current/proposed compensation evidence, compensation policy, pay-equity review, budget authorization, effective date, and payroll-handoff provenance. UUID syntax is not authorization or relationship evidence: the tenant UUID is governed by authoritative Orgmetra core, while UUIDv4 on packet-owned references is only an opacity/privacy constraint. Any authorized HRIS change then goes through Orgmetra's authoritative People boundary. Payroll execution remains behind the payroll owner's published contract. +Each live packet instance is also bound to the SHA-256 of its exact canonical evidence at construction. `canonical_json()` snapshots every trust-bearing field once, verifies that exact snapshot against the construction-time seal, and returns the same verified snapshot. A low-level valid-value rewrite therefore fails closed rather than emitting a second audit truth, and an unsupported shallow-copied instance has no issuance binding. This is process-local defense in depth only: it is not a signature, durable uniqueness constraint, authorization token, or substitute for immutable audit/outbox persistence. + ## Example ```python From c9af575960cd4976d9dc918ea3ead966e938577a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 14:17:10 -0700 Subject: [PATCH 39/63] docs(compensation-review): trace post-issuance integrity --- docs/traceability/compensation-change-review.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 7519ba0a8..2431dccca 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -9,6 +9,7 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Preserve accountable separation of duties | Requester/reviewer references differ; canonical next action requires tenant-scoped identity re-resolution and distinct resolved actors | `test_same_opaque_actor_reference_is_rejected_early`, `test_next_action_requires_identity_scope_and_evidence_before_approval` | | Prevent review evidence from authorizing a high-impact action | Human-only review, unresolved authoritative scope, no HRIS mutation, no external execution are immutable | `test_direct_constructor_and_replace_fail_closed` | | Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | +| Prevent an issued packet from emitting rewritten compensation evidence | live object identity is bound to construction-time canonical SHA-256; canonical export snapshots once and rejects later valid-value rewrites or unsupported copies | `test_valid_value_mutation_cannot_rewrite_emitted_evidence`, `test_shallow_copy_does_not_inherit_process_local_issuance_evidence`, `test_collected_packet_releases_process_local_issuance_binding` | | Avoid accidental logging of worker/pay correlations | Generated dataclass representation disabled; custom representation fully redacted | `test_repr_redacts_personal_and_compensation_correlations` | | Preserve exact audit time semantics | Timezone-aware timestamps normalize to UTC without discarding fractional precision | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected` | @@ -18,4 +19,4 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt ## Ownership boundary -This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative tenant identity plus Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. UUIDv4 is an opacity/privacy constraint for packet-owned namespaced references only and does not establish tenant membership, actor identity, worker relationship, or policy applicability. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. +This slice adds no database migration, compensation calculation, payroll implementation, protected-attribute analytics, direct cross-service SQL, or competing People mutation surface. Authoritative tenant identity plus Person/Employment/Assignment scope and subsequent HRIS mutation remain Orgmetra core responsibilities; external payroll work remains behind its published owner contract. UUIDv4 is an opacity/privacy constraint for packet-owned namespaced references only and does not establish tenant membership, actor identity, worker relationship, or policy applicability. The process-local issuance registry protects only the live Python object and is not durable uniqueness, authorization, attestation, or a replacement for immutable audit/outbox persistence. `docs/doctoring/compensation-change-review-references.md` records current primary-source context without promoting the package into a legal-compliance engine. From dc3fa7ec81e6e2e6bc433d6cb39573fd8e185ff0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 18:09:04 +0900 Subject: [PATCH 40/63] fix(compensation-review): reject digest string subclasses --- packages/compensation-change-review/CHANGELOG.md | 1 + .../src/orgmetra_compensation_change_review/packet.py | 4 ++-- .../tests/test_string_runtime_evidence_integrity.py | 8 ++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index c6e46b46b..84319d503 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -13,3 +13,4 @@ - `tenant_record_id` now follows the authoritative Orgmetra canonical non-sentinel operational-UUID contract instead of imposing a duplicate UUIDv4-only leaf policy; the protected-core UUIDv7 tenant form is covered explicitly while RFC 9562 Nil/Max sentinels remain rejected. - Canonical export now snapshots all trust-bearing fields once and verifies the exact snapshot against the packet's construction-time SHA-256 seal before returning evidence. This process-local seal is defense in depth only and does not replace durable authorization, signatures, audit/outbox persistence, or correlation uniqueness. +- Digest evidence now rejects `str` subclasses so packet fields cannot retain caller-defined runtime behavior. diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index b09afa136..a617eb154 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -86,7 +86,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: def _validate_digest(value: str, field_name: str) -> None: """Require lowercase SHA-256 hexadecimal evidence.""" - if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + if type(value) is not str or not _DIGEST_PATTERN.fullmatch(value): raise ValueError(f"{field_name} must be lowercase SHA-256 hex") @@ -393,4 +393,4 @@ def build_compensation_change_review_packet( proposed_effective_on=proposed_effective_on, generated_at=generated_at, evidence_version=evidence_version, - ) \ No newline at end of file + ) diff --git a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py index f2738974d..8b0c23812 100644 --- a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py @@ -104,6 +104,14 @@ def test_rejects_forged_purpose_and_reason_codes() -> None: build_compensation_change_review_packet(**reason_kwargs) +def test_rejects_digest_string_subclass_before_pattern_match() -> None: + """Digest evidence must not retain caller-defined string behavior.""" + kwargs = valid_kwargs() + kwargs["compensation_policy_digest"] = ForgedGovernanceText("a" * 64) + with pytest.raises(ValueError, match="compensation_policy_digest"): + build_compensation_change_review_packet(**kwargs) + + @pytest.mark.parametrize( "field_name", ( From b1f1aa54637e0508b7785edc833430a7b58b4d77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:05:03 -0700 Subject: [PATCH 41/63] test(compensation-review): remove invalid static f-string --- packages/compensation-change-review/tests/test_packet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compensation-change-review/tests/test_packet.py b/packages/compensation-change-review/tests/test_packet.py index 30b2dbce4..e3750cf1e 100644 --- a/packages/compensation-change-review/tests/test_packet.py +++ b/packages/compensation-change-review/tests/test_packet.py @@ -196,7 +196,7 @@ def test_every_reference_and_digest_is_validated() -> None: for field_name, prefix in reference_fields.items(): with pytest.raises(ValueError, match=re.escape(f"opaque {prefix}: reference")): - replace(packet, **{field_name: f"wrong:11111111-1111-4111-8111-111111111111"}) + replace(packet, **{field_name: "wrong:11111111-1111-4111-8111-111111111111"}) for field_name in digest_fields: with pytest.raises(ValueError, match="lowercase SHA-256"): replace(packet, **{field_name: "z" * 64}) From 28e26a81dc8befcd3482689f62dca93c28e0983d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:14:35 -0700 Subject: [PATCH 42/63] test(compensation-review): require one canonical valid packet fixture --- .../tests/test_shared_fixture_architecture.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_shared_fixture_architecture.py diff --git a/packages/compensation-change-review/tests/test_shared_fixture_architecture.py b/packages/compensation-change-review/tests/test_shared_fixture_architecture.py new file mode 100644 index 000000000..48cab100c --- /dev/null +++ b/packages/compensation-change-review/tests/test_shared_fixture_architecture.py @@ -0,0 +1,19 @@ +"""Regression coverage for one canonical compensation-review test input fixture.""" + +from pathlib import Path + + +def test_valid_packet_input_fixture_has_one_canonical_definition() -> None: + """Keep the 24-field valid packet seed in conftest instead of copy-pasted tests.""" + tests_dir = Path(__file__).parent + marker = ( + '"compensation_review_reference": ' + '"compensation_change_review:22222222-2222-4222-8222-222222222222"' + ) + owners = sorted( + path.name + for path in tests_dir.glob("*.py") + if marker in path.read_text(encoding="utf-8") + ) + + assert owners == ["conftest.py"] From 551a83e77af9428911d2021b7b31a82ba2a4a173 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:14:55 -0700 Subject: [PATCH 43/63] test(compensation-review): add canonical valid packet fixture --- .../tests/conftest.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/compensation-change-review/tests/conftest.py diff --git a/packages/compensation-change-review/tests/conftest.py b/packages/compensation-change-review/tests/conftest.py new file mode 100644 index 000000000..b40e07241 --- /dev/null +++ b/packages/compensation-change-review/tests/conftest.py @@ -0,0 +1,37 @@ +"""Shared pytest fixtures for governed compensation-change review contracts.""" + +from datetime import date, datetime, timezone + +import pytest + + +@pytest.fixture +def valid_packet_kwargs() -> dict[str, object]: + """Return one fresh complete valid packet input set with opaque identities.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", + "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", + "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", + "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", + "active_assignment_snapshot_digest": "a" * 64, + "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", + "current_compensation_snapshot_digest": "b" * 64, + "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", + "proposed_compensation_plan_digest": "c" * 64, + "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", + "compensation_policy_digest": "d" * 64, + "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", + "pay_equity_review_digest": "e" * 64, + "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "budget_authorization_digest": "f" * 64, + "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "payroll_handoff_plan_digest": "1" * 64, + "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + "purpose_code": "compensation_change_review", + "reason_code": "annual_compensation_review", + "proposed_effective_on": date(2026, 10, 1), + "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), + "evidence_version": 1, + } From 02487646947f9d936375169dd60e0542944ffed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:15:25 -0700 Subject: [PATCH 44/63] test(compensation-review): use shared valid packet fixture --- .../tests/test_packet.py | 93 +++++++------------ 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/packages/compensation-change-review/tests/test_packet.py b/packages/compensation-change-review/tests/test_packet.py index e3750cf1e..63a4adab7 100644 --- a/packages/compensation-change-review/tests/test_packet.py +++ b/packages/compensation-change-review/tests/test_packet.py @@ -1,6 +1,6 @@ """Regression coverage for governed compensation-change review evidence.""" from dataclasses import replace -from datetime import date, datetime, timedelta, timezone, tzinfo +from datetime import datetime, timedelta, timezone, tzinfo import json import re @@ -12,45 +12,14 @@ ) -def valid_kwargs() -> dict[str, object]: - """Return one complete valid packet input set with opaque deterministic identities.""" - return { - "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", - "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", - "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", - "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - "active_assignment_snapshot_digest": "a" * 64, - "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", - "current_compensation_snapshot_digest": "b" * 64, - "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", - "proposed_compensation_plan_digest": "c" * 64, - "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", - "compensation_policy_digest": "d" * 64, - "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", - "pay_equity_review_digest": "e" * 64, - "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "budget_authorization_digest": "f" * 64, - "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "payroll_handoff_plan_digest": "1" * 64, - "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - "purpose_code": "compensation_change_review", - "reason_code": "annual_compensation_review", - "proposed_effective_on": date(2026, 10, 1), - "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), - "evidence_version": 1, - } - - -def build_valid() -> CompensationChangeReviewPacket: +def build_valid(valid_packet_kwargs: dict[str, object]) -> CompensationChangeReviewPacket: """Build one valid packet through the supported public builder.""" - return build_compensation_change_review_packet(**valid_kwargs()) + return build_compensation_change_review_packet(**valid_packet_kwargs) -def test_builds_deterministic_value_minimized_packet() -> None: +def test_builds_deterministic_value_minimized_packet(valid_packet_kwargs: dict[str, object]) -> None: """A valid packet is canonical, human-only, unresolved, and excludes protected values.""" - packet = build_valid() + packet = build_valid(valid_packet_kwargs) canonical = json.loads(packet.canonical_json()) assert packet.decision_authority == "human_review_only" @@ -64,12 +33,12 @@ def test_builds_deterministic_value_minimized_packet() -> None: assert canonical["generated_at"] == "2026-08-19T06:12:13.456789Z" assert canonical["evidence_version"] == 1 assert len(packet.sha256_digest()) == 64 - assert packet.sha256_digest() == build_valid().sha256_digest() + assert packet.sha256_digest() == build_valid(valid_packet_kwargs).sha256_digest() -def test_canonical_evidence_changes_with_governed_artifact_or_version() -> None: +def test_canonical_evidence_changes_with_governed_artifact_or_version(valid_packet_kwargs: dict[str, object]) -> None: """Changing exact reviewed evidence or its version changes the immutable packet digest.""" - packet = build_valid() + packet = build_valid(valid_packet_kwargs) changed_plan = replace(packet, proposed_compensation_plan_digest="2" * 64) changed_policy = replace(packet, compensation_policy_digest="3" * 64) changed_version = replace(packet, evidence_version=2) @@ -77,9 +46,9 @@ def test_canonical_evidence_changes_with_governed_artifact_or_version() -> None: assert len({packet.sha256_digest(), changed_plan.sha256_digest(), changed_policy.sha256_digest(), changed_version.sha256_digest()}) == 4 -def test_repr_redacts_personal_and_compensation_correlations() -> None: +def test_repr_redacts_personal_and_compensation_correlations(valid_packet_kwargs: dict[str, object]) -> None: """Normal repr formatting cannot leak worker, actor, or evidence correlation identifiers.""" - packet = build_valid() + packet = build_valid(valid_packet_kwargs) rendered = repr(packet) assert rendered == "CompensationChangeReviewPacket()" @@ -95,9 +64,9 @@ def test_repr_redacts_personal_and_compensation_correlations() -> None: assert sensitive not in rendered -def test_next_action_requires_identity_scope_and_evidence_before_approval() -> None: +def test_next_action_requires_identity_scope_and_evidence_before_approval(valid_packet_kwargs: dict[str, object]) -> None: """The canonical operator instruction orders tenant, actor, worker, and evidence checks first.""" - action = build_valid().next_action + action = build_valid(valid_packet_kwargs).next_action actor_clause = "verify their resolved actor identities are distinct" worker_clause = "prove the Person-to-Employment binding" evidence_clause = "verify the current compensation snapshot" @@ -126,14 +95,19 @@ def test_next_action_requires_identity_scope_and_evidence_before_approval() -> N ("next_action", "Send directly to payroll.", "next_action must remain"), ], ) -def test_direct_constructor_and_replace_fail_closed(field_name: str, value: object, message: str) -> None: +def test_direct_constructor_and_replace_fail_closed( + field_name: str, + value: object, + message: str, + valid_packet_kwargs: dict[str, object], +) -> None: """Both construction paths reject attempts to weaken immutable high-impact guardrails.""" - direct_kwargs = valid_kwargs() + direct_kwargs = valid_packet_kwargs.copy() direct_kwargs[field_name] = value with pytest.raises(ValueError, match=message): CompensationChangeReviewPacket(**direct_kwargs) - packet = build_valid() + packet = build_valid(valid_packet_kwargs) with pytest.raises(ValueError, match=message): replace(packet, **{field_name: value}) @@ -167,17 +141,22 @@ def test_direct_constructor_and_replace_fail_closed(field_name: str, value: obje ("evidence_version", 2_147_483_648, "evidence_version must be an integer"), ], ) -def test_invalid_core_inputs_fail_closed(field_name: str, value: object, message: str) -> None: +def test_invalid_core_inputs_fail_closed( + field_name: str, + value: object, + message: str, + valid_packet_kwargs: dict[str, object], +) -> None: """Malformed identity, evidence, purpose, time, and version inputs are rejected.""" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs[field_name] = value with pytest.raises(ValueError, match=message): build_compensation_change_review_packet(**kwargs) -def test_every_reference_and_digest_is_validated() -> None: +def test_every_reference_and_digest_is_validated(valid_packet_kwargs: dict[str, object]) -> None: """Each trust-bearing reference and digest field is individually bound to its namespace.""" - packet = build_valid() + packet = build_valid(valid_packet_kwargs) reference_fields = { "compensation_review_reference": "compensation_change_review", "person_record_reference": "person_record", @@ -192,7 +171,7 @@ def test_every_reference_and_digest_is_validated() -> None: "requester_reference": "actor", "reviewer_reference": "actor", } - digest_fields = [name for name in valid_kwargs() if name.endswith("_digest")] + digest_fields = [name for name in valid_packet_kwargs if name.endswith("_digest")] for field_name, prefix in reference_fields.items(): with pytest.raises(ValueError, match=re.escape(f"opaque {prefix}: reference")): @@ -202,17 +181,17 @@ def test_every_reference_and_digest_is_validated() -> None: replace(packet, **{field_name: "z" * 64}) -def test_same_opaque_actor_reference_is_rejected_early() -> None: +def test_same_opaque_actor_reference_is_rejected_early(valid_packet_kwargs: dict[str, object]) -> None: """Exact requester/reviewer reuse is denied before authoritative identity re-resolution.""" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs["reviewer_reference"] = kwargs["requester_reference"] with pytest.raises(ValueError, match="different actor references"): build_compensation_change_review_packet(**kwargs) -def test_fractional_and_offset_timestamps_preserve_distinct_instants() -> None: +def test_fractional_and_offset_timestamps_preserve_distinct_instants(valid_packet_kwargs: dict[str, object]) -> None: """Canonical evidence preserves microseconds while normalizing equivalent offsets to UTC.""" - packet = build_valid() + packet = build_valid(valid_packet_kwargs) later = replace(packet, generated_at=packet.generated_at.replace(microsecond=456790)) offset = replace( packet, @@ -235,9 +214,9 @@ def dst(self, dt: datetime | None) -> None: return None -def test_timezone_with_unknown_offset_is_rejected() -> None: +def test_timezone_with_unknown_offset_is_rejected(valid_packet_kwargs: dict[str, object]) -> None: """A tzinfo object is insufficient when it cannot establish an actual UTC offset.""" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs["generated_at"] = datetime(2026, 8, 19, 6, 12, tzinfo=UnknownOffset()) with pytest.raises(ValueError, match="generated_at must be timezone-aware"): build_compensation_change_review_packet(**kwargs) From ec47ec3806b35499c4b2044a6a27f1bef32f7b0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:15:36 -0700 Subject: [PATCH 45/63] test(compensation-review): reuse shared fixture for issuance integrity --- .../test_post_issuance_evidence_integrity.py | 50 ++++--------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py index df4a76c4e..fde9adc60 100644 --- a/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_post_issuance_evidence_integrity.py @@ -1,7 +1,6 @@ """Regression coverage for post-issuance compensation-review evidence integrity.""" from copy import copy -from datetime import date, datetime, timezone from gc import collect import pytest @@ -12,45 +11,14 @@ ) -def _packet_args() -> dict[str, object]: - """Return one complete valid governed compensation-review fixture.""" - return { - "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", - "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", - "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", - "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - "active_assignment_snapshot_digest": "a" * 64, - "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", - "current_compensation_snapshot_digest": "b" * 64, - "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", - "proposed_compensation_plan_digest": "c" * 64, - "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", - "compensation_policy_digest": "d" * 64, - "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", - "pay_equity_review_digest": "e" * 64, - "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "budget_authorization_digest": "f" * 64, - "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "payroll_handoff_plan_digest": "1" * 64, - "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - "purpose_code": "compensation_change_review", - "reason_code": "annual_compensation_review", - "proposed_effective_on": date(2026, 10, 1), - "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), - "evidence_version": 1, - } - - -def _packet() -> CompensationChangeReviewPacket: +def _packet(valid_packet_kwargs: dict[str, object]) -> CompensationChangeReviewPacket: """Build one governed packet through the supported public constructor.""" - return build_compensation_change_review_packet(**_packet_args()) + return build_compensation_change_review_packet(**valid_packet_kwargs) -def test_valid_value_mutation_cannot_rewrite_emitted_evidence() -> None: +def test_valid_value_mutation_cannot_rewrite_emitted_evidence(valid_packet_kwargs: dict[str, object]) -> None: """Low-level replacement with another valid value cannot emit a second audit truth.""" - packet = _packet() + packet = _packet(valid_packet_kwargs) original = packet.canonical_json() object.__setattr__( packet, @@ -63,9 +31,9 @@ def test_valid_value_mutation_cannot_rewrite_emitted_evidence() -> None: assert '"compensation_policy_digest":"' + ("d" * 64) + '"' in original -def test_shallow_copy_does_not_inherit_process_local_issuance_evidence() -> None: +def test_shallow_copy_does_not_inherit_process_local_issuance_evidence(valid_packet_kwargs: dict[str, object]) -> None: """Unsupported object copies must fail closed rather than inheriting issuance trust.""" - packet = _packet() + packet = _packet(valid_packet_kwargs) copied = copy(packet) assert copied is not packet @@ -73,12 +41,12 @@ def test_shallow_copy_does_not_inherit_process_local_issuance_evidence() -> None copied.canonical_json() -def test_collected_packet_releases_process_local_issuance_binding() -> None: +def test_collected_packet_releases_process_local_issuance_binding(valid_packet_kwargs: dict[str, object]) -> None: """Weak cleanup must not leave stale process-local issuance state behind.""" - packet = _packet() + packet = _packet(valid_packet_kwargs) assert packet.canonical_json() del packet collect() - replacement = _packet() + replacement = _packet(valid_packet_kwargs) assert replacement.canonical_json() From da158817d14044ff5c5b8ecc5dd777a179686730 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:15:46 -0700 Subject: [PATCH 46/63] test(compensation-review): reuse shared fixture for reference privacy --- .../tests/test_reference_privacy.py | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/packages/compensation-change-review/tests/test_reference_privacy.py b/packages/compensation-change-review/tests/test_reference_privacy.py index f1f25792c..c419f3078 100644 --- a/packages/compensation-change-review/tests/test_reference_privacy.py +++ b/packages/compensation-change-review/tests/test_reference_privacy.py @@ -1,6 +1,5 @@ """Privacy regressions for opaque compensation-review trust references.""" from dataclasses import replace -from datetime import date, datetime, timezone import pytest @@ -9,37 +8,6 @@ UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" -def valid_kwargs() -> dict[str, object]: - """Return one complete packet whose trust references use opaque UUIDv4 suffixes.""" - return { - "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", - "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", - "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", - "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - "active_assignment_snapshot_digest": "a" * 64, - "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", - "current_compensation_snapshot_digest": "b" * 64, - "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", - "proposed_compensation_plan_digest": "c" * 64, - "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", - "compensation_policy_digest": "d" * 64, - "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", - "pay_equity_review_digest": "e" * 64, - "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "budget_authorization_digest": "f" * 64, - "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "payroll_handoff_plan_digest": "1" * 64, - "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - "purpose_code": "compensation_change_review", - "reason_code": "annual_compensation_review", - "proposed_effective_on": date(2026, 10, 1), - "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), - "evidence_version": 1, - } - - @pytest.mark.parametrize( ("field_name", "prefix"), [ @@ -60,14 +28,15 @@ def valid_kwargs() -> dict[str, object]: def test_uuid1_trust_reference_is_rejected_by_builder_and_replace( field_name: str, prefix: str, + valid_packet_kwargs: dict[str, object], ) -> None: """UUIDv1 timestamp/node metadata must never enter an opaque trust-reference field.""" value = f"{prefix}:{UUID1_ID}" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs[field_name] = value with pytest.raises(ValueError, match=field_name): build_compensation_change_review_packet(**kwargs) - packet = build_compensation_change_review_packet(**valid_kwargs()) + packet = build_compensation_change_review_packet(**valid_packet_kwargs) with pytest.raises(ValueError, match=field_name): replace(packet, **{field_name: value}) From c74c67d8d0bb7c070173aa6b295dd4b7e266206b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:16:00 -0700 Subject: [PATCH 47/63] test(compensation-review): reuse shared fixture for string integrity --- .../test_string_runtime_evidence_integrity.py | 62 ++++++------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py index 8b0c23812..3084734c8 100644 --- a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import replace -from datetime import date, datetime, timezone +from datetime import datetime import pytest @@ -47,66 +47,39 @@ def __hash__(self) -> int: return hash("annual_compensation_review") -def valid_kwargs() -> dict[str, object]: - """Return one otherwise valid compensation-change review packet input.""" - return { - "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", - "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", - "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", - "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - "active_assignment_snapshot_digest": "a" * 64, - "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", - "current_compensation_snapshot_digest": "b" * 64, - "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", - "proposed_compensation_plan_digest": "c" * 64, - "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", - "compensation_policy_digest": "d" * 64, - "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", - "pay_equity_review_digest": "e" * 64, - "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "budget_authorization_digest": "f" * 64, - "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "payroll_handoff_plan_digest": "1" * 64, - "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - "purpose_code": "compensation_change_review", - "reason_code": "annual_compensation_review", - "proposed_effective_on": date(2026, 10, 1), - "generated_at": datetime(2026, 8, 19, 6, 12, 13, 456789, tzinfo=timezone.utc), - "evidence_version": 1, - } - - -def test_rejects_reference_string_subclass_that_can_forge_namespace_validation() -> None: - kwargs = valid_kwargs() +def test_rejects_reference_string_subclass_that_can_forge_namespace_validation( + valid_packet_kwargs: dict[str, object], +) -> None: + kwargs = valid_packet_kwargs.copy() kwargs["compensation_review_reference"] = ForgedReference("evil:payload") with pytest.raises(ValueError, match="compensation_review_reference"): build_compensation_change_review_packet(**kwargs) -def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation() -> None: - kwargs = valid_kwargs() +def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation( + valid_packet_kwargs: dict[str, object], +) -> None: + kwargs = valid_packet_kwargs.copy() kwargs["tenant_record_id"] = ForgedTenantUUIDText("not-a-tenant-uuid") with pytest.raises(ValueError, match="tenant_record_id"): build_compensation_change_review_packet(**kwargs) -def test_rejects_forged_purpose_and_reason_codes() -> None: - purpose_kwargs = valid_kwargs() +def test_rejects_forged_purpose_and_reason_codes(valid_packet_kwargs: dict[str, object]) -> None: + purpose_kwargs = valid_packet_kwargs.copy() purpose_kwargs["purpose_code"] = ForgedGovernanceText("attacker_controlled_purpose") with pytest.raises(ValueError, match="purpose_code"): build_compensation_change_review_packet(**purpose_kwargs) - reason_kwargs = valid_kwargs() + reason_kwargs = valid_packet_kwargs.copy() reason_kwargs["reason_code"] = ForgedGovernanceText("attacker_controlled_reason") with pytest.raises(ValueError, match="reason_code"): build_compensation_change_review_packet(**reason_kwargs) -def test_rejects_digest_string_subclass_before_pattern_match() -> None: +def test_rejects_digest_string_subclass_before_pattern_match(valid_packet_kwargs: dict[str, object]) -> None: """Digest evidence must not retain caller-defined string behavior.""" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs["compensation_policy_digest"] = ForgedGovernanceText("a" * 64) with pytest.raises(ValueError, match="compensation_policy_digest"): build_compensation_change_review_packet(**kwargs) @@ -123,7 +96,10 @@ def test_rejects_digest_string_subclass_before_pattern_match() -> None: "next_action", ), ) -def test_rejects_forged_direct_construction_constant_text(field_name: str) -> None: - packet = build_compensation_change_review_packet(**valid_kwargs()) +def test_rejects_forged_direct_construction_constant_text( + field_name: str, + valid_packet_kwargs: dict[str, object], +) -> None: + packet = build_compensation_change_review_packet(**valid_packet_kwargs) with pytest.raises(ValueError, match=field_name): replace(packet, **{field_name: ForgedGovernanceText("attacker_controlled_text")}) From e5a1e74d413d341ab4946ee053259f73ac35379a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:16:07 -0700 Subject: [PATCH 48/63] test(compensation-review): reuse shared fixture for temporal integrity --- .../tests/test_temporal_evidence_integrity.py | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py index c4f622a75..b89e45b5e 100644 --- a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import date, datetime, timezone +from datetime import datetime, timezone import pytest @@ -21,40 +21,11 @@ def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] return "2099-12-31T23:59:59+00:00" -def valid_kwargs() -> dict[str, object]: - """Return one otherwise valid compensation-change review packet input.""" - return { - "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "compensation_review_reference": "compensation_change_review:22222222-2222-4222-8222-222222222222", - "person_record_reference": "person_record:33333333-3333-4333-8333-333333333333", - "employment_record_reference": "employment_record:44444444-4444-4444-8444-444444444444", - "active_assignment_snapshot_reference": "active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - "active_assignment_snapshot_digest": "a" * 64, - "current_compensation_snapshot_reference": "compensation_snapshot:66666666-6666-4666-8666-666666666666", - "current_compensation_snapshot_digest": "b" * 64, - "proposed_compensation_plan_reference": "compensation_plan:77777777-7777-4777-8777-777777777777", - "proposed_compensation_plan_digest": "c" * 64, - "compensation_policy_reference": "compensation_policy:88888888-8888-4888-8888-888888888888", - "compensation_policy_digest": "d" * 64, - "pay_equity_review_reference": "pay_equity_review:99999999-9999-4999-8999-999999999999", - "pay_equity_review_digest": "e" * 64, - "budget_authorization_reference": "budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - "budget_authorization_digest": "f" * 64, - "payroll_handoff_plan_reference": "payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - "payroll_handoff_plan_digest": "1" * 64, - "requester_reference": "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - "reviewer_reference": "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - "purpose_code": "compensation_change_review", - "reason_code": "annual_compensation_review", - "proposed_effective_on": date(2026, 10, 1), - "generated_at": datetime(2026, 8, 21, 4, 25, tzinfo=timezone.utc), - "evidence_version": 1, - } - - -def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence() -> None: +def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence( + valid_packet_kwargs: dict[str, object], +) -> None: """Canonical audit evidence must not call caller-overridable datetime methods.""" - kwargs = valid_kwargs() + kwargs = valid_packet_kwargs.copy() kwargs["generated_at"] = ForgedDateTime(2026, 8, 21, 4, 25, tzinfo=timezone.utc) with pytest.raises(ValueError, match="generated_at"): From d767befe864218c5592dbb71c3e075eb3d2082ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:16:15 -0700 Subject: [PATCH 49/63] test(compensation-review): reuse shared fixture for tenant privacy --- .../tests/test_tenant_uuid_privacy.py | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py index 3538a770f..a5480a594 100644 --- a/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py +++ b/packages/compensation-change-review/tests/test_tenant_uuid_privacy.py @@ -1,7 +1,5 @@ """Tenant identity interoperability regressions for compensation-change review evidence.""" -from datetime import date, datetime, timezone - import pytest from orgmetra_compensation_change_review import build_compensation_change_review_packet @@ -10,40 +8,18 @@ _AUTHORITATIVE_UUIDV7_TENANT = "10000000-0000-7000-8000-000000000001" -def _build_with_tenant(tenant_record_id: str): +def _build_with_tenant(tenant_record_id: str, valid_packet_kwargs: dict[str, object]): """Build a valid value-minimized compensation packet around one tenant identity.""" - return build_compensation_change_review_packet( - tenant_record_id=tenant_record_id, - compensation_review_reference="compensation_change_review:22222222-2222-4222-8222-222222222222", - person_record_reference="person_record:33333333-3333-4333-8333-333333333333", - employment_record_reference="employment_record:44444444-4444-4444-8444-444444444444", - active_assignment_snapshot_reference="active_assignment_snapshot:55555555-5555-4555-8555-555555555555", - active_assignment_snapshot_digest="a" * 64, - current_compensation_snapshot_reference="compensation_snapshot:66666666-6666-4666-8666-666666666666", - current_compensation_snapshot_digest="b" * 64, - proposed_compensation_plan_reference="compensation_plan:77777777-7777-4777-8777-777777777777", - proposed_compensation_plan_digest="c" * 64, - compensation_policy_reference="compensation_policy:88888888-8888-4888-8888-888888888888", - compensation_policy_digest="d" * 64, - pay_equity_review_reference="pay_equity_review:99999999-9999-4999-8999-999999999999", - pay_equity_review_digest="e" * 64, - budget_authorization_reference="budget_authorization:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", - budget_authorization_digest="f" * 64, - payroll_handoff_plan_reference="payroll_handoff_plan:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", - payroll_handoff_plan_digest="1" * 64, - requester_reference="actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", - reviewer_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", - purpose_code="compensation_change_review", - reason_code="annual_compensation_review", - proposed_effective_on=date(2026, 10, 1), - generated_at=datetime(2026, 8, 20, 8, 15, tzinfo=timezone.utc), - evidence_version=1, - ) - - -def test_accepts_authoritative_operational_uuidv7_tenant_identity() -> None: + kwargs = valid_packet_kwargs.copy() + kwargs["tenant_record_id"] = tenant_record_id + return build_compensation_change_review_packet(**kwargs) + + +def test_accepts_authoritative_operational_uuidv7_tenant_identity( + valid_packet_kwargs: dict[str, object], +) -> None: """Accept the canonical UUIDv7 tenant form already accepted by protected HRIS core.""" - packet = _build_with_tenant(_AUTHORITATIVE_UUIDV7_TENANT) + packet = _build_with_tenant(_AUTHORITATIVE_UUIDV7_TENANT, valid_packet_kwargs) assert packet.tenant_record_id == _AUTHORITATIVE_UUIDV7_TENANT @@ -54,7 +30,10 @@ def test_accepts_authoritative_operational_uuidv7_tenant_identity() -> None: "ffffffff-ffff-ffff-ffff-ffffffffffff", ], ) -def test_rejects_reserved_sentinel_tenant_identity(tenant_record_id: str) -> None: +def test_rejects_reserved_sentinel_tenant_identity( + tenant_record_id: str, + valid_packet_kwargs: dict[str, object], +) -> None: """Reject RFC 9562 Nil/Max sentinels while deferring UUID version policy to HRIS core.""" with pytest.raises(ValueError, match="tenant_record_id"): - _build_with_tenant(tenant_record_id) + _build_with_tenant(tenant_record_id, valid_packet_kwargs) From 227926e3dafed17a2ec287b3102315c866648f6c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:21:33 -0700 Subject: [PATCH 50/63] test(compensation-review): remove stale datetime import --- .../tests/test_string_runtime_evidence_integrity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py index 3084734c8..914969800 100644 --- a/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_string_runtime_evidence_integrity.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import replace -from datetime import datetime import pytest From 21fb130bf5e35430824fc649aa25503d68f0add7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:17:46 -0700 Subject: [PATCH 51/63] test(compensation): expose mutable timezone evidence drift --- .../tests/test_temporal_evidence_integrity.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py index b89e45b5e..a34801707 100644 --- a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone, tzinfo import pytest @@ -21,6 +21,21 @@ def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] return "2099-12-31T23:59:59+00:00" +class MutableOffsetTimezone(tzinfo): + """Timezone whose offset can change after a packet has been issued.""" + + def __init__(self, offset_hours: int) -> None: + self.offset_hours = offset_hours + + def utcoffset(self, dt): # type: ignore[no-untyped-def] + """Return the current mutable offset.""" + return timedelta(hours=self.offset_hours) + + def dst(self, dt): # type: ignore[no-untyped-def] + """Keep daylight-saving behavior deterministic for the regression.""" + return timedelta(0) + + def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence( valid_packet_kwargs: dict[str, object], ) -> None: @@ -30,3 +45,24 @@ def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence( with pytest.raises(ValueError, match="generated_at"): build_compensation_change_review_packet(**kwargs) + + +def test_freezes_mutable_timezone_before_issuing_recorded_time_evidence( + valid_packet_kwargs: dict[str, object], +) -> None: + """Caller-owned tzinfo mutation must not rewrite or invalidate issued audit evidence.""" + mutable_timezone = MutableOffsetTimezone(9) + kwargs = valid_packet_kwargs.copy() + kwargs["generated_at"] = datetime(2026, 8, 21, 13, 25, tzinfo=mutable_timezone) + + packet = build_compensation_change_review_packet(**kwargs) + canonical_before = packet.canonical_json() + digest_before = packet.sha256_digest() + + assert packet.generated_at.tzinfo is timezone.utc + assert '"generated_at":"2026-08-21T04:25:00Z"' in canonical_before + + mutable_timezone.offset_hours = -5 + + assert packet.canonical_json() == canonical_before + assert packet.sha256_digest() == digest_before From 729711c555a28d12bac3b6df6cea69dbdbb77b8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:21:36 -0700 Subject: [PATCH 52/63] fix(compensation): freeze recorded-time evidence --- .../orgmetra_compensation_change_review/packet.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py index a617eb154..692fa6798 100644 --- a/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py +++ b/packages/compensation-change-review/src/orgmetra_compensation_change_review/packet.py @@ -90,6 +90,17 @@ def _validate_digest(value: str, field_name: str) -> None: raise ValueError(f"{field_name} must be lowercase SHA-256 hex") +def _freeze_recorded_timestamp(value: datetime) -> datetime: + """Detach one recorded-time instant from caller-owned timezone state.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError("generated_at must be timezone-aware exact datetime") + offset = value.utcoffset() + if offset is None: + raise ValueError("generated_at must be timezone-aware exact datetime") + utc_naive = value.replace(tzinfo=None) - offset + return utc_naive.replace(tzinfo=timezone.utc) + + def _canonical_timestamp(value: datetime) -> str: """Render an aware instant as precision-preserving UTC RFC 3339 text.""" if type(value) is not datetime or value.tzinfo is None or value.utcoffset() is None: @@ -257,6 +268,7 @@ def __post_init__(self) -> None: if type(self.reason_code) is not str or self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must be an approved non-sensitive compensation-review category") _validate_business_date(self.proposed_effective_on, "proposed_effective_on") + object.__setattr__(self, "generated_at", _freeze_recorded_timestamp(self.generated_at)) _canonical_timestamp(self.generated_at) _validate_evidence_version(self.evidence_version) if self.contains_personal_data is not True: From 4b269cc86f3a5dcce575ab9f5eb1e0751eb043b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:23:32 -0700 Subject: [PATCH 53/63] test(compensation): cover indeterminate timezone offsets --- .../tests/test_temporal_evidence_integrity.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py index a34801707..08a32c15c 100644 --- a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py @@ -36,6 +36,18 @@ def dst(self, dt): # type: ignore[no-untyped-def] return timedelta(0) +class MissingOffsetTimezone(tzinfo): + """Timezone object that cannot establish a concrete UTC offset.""" + + def utcoffset(self, dt): # type: ignore[no-untyped-def] + """Signal that the recorded instant cannot be resolved.""" + return None + + def dst(self, dt): # type: ignore[no-untyped-def] + """Keep daylight-saving behavior absent for this invalid timezone.""" + return None + + def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence( valid_packet_kwargs: dict[str, object], ) -> None: @@ -47,6 +59,17 @@ def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence( build_compensation_change_review_packet(**kwargs) +def test_rejects_timezone_without_resolvable_utc_offset( + valid_packet_kwargs: dict[str, object], +) -> None: + """Recorded-time evidence must fail closed when its UTC instant is indeterminate.""" + kwargs = valid_packet_kwargs.copy() + kwargs["generated_at"] = datetime(2026, 8, 21, 4, 25, tzinfo=MissingOffsetTimezone()) + + with pytest.raises(ValueError, match="generated_at"): + build_compensation_change_review_packet(**kwargs) + + def test_freezes_mutable_timezone_before_issuing_recorded_time_evidence( valid_packet_kwargs: dict[str, object], ) -> None: From 876376f2bcb21e78054300364767cd1a7c44288c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:24:38 -0700 Subject: [PATCH 54/63] docs(compensation): trace recorded-time detachment --- docs/traceability/compensation-change-review.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 2431dccca..1a831b77d 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -11,7 +11,7 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Bind policy, equity, budget, payroll handoff, and evidence version | Independent SHA-256 digests and bounded positive `evidence_version` are canonical | `test_canonical_evidence_changes_with_governed_artifact_or_version`, `test_invalid_core_inputs_fail_closed` | | Prevent an issued packet from emitting rewritten compensation evidence | live object identity is bound to construction-time canonical SHA-256; canonical export snapshots once and rejects later valid-value rewrites or unsupported copies | `test_valid_value_mutation_cannot_rewrite_emitted_evidence`, `test_shallow_copy_does_not_inherit_process_local_issuance_evidence`, `test_collected_packet_releases_process_local_issuance_binding` | | Avoid accidental logging of worker/pay correlations | Generated dataclass representation disabled; custom representation fully redacted | `test_repr_redacts_personal_and_compensation_correlations` | -| Preserve exact audit time semantics | Timezone-aware timestamps normalize to UTC without discarding fractional precision | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected` | +| Preserve exact audit time semantics without retaining caller-owned mutable timezone state | Exact built-in timezone-aware `datetime` input is resolved once, detached into a built-in UTC `datetime`, and then used for canonical evidence; unresolved offsets and hostile datetime subclasses fail closed | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected`, `test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence`, `test_rejects_timezone_without_resolvable_utc_offset`, `test_freezes_mutable_timezone_before_issuing_recorded_time_evidence` | ## Quality gate From 861274aa3e12b10c9f391a1049be47d147c0a51f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:24:58 -0700 Subject: [PATCH 55/63] docs(compensation): define detached UTC evidence time --- docs/adr/0022-governed-compensation-change-review.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/adr/0022-governed-compensation-change-review.md b/docs/adr/0022-governed-compensation-change-review.md index da7d53c68..22982ba31 100644 --- a/docs/adr/0022-governed-compensation-change-review.md +++ b/docs/adr/0022-governed-compensation-change-review.md @@ -9,13 +9,17 @@ Compensation changes are high-impact employment actions. A useful enterprise rev Syntactically valid Person, Employment, Assignment, policy, actor, or compensation-artifact references do not prove tenant membership, worker scope, policy applicability, actor separation, or effective-date correctness. Opaque identifiers also remain sensitive correlating metadata even when direct identifiers and pay values are absent. The authoritative HRIS already owns tenant identity semantics: protected Orgmetra core accepts canonical non-sentinel operational UUIDs, including UUIDv7. This leaf package therefore must not narrow `tenant_record_id` to UUIDv4. UUIDv4 remains appropriate for packet-owned opaque references where this package owns the correlation-privacy contract. +Recorded-time evidence also crosses an ownership boundary. Python permits a caller to attach a custom `tzinfo` object to an otherwise exact built-in `datetime`; that timezone object can be mutable. Retaining it inside an issued review packet would allow later caller-side state changes to rewrite the apparent UTC instant or invalidate the packet's creation-time digest. The packet therefore must resolve the input offset exactly once and retain only a detached built-in UTC `datetime` as its trust-bearing recorded-time value. + Current primary-source context is recorded in `docs/doctoring/compensation-change-review-references.md`. ISO 30414:2025 provides current human-capital reporting/disclosure context; U.S. EEOC compensation guidance demonstrates why compensation governance must not turn protected-attribute review into uncontrolled evidence copying; U.S. Department of Labor FLSA recordkeeping guidance demonstrates the need for accountable wage/time records. These are governance inputs only, not certification or universal legal-compliance claims. ## Decision Orgmetra will expose a value-minimized `CompensationChangeReviewPacket` before any authoritative compensation-related HRIS mutation or payroll execution. -The packet binds one authoritative Orgmetra `tenant_record_id` that satisfies the protected-core canonical non-sentinel operational-UUID contract. Packet-owned opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer remain canonical non-sentinel UUIDv4-backed namespaced references. This preserves interoperability with authoritative UUIDv7 tenant identities while preventing timestamp/node-derived correlation metadata from entering packet-owned references presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving timezone-aware evidence instant are part of canonical evidence. +The packet binds one authoritative Orgmetra `tenant_record_id` that satisfies the protected-core canonical non-sentinel operational-UUID contract. Packet-owned opaque references for the compensation review, Person, Employment, active Assignment/Job/Position snapshot, current compensation snapshot, proposed compensation plan, compensation policy, pay-equity review, budget authorization, payroll handoff plan, requester, and reviewer remain canonical non-sentinel UUIDv4-backed namespaced references. This preserves interoperability with authoritative UUIDv7 tenant identities while preventing timestamp/node-derived correlation metadata from entering packet-owned references presented as opaque. Evidence artifacts carry independent lowercase SHA-256 digests; a bounded positive `evidence_version`, proposed business effective date, and precision-preserving recorded-time instant are part of canonical evidence. + +`generated_at` accepts only an exact built-in timezone-aware `datetime`. Construction obtains a concrete UTC offset once, fails closed when the offset is indeterminate, converts the wall time to the corresponding instant, and stores a detached exact built-in UTC `datetime` backed by `datetime.timezone.utc`. Canonical export therefore never depends on later mutation of a caller-owned timezone object. Datetime subclasses are rejected so caller-overridable conversion or formatting methods cannot forge recorded-time evidence. The packet deliberately excludes compensation values, protected-attribute values, free-form case narrative, credentials, and free-form model output. It explicitly acknowledges remaining personal-data correlation with `contains_personal_data = true`. @@ -41,6 +45,7 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w - Authoritative UUIDv7 tenant identities remain interoperable instead of being rejected by a leaf-only UUIDv4 policy. - UUIDv1 timestamp/node correlation remains excluded from packet-owned namespaced trust references. - Changes to governed evidence or `evidence_version` change the canonical packet digest. +- Recorded-time evidence is detached from caller-owned mutable timezone state before issuance, so a previously issued canonical payload cannot drift because an input timezone object later changes. - Cross-tenant, wrong-worker, stale-policy, actor-separation, and effective-date questions remain explicit authoritative-resolution obligations instead of being inferred from reference syntax. ### Trade-offs @@ -50,7 +55,8 @@ Any authorized HRIS change must use the authoritative Orgmetra People boundary w - A pay-equity evidence reference/digest proves which artifact was reviewed, not that compensation is fair, nondiscriminatory, or legally sufficient. - The packet does not calculate compensation, payroll, taxes, protected-class statistics, pay-equity findings, or legal conclusions. - Hosts must perform authoritative scope and actor resolution at approval time. +- The stored recorded-time representation is normalized to UTC; an input timezone's display-zone identity is intentionally not retained as trust-bearing evidence. ## Verification -The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; authoritative tenant interoperability with the protected-core canonical non-sentinel operational-UUID contract, including the canonical UUIDv7 tenant used by the core PostgreSQL regression and rejection of RFC 9562 Nil/Max sentinels; strict UUIDv4 packet-owned namespaced references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date and precision-preserving timestamp validation; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. +The package requires exact 100% owned statement and branch coverage; beginner-readable module/class/callable docstrings; direct-construction and `dataclasses.replace(...)` fail-closed regressions; authoritative tenant interoperability with the protected-core canonical non-sentinel operational-UUID contract, including the canonical UUIDv7 tenant used by the core PostgreSQL regression and rejection of RFC 9562 Nil/Max sentinels; strict UUIDv4 packet-owned namespaced references including UUIDv1 rejection; lowercase SHA-256 evidence; redacted `repr`; closed non-sensitive reason categories; bounded evidence versions; business-date validation; exact built-in timezone-aware timestamp validation; one-time offset resolution with detached UTC storage; indeterminate-offset and datetime-subclass rejection; mutable-timezone post-issuance regression; deterministic canonical JSON/digest evidence; separate requester/reviewer references plus explicit authoritative identity re-resolution; and immutable human-review/no-mutation/no-execution states. From faab04e94627515d8b26ffff0de36eb7eaacc880 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:26:49 -0700 Subject: [PATCH 56/63] test(compensation): fail closed on corrupted recorded time --- .../tests/test_temporal_evidence_integrity.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py index 08a32c15c..f2087cc13 100644 --- a/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py +++ b/packages/compensation-change-review/tests/test_temporal_evidence_integrity.py @@ -70,6 +70,21 @@ def test_rejects_timezone_without_resolvable_utc_offset( build_compensation_change_review_packet(**kwargs) +def test_corrupted_post_issuance_time_fails_closed_before_canonical_export( + valid_packet_kwargs: dict[str, object], +) -> None: + """Low-level mutation must not make an indeterminate recorded time exportable.""" + packet = build_compensation_change_review_packet(**valid_packet_kwargs) + object.__setattr__( + packet, + "generated_at", + datetime(2026, 8, 21, 4, 25, tzinfo=MissingOffsetTimezone()), + ) + + with pytest.raises(ValueError, match="generated_at"): + packet.canonical_json() + + def test_freezes_mutable_timezone_before_issuing_recorded_time_evidence( valid_packet_kwargs: dict[str, object], ) -> None: From 5ed358a7c393394fddae12c2777f55aafbe72196 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:27:06 -0700 Subject: [PATCH 57/63] docs(compensation): record timestamp evidence hardening --- packages/compensation-change-review/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 84319d503..ac01b66b1 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -14,3 +14,4 @@ - `tenant_record_id` now follows the authoritative Orgmetra canonical non-sentinel operational-UUID contract instead of imposing a duplicate UUIDv4-only leaf policy; the protected-core UUIDv7 tenant form is covered explicitly while RFC 9562 Nil/Max sentinels remain rejected. - Canonical export now snapshots all trust-bearing fields once and verifies the exact snapshot against the packet's construction-time SHA-256 seal before returning evidence. This process-local seal is defense in depth only and does not replace durable authorization, signatures, audit/outbox persistence, or correlation uniqueness. - Digest evidence now rejects `str` subclasses so packet fields cannot retain caller-defined runtime behavior. +- Recorded-time evidence now resolves the input UTC offset once and stores a detached exact built-in UTC `datetime`; caller-owned mutable timezone state cannot rewrite or invalidate an already-issued packet, while indeterminate offsets and datetime subclasses fail closed. From 9eab9d50ae0a202f4c3398ae60fba726c60ccb67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 08:27:23 -0700 Subject: [PATCH 58/63] docs(compensation): explain stable recorded-time evidence --- packages/compensation-change-review/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/compensation-change-review/README.md b/packages/compensation-change-review/README.md index d053eb9f2..44194a1d0 100644 --- a/packages/compensation-change-review/README.md +++ b/packages/compensation-change-review/README.md @@ -8,6 +8,8 @@ The packet correlates one authoritative Orgmetra tenant, Person, Employment, act The envelope intentionally **does not carry salary, wage, bonus, benefit, equity, protected-attribute, credential, or free-form case/model values**. Opaque Person/Employment and evidence references still create sensitive personal-data correlation, so the packet explicitly reports `contains_personal_data = true` rather than claiming anonymity. +Recorded-time evidence is normalized at construction: Orgmetra resolves the supplied aware `datetime` to one concrete instant and stores a detached exact built-in UTC `datetime`. This means an application-owned mutable timezone object cannot later rewrite or invalidate an already-issued packet. Inputs whose UTC offset cannot be resolved, and `datetime` subclasses that could override trust-bearing conversion or formatting behavior, fail closed. + ## Human and system boundary A valid packet is not an approval. It remains: From 118a3a385a76b2a791a107465ad51e213e4477de Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:32:12 -0700 Subject: [PATCH 59/63] test(compensation-review): require shared config quality triggers --- .../tests/test_quality_workflow_trigger.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 packages/compensation-change-review/tests/test_quality_workflow_trigger.py diff --git a/packages/compensation-change-review/tests/test_quality_workflow_trigger.py b/packages/compensation-change-review/tests/test_quality_workflow_trigger.py new file mode 100644 index 000000000..0352bb9e8 --- /dev/null +++ b/packages/compensation-change-review/tests/test_quality_workflow_trigger.py @@ -0,0 +1,27 @@ +"""Regression tests for the compensation-review quality-gate trigger surface.""" + +from pathlib import Path + + +_WORKFLOW_PATH = Path(".github/workflows/compensation-change-review-quality.yml") +_SHARED_TEST_CONFIGURATION = ( + ".gitignore", + ".python-version", + "conftest.py", + "packages/conftest.py", + "pyproject.toml", + "pytest.ini", + "setup.cfg", + "tox.ini", +) + + +def test_quality_workflow_retriggers_on_shared_test_configuration() -> None: + """Require every shared test/runtime configuration input to retrigger this gate.""" + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + + for path in _SHARED_TEST_CONFIGURATION: + assert f'- "{path}"' in workflow, ( + f"{path} can change package test or clean-checkout behavior and must retrigger " + "Compensation Change Review Quality" + ) From 2842218d135b30f81f980e7174679ec265469542 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:33:49 -0700 Subject: [PATCH 60/63] fix(compensation-review): retrigger quality on shared config --- .github/workflows/compensation-change-review-quality.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/compensation-change-review-quality.yml b/.github/workflows/compensation-change-review-quality.yml index 64a2b4dbb..e1d111eef 100644 --- a/.github/workflows/compensation-change-review-quality.yml +++ b/.github/workflows/compensation-change-review-quality.yml @@ -8,6 +8,14 @@ on: - "packages/compensation-change-review/**" - ".github/requirements/foundation-test.txt" - ".github/workflows/compensation-change-review-quality.yml" + - ".gitignore" + - ".python-version" + - "conftest.py" + - "packages/conftest.py" + - "pyproject.toml" + - "pytest.ini" + - "setup.cfg" + - "tox.ini" - "docs/adr/0022-governed-compensation-change-review.md" - "docs/doctoring/compensation-change-review-references.md" - "docs/traceability/compensation-change-review.md" From d753741f596c8c6efa03c6eb180c29df6afe1d97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:36:31 -0700 Subject: [PATCH 61/63] docs(compensation-review): record shared-config gate integrity --- packages/compensation-change-review/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index ac01b66b1..15d1b8acc 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -8,6 +8,7 @@ - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. - Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identities for packet-owned namespaced opaque trust references, and exact 100% owned statement/branch coverage regressions. - Creation-bound process-local evidence integrity: low-level valid-value field rewrites and unsupported object copies cannot emit a second valid-looking canonical compensation-review truth. +- `Compensation Change Review Quality` now retriggers on shared repository Python/test/clean-checkout configuration, with an executable regression preventing stale package-quality evidence after shared tooling changes. ### Changed From b9e487cc1e07267ff69d222b0917ec108804b799 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 15:36:46 -0700 Subject: [PATCH 62/63] docs(compensation-review): trace shared-config quality evidence --- docs/traceability/compensation-change-review.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 1a831b77d..8e0440fd9 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -12,10 +12,11 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Prevent an issued packet from emitting rewritten compensation evidence | live object identity is bound to construction-time canonical SHA-256; canonical export snapshots once and rejects later valid-value rewrites or unsupported copies | `test_valid_value_mutation_cannot_rewrite_emitted_evidence`, `test_shallow_copy_does_not_inherit_process_local_issuance_evidence`, `test_collected_packet_releases_process_local_issuance_binding` | | Avoid accidental logging of worker/pay correlations | Generated dataclass representation disabled; custom representation fully redacted | `test_repr_redacts_personal_and_compensation_correlations` | | Preserve exact audit time semantics without retaining caller-owned mutable timezone state | Exact built-in timezone-aware `datetime` input is resolved once, detached into a built-in UTC `datetime`, and then used for canonical evidence; unresolved offsets and hostile datetime subclasses fail closed | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected`, `test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence`, `test_rejects_timezone_without_resolvable_utc_offset`, `test_freezes_mutable_timezone_before_issuing_recorded_time_evidence` | +| Keep package-quality evidence current when shared repository test/runtime configuration changes | package quality workflow explicitly retriggers on shared Python/test/clean-checkout configuration inputs | `tests/test_quality_workflow_trigger.py`; supplemental to central required workflows | ## Quality gate -`.github/workflows/compensation-change-review-quality.yml` checks out the exact PR head, uses the repository's hash-pinned reviewed Python test toolchain, compiles source/tests, requires 100% package statement and branch coverage, and rejects checkout side effects. +`.github/workflows/compensation-change-review-quality.yml` checks out the exact PR head, uses the repository's hash-pinned reviewed Python test toolchain, compiles source/tests, requires 100% package statement and branch coverage, rejects checkout side effects, and explicitly retriggers when shared repository Python/test/clean-checkout configuration changes. ## Ownership boundary From 13327760f71dbfacd0d02411a79c875ade0d7417 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:19:31 +0900 Subject: [PATCH 63/63] fix(compensation-review): consolidate quality admission Exact hosted Foundation run 34005848591 exposed a semantic protected-parent adoption defect: the feature branch retained the package-specific compensation review workflow even though protected #161 had consolidated local quality admission into Foundation CI. That extra leaf workflow violated the explicit runner/workflow inventory before validation or package tests could run. Retire the leaf workflow, run the compensation-review suite directly from canonical Foundation CI, preserve the package's exact 100% statement/branch coverage contract, add a regression against leaf-workflow resurrection, update traceability/changelog, and reseal the Foundation manifest entry for the final bytes. No production compensation logic, database contract, HR domain truth, coverage threshold, or required central gate is weakened. --- .../compensation-change-review-quality.yml | 65 ------------------- .github/workflows/foundation-ci.yml | 1 + .../compensation-change-review.md | 4 +- manifest.json | 6 +- .../compensation-change-review/CHANGELOG.md | 2 +- .../tests/test_quality_workflow_trigger.py | 38 +++++------ 6 files changed, 26 insertions(+), 90 deletions(-) delete mode 100644 .github/workflows/compensation-change-review-quality.yml diff --git a/.github/workflows/compensation-change-review-quality.yml b/.github/workflows/compensation-change-review-quality.yml deleted file mode 100644 index e1d111eef..000000000 --- a/.github/workflows/compensation-change-review-quality.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Compensation Change Review Quality - -on: - pull_request: - branches: - - develop - paths: - - "packages/compensation-change-review/**" - - ".github/requirements/foundation-test.txt" - - ".github/workflows/compensation-change-review-quality.yml" - - ".gitignore" - - ".python-version" - - "conftest.py" - - "packages/conftest.py" - - "pyproject.toml" - - "pytest.ini" - - "setup.cfg" - - "tox.ini" - - "docs/adr/0022-governed-compensation-change-review.md" - - "docs/doctoring/compensation-change-review-references.md" - - "docs/traceability/compensation-change-review.md" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: compensation-change-review-quality-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - unit: - name: Compensation change 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 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 compensation change review package - run: python -m compileall -q packages/compensation-change-review/src packages/compensation-change-review/tests - - name: Test compensation change review with exact statement and branch coverage - env: - PYTHONPATH: packages/compensation-change-review/src - COVERAGE_FILE: /tmp/orgmetra-compensation-change-review.coverage - run: python -m pytest -c packages/compensation-change-review/pyproject.toml packages/compensation-change-review/tests - - name: Require clean checkout - run: | - git diff --exit-code - test -z "$(git status --porcelain)" diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 6b475d6f2..ef069fdf3 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -59,6 +59,7 @@ jobs: - name: Run owned unit and service contracts once run: | PYTHONPATH=packages/candidate-evidence/src COVERAGE_FILE=/tmp/orgmetra-candidate-evidence.coverage python -m pytest -c packages/candidate-evidence/pyproject.toml packages/candidate-evidence/tests + PYTHONPATH=packages/compensation-change-review/src COVERAGE_FILE=/tmp/orgmetra-compensation-change-review.coverage python -m pytest -c packages/compensation-change-review/pyproject.toml packages/compensation-change-review/tests PYTHONPATH=packages/hris-kernel/src COVERAGE_FILE=/tmp/orgmetra-hris-kernel.coverage python -m pytest -c packages/hris-kernel/pyproject.toml packages/hris-kernel/tests PYTHONPATH=packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-keyverse-adapter.coverage python -m pytest -c packages/keyverse-adapter/pyproject.toml packages/keyverse-adapter/tests PYTHONPATH=packages/migration-adapter/src COVERAGE_FILE=/tmp/orgmetra-migration-adapter.coverage python -m pytest -c packages/migration-adapter/pyproject.toml packages/migration-adapter/tests diff --git a/docs/traceability/compensation-change-review.md b/docs/traceability/compensation-change-review.md index 8e0440fd9..ac34c72cb 100644 --- a/docs/traceability/compensation-change-review.md +++ b/docs/traceability/compensation-change-review.md @@ -12,11 +12,11 @@ Status: **active PR only / proposed**, not protected-`develop` product truth unt | Prevent an issued packet from emitting rewritten compensation evidence | live object identity is bound to construction-time canonical SHA-256; canonical export snapshots once and rejects later valid-value rewrites or unsupported copies | `test_valid_value_mutation_cannot_rewrite_emitted_evidence`, `test_shallow_copy_does_not_inherit_process_local_issuance_evidence`, `test_collected_packet_releases_process_local_issuance_binding` | | Avoid accidental logging of worker/pay correlations | Generated dataclass representation disabled; custom representation fully redacted | `test_repr_redacts_personal_and_compensation_correlations` | | Preserve exact audit time semantics without retaining caller-owned mutable timezone state | Exact built-in timezone-aware `datetime` input is resolved once, detached into a built-in UTC `datetime`, and then used for canonical evidence; unresolved offsets and hostile datetime subclasses fail closed | `test_fractional_and_offset_timestamps_preserve_distinct_instants`, `test_timezone_with_unknown_offset_is_rejected`, `test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence`, `test_rejects_timezone_without_resolvable_utc_offset`, `test_freezes_mutable_timezone_before_issuing_recorded_time_evidence` | -| Keep package-quality evidence current when shared repository test/runtime configuration changes | package quality workflow explicitly retriggers on shared Python/test/clean-checkout configuration inputs | `tests/test_quality_workflow_trigger.py`; supplemental to central required workflows | +| Keep package-quality evidence current without recreating leaf CI | Canonical Foundation CI invokes the compensation-review package suite directly on every `develop` PR and the package-specific workflow remains absent | `tests/test_quality_workflow_trigger.py`, `tests.test_github_actions_runner_image`; supplemental to central required workflows | ## Quality gate -`.github/workflows/compensation-change-review-quality.yml` checks out the exact PR head, uses the repository's hash-pinned reviewed Python test toolchain, compiles source/tests, requires 100% package statement and branch coverage, rejects checkout side effects, and explicitly retriggers when shared repository Python/test/clean-checkout configuration changes. +`.github/workflows/foundation-ci.yml` checks out the exact PR head on the explicit `ubuntu-24.04` runner, uses the repository's hash-pinned reviewed Python test toolchain, and invokes the compensation-review package suite directly. The package's `pyproject.toml` continues to require exact 100% statement and branch coverage. `.github/workflows/compensation-change-review-quality.yml` is deliberately absent after repository-quality consolidation; executable regressions reject its reintroduction rather than duplicating the canonical Foundation lane. ## Ownership boundary diff --git a/manifest.json b/manifest.json index f7b6cf55e..0d6e4531b 100644 --- a/manifest.json +++ b/manifest.json @@ -5,9 +5,9 @@ "files": [ { "path": ".github/workflows/foundation-ci.yml", - "sha256": "b6a4365936b66803a8112f034c77d53d33301a7a798ed4f68746a4f2d8b081d7", - "bytes": 6651, - "lines": 125 + "sha256": "972ef8929abe93342eb11d000c490e397b44e6e34ddb41d317383ff67067ffe8", + "bytes": 6889, + "lines": 126 }, { "path": ".gitignore", diff --git a/packages/compensation-change-review/CHANGELOG.md b/packages/compensation-change-review/CHANGELOG.md index 15d1b8acc..345fbabb5 100644 --- a/packages/compensation-change-review/CHANGELOG.md +++ b/packages/compensation-change-review/CHANGELOG.md @@ -8,7 +8,7 @@ - Fail-closed human-review, authoritative-resolution, no-HRIS-mutation, and no-payroll-execution states with separate requester/reviewer actor references and bounded evidence versions. - Deterministic canonical JSON/SHA-256 evidence, redacted representations, canonical non-sentinel UUIDv4 identities for packet-owned namespaced opaque trust references, and exact 100% owned statement/branch coverage regressions. - Creation-bound process-local evidence integrity: low-level valid-value field rewrites and unsupported object copies cannot emit a second valid-looking canonical compensation-review truth. -- `Compensation Change Review Quality` now retriggers on shared repository Python/test/clean-checkout configuration, with an executable regression preventing stale package-quality evidence after shared tooling changes. +- Compensation-review quality now runs inside canonical `Foundation CI`; the package-specific workflow is retired after protected repository-quality consolidation, while the package `pyproject.toml` continues to enforce exact 100% statement and branch coverage. ### Changed diff --git a/packages/compensation-change-review/tests/test_quality_workflow_trigger.py b/packages/compensation-change-review/tests/test_quality_workflow_trigger.py index 0352bb9e8..b6e53ca18 100644 --- a/packages/compensation-change-review/tests/test_quality_workflow_trigger.py +++ b/packages/compensation-change-review/tests/test_quality_workflow_trigger.py @@ -1,27 +1,27 @@ -"""Regression tests for the compensation-review quality-gate trigger surface.""" +"""Regression tests for consolidated compensation-review quality admission.""" from pathlib import Path -_WORKFLOW_PATH = Path(".github/workflows/compensation-change-review-quality.yml") -_SHARED_TEST_CONFIGURATION = ( - ".gitignore", - ".python-version", - "conftest.py", - "packages/conftest.py", - "pyproject.toml", - "pytest.ini", - "setup.cfg", - "tox.ini", +_FOUNDATION_WORKFLOW = Path(".github/workflows/foundation-ci.yml") +_RETIRED_LEAF_WORKFLOW = Path( + ".github/workflows/compensation-change-review-quality.yml" ) +_PACKAGE_TEST_COMMAND = ( + "PYTHONPATH=packages/compensation-change-review/src " + "COVERAGE_FILE=/tmp/orgmetra-compensation-change-review.coverage " + "python -m pytest -c packages/compensation-change-review/pyproject.toml " + "packages/compensation-change-review/tests" +) + + +def test_foundation_runs_compensation_review_with_exact_package_coverage() -> None: + """Keep compensation review inside the canonical one-job Foundation lane.""" + workflow = _FOUNDATION_WORKFLOW.read_text(encoding="utf-8") + assert _PACKAGE_TEST_COMMAND in workflow -def test_quality_workflow_retriggers_on_shared_test_configuration() -> None: - """Require every shared test/runtime configuration input to retrigger this gate.""" - workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") - for path in _SHARED_TEST_CONFIGURATION: - assert f'- "{path}"' in workflow, ( - f"{path} can change package test or clean-checkout behavior and must retrigger " - "Compensation Change Review Quality" - ) +def test_leaf_quality_workflow_stays_retired() -> None: + """Do not recreate a package-specific runner after repository consolidation.""" + assert not _RETIRED_LEAF_WORKFLOW.exists()