diff --git a/.github/workflows/performance-review-quality.yml b/.github/workflows/performance-review-quality.yml new file mode 100644 index 000000000..7f1e4e4eb --- /dev/null +++ b/.github/workflows/performance-review-quality.yml @@ -0,0 +1,65 @@ +name: Performance Review Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/performance-review/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/performance-review-quality.yml" + - ".gitignore" + - ".python-version" + - "conftest.py" + - "packages/conftest.py" + - "pyproject.toml" + - "pytest.ini" + - "setup.cfg" + - "tox.ini" + - "docs/adr/0018-governed-performance-review.md" + - "docs/doctoring/performance-review-references.md" + - "docs/traceability/performance-review.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: performance-review-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Performance 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 performance review package + run: python -m compileall -q packages/performance-review/src packages/performance-review/tests + - name: Test performance review with exact statement and branch coverage + env: + PYTHONPATH: packages/performance-review/src + COVERAGE_FILE: /tmp/orgmetra-performance-review.coverage + run: python -m pytest -c packages/performance-review/pyproject.toml packages/performance-review/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/CHANGELOG.md b/CHANGELOG.md index 99f4752d7..a37a982de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to Orgmetra will be documented in this file. - Accepted ADRs 0001–0003 now include buyer-facing Context, Decision, and Consequences grounded in verified ISO 30400:2022, ISO 30414:2025, Uniform Guidelines (29 C.F.R. Part 1607), SIOP (2018), OpenAPI Specification v3.2.0, OpenID Connect Core 1.0 errata set 2, CloudEvents v1.0.2, Jensen and Snodgrass (1999), Snodgrass (1999), and Allen (1983) records already listed in `docs/doctoring/REFERENCES.md`. ADRs 0004 and 0005 gained APA 7th References pointers to that same bibliography without changing their Decision bodies. - Active-PR governed Job Analysis persistence/API on the canonical `JobAnalysisSnapshot` model: migration `0013_job_analysis_snapshot.sql` stores immutable tenant-scoped snapshot, Task, KSAO, Task–KSAO, FJA and write-command evidence; `POST /v1/tenants/{tenant_record_id}/job-analysis-snapshots` and matching GET enforce purpose-bound Keyverse scope, authenticated-principal actor authority, bounded/strict JSON handling, transactional Idempotency-Key serialization, parent-scope fail-closed integrity, forced RLS, and atomic audit/outbox evidence. ADR 0014 records the persistence decision while ADR 0007 remains the domain/evidence authority; validated evidence still requires accountable human review and non-LLM provenance, and the service does not make a high-impact employment decision. - Active-PR `orgmetra_selection_review` packet for PII-minimized, evidence-bound human selection review: canonical operational tenant identity, UUID-backed opaque candidate/Job/sealed-evidence/reviewer references, explicit purpose/reason/evidence version, deterministic canonical JSON and SHA-256 correlation, mandatory human decision state, redacted packet repr, and provenance-paired model evidence that remains `untrusted_draft`, with exact 100% owned statement and branch coverage required by its quality gate. +- Active-PR `orgmetra-performance-review` packet for value-minimized, pre-rating human-review evidence: system-recorded issuance time, conservative person-reference handling until authoritative provenance and scope resolution, governed performance-cycle/criterion/goal evidence, optional development-plan provenance, issuance integrity sealing, and exact 100% owned statement and branch coverage required by its quality gate. This capability is not protected-branch truth until its current PR passes fresh gates and merges. - Active performance-criterion scope hardening: `criterion_observation_scope_guard` rejects criterion outcomes for a Job the worker did not effectively hold at the observation date, observations before the relevant assignment, and observations outside the referenced performance cycle while preserving valid multiple-assignment cases and existing bitemporal correction semantics. The guard evaluates current-recorded facts, derives the date coordinate from `observed_at` in UTC so session `TimeZone` cannot alter the result, uses a trusted function search path, and adds no PII or automated employment decision authority. The Foundation PostgreSQL contract also rejects a closed `recorded_to` on each time-coordinate lookup and proves UTC midnight plus non-UTC session `TimeZone` boundaries. - Bitemporal tenant-scoped organization hierarchy validation that rejects visible indirect parent cycles and reuses single-valued recorded-time reconstruction before graph traversal. - Stacked governed job-analysis evidence contract via `JobAnalysisSnapshot`, `TaskEvidence`, `KSAORequirement`, `TaskKSAOLink`, `FunctionalJobAnalysisProfile`, and `EvidenceSource`: tenant/Job-scoped observable tasks, explicit Task-to-KSAO linkage, importance/difficulty/proficiency ratings, source/version/retrieval/SHA-256 provenance, deterministic canonical snapshot bytes, current O*NET evidence support, and historical DOT Data/People/Things compatibility. Validated snapshots require accountable human review and complete non-LLM evidence; LLM-origin material remains `analysis_draft`, and the snapshot is evidence input rather than a hiring, promotion, termination, compensation, or other high-impact employment decision. diff --git a/README.md b/README.md index 47bb087a3..8c0140a02 100644 --- a/README.md +++ b/README.md @@ -78,4 +78,6 @@ Job evidence ## Status -Protected `develop` includes the employment-truth kernel, governed candidate-to-worker conversion, purpose-bound PII authorization, normalized worker-bound validity studies, criterion-observation scope, bitemporal workforce-composition evidence, the governed Naruon intent adapter, and requisition review packets. This active PR adds durable purpose-bound People mutation and confirmed-hire materialization paths for Employment, Position, and Assignment with atomic audit/outbox evidence and tenant-scoped idempotency; treat those write paths as active-PR truth until this exact head passes all fresh protected-base gates and merges. +Protected `develop` remains the source of shipped truth and includes the employment-truth kernel, governed candidate-to-worker conversion, purpose-bound PII authorization, normalized worker-bound validity studies, criterion-observation scope, bitemporal workforce-composition evidence, the governed Naruon intent adapter, and requisition review packets. + +This **active PR only** adds the governed `orgmetra-performance-review` evidence packet described by ADR 0018. It prepares accountable pre-rating human-review evidence without calculating or persisting a rating or making an employment decision. Its issuance time is system-recorded inside the packet boundary rather than caller supplied, and independently supplied UUIDv4-shaped references are conservatively treated as potentially containing direct identifier content until authoritative reference provenance/opacity and Employment/Job scope are resolved. Do not describe this capability as shipped until this exact PR head satisfies fresh protected-base gates and merges. diff --git a/docs/adr/0018-governed-performance-review.md b/docs/adr/0018-governed-performance-review.md new file mode 100644 index 000000000..95747604a --- /dev/null +++ b/docs/adr/0018-governed-performance-review.md @@ -0,0 +1,57 @@ +# ADR 0018: Governed performance-review evidence packet + +- Status: Proposed — active PR only +- Date: 2026-08-19 + +## Context + +Orgmetra already owns authoritative Employment/Job truth and performance/criterion evidence boundaries, but a buyer-facing review workflow also needs a small pre-rating object that identifies which employment references, review period, performance cycle, criteria, goals, outcome evidence, and reviewer are being considered without copying rating values, narrative feedback, or model output into the envelope. + +A transport-neutral packet cannot prove merely from syntactically valid references that the Person, Employment, Job, cycle, goals, and observation snapshot all resolve to one authoritative temporal scope. Nor can UUIDv4 syntax prove that independently supplied bytes were randomly generated or contain no encoded identifier content. Treating either relationship resolution or reference opacity as established from syntax would create a misleading high-impact evidence boundary. Authoritative relationship/temporal resolution and trusted reference-provenance verification therefore remain required downstream steps before rating. UUIDv1 is still rejected for packet-owned namespaced references because its timestamp/node layout is unnecessary metadata for this boundary. The authoritative tenant identifier is different: it is issued by Orgmetra core, so this leaf package accepts the canonical non-sentinel operational UUID contract owned by that boundary rather than imposing a second version policy. + +System-recorded time is also audit evidence. Accepting an arbitrary caller-supplied historical `generated_at` would let a caller backdate issuance while still passing a future-only validation gate. The packet therefore owns the issuance timestamp and reads it from the host clock during construction rather than accepting it as a public constructor or builder input. + +U.S. OPM performance-management guidance treats performance management as a continuous cycle of planning, monitoring, developing, rating, and rewarding, and describes rating as evaluation against established elements and standards. ISO 30414:2025 Edition 2 provides current human-capital reporting requirements and recommendations across areas including productivity, skills/capabilities, and related workforce governance. Orgmetra uses those sources as design evidence, not as a claim that this packet by itself satisfies any jurisdiction-specific appraisal rule or ISO certification requirement. + +## Decision + +Introduce a transport-neutral `PerformanceReviewPacket` that remains pre-rating governance evidence. + +The packet MUST bind: + +- a canonical non-sentinel tenant identity under Orgmetra's authoritative operational UUID contract; +- canonical non-sentinel UUIDv4-shaped namespaced Person, Employment, Job, performance-cycle and performance-review references, rejecting UUIDv1 and other non-v4 suffixes without claiming that UUIDv4 syntax proves opacity; +- a governed criterion-set UUIDv4-shaped reference plus independent SHA-256 digest; +- a governed performance-goal-plan UUIDv4-shaped reference plus independent SHA-256 digest; +- an exact criterion-observation-snapshot UUIDv4-shaped reference plus independent SHA-256 digest; +- an optional development-plan UUIDv4-shaped reference/digest pair; +- explicit business review-period dates; +- one accountable UUIDv4-shaped reviewer, fixed `performance_review` purpose, and a reviewed closed reason code; +- a **system-owned** precision-preserving issuance timestamp read from the host clock inside the packet boundary, with no caller-supplied `generated_at` parameter; and +- a bounded positive integer `evidence_version`, defaulting to `1`, that is included in canonical evidence and therefore changes the packet digest when the governed evidence version changes. + +The initial closed reason vocabulary contains only `scheduled_cycle_review`. Arbitrary lower-snake-case values are rejected even when syntactically well formed, because free-form reason text can encode a person name, identifier, or unreviewed decision context. Additional reasons require an explicit governed contract change and regression evidence before they can enter canonical review evidence. + +`evidence_version` accepts only real integers from `1` through `2147483647`; booleans, text, zero, negative values, and overflow values fail closed. The field versions the immutable review evidence envelope and does not itself prove source-version resolution, human approval, or rating completion. + +Because this package cannot prove independently supplied reference provenance, `contains_personal_data` and `contains_direct_person_identifiers` are both fixed to `True`. The latter is deliberately conservative: it means the envelope must be handled as potentially containing direct identifier content until an authoritative issuer/resolver verifies opacity. The packet MUST NOT carry a rating value, free-form feedback, or free-form model output. Those exclusions are machine-verifiable canonical evidence through fixed `contains_rating_value=False`, `contains_free_form_feedback=False`, and `contains_free_form_model_output=False`; mutation-by-copy that attempts to weaken any exclusion fails closed. Direct construction and mutation-by-copy MUST also fail closed unless `human_confirmation_required=True`, `decision_authority="human_review_only"`, `review_state="requires_human_review"`, and `scope_verification_state="requires_authoritative_resolution"` remain intact. + +`scope_verification_state` deliberately cannot be changed to `verified` inside this package. Before rating, the authoritative HRIS/performance boundary must verify reference provenance and opacity, then resolve the Person↔Employment↔Job relation, performance-cycle/review-period alignment, and governed evidence scope using current temporal truth and purpose-bound authorization. UUIDv4 shape alone is not provenance evidence; tenant UUID generation/version/privacy policy likewise remains owned by the authoritative HRIS boundary. + +`generated_at` is constructed from a trusted internal clock adapter. The resulting exact built-in timezone-aware `datetime` is normalized once to UTC and then sealed; future instants, missing/raising offsets, normalization overflow, datetime subclasses, or post-construction non-UTC reinjection fail closed. Tests may replace the internal clock adapter to make canonical bytes deterministic, but production callers cannot provide the issuance timestamp. + +The process-local HMAC issuance seal is registered exactly once for each live packet identity. A separate weak live-issued-identity registry persists for the lifetime of the packet even if current seal bytes are discarded, so seal loss is a fail-closed export condition rather than permission to issue again. Re-entering `__post_init__()` on the same live object after seal loss or after a valid-value rewrite MUST fail before replacement evidence can be installed. Canonical export continues to compare the current deterministic bytes against the original external seal when that seal exists. This registry is in-process mutation defense only and is not a portable signature, distributed uniqueness service, authorization record, or durable audit/outbox substitute. + +Canonical JSON and SHA-256 are immutable correlation evidence only. They do not prove the correctness of source evidence, authoritative cross-record scope, substantive validity or fairness of a criterion, lawful use, human completion, reference provenance, or the final rating. + +## Consequences + +Buyers can present a review-ready correlation envelope while keeping authoritative Employment/Job and performance evidence separable from the later human rating/feedback event. The envelope itself now carries explicit canonical proof that rating values, free-form feedback, and free-form model output are excluded from this pre-rating evidence boundary. A consumer cannot truthfully treat the packet itself as proof that all referenced records belong to the same employee/job/cycle or that UUIDv4-shaped values are opaque. Until authoritative provenance verification occurs, the packet receives the more restrictive identifier-risk classification rather than a false no-direct-identifier assertion. + +The system-recorded timestamp can no longer be backdated through public packet construction, and one live packet cannot renew its process-local issuance evidence after mutation or after losing its current seal bytes. Hosts still own purpose-bound authorization, durable immutable audit/outbox, retention/export controls, and the authoritative clock/runtime environment. + +This slice adds no database migration, no rating computation, no cross-service table access, and no automated employment decision. The pre-rating packet preserves actor, purpose, reviewed reason, evidence version, conservative identifier-risk classification, system-recorded issuance time, and explicit content-exclusion flags in its immutable correlation evidence; later authoritative rating persistence must independently preserve those values plus human confirmation, audit/outbox, temporal scope, authoritative scope/provenance resolution evidence, and any applicable policy requirements. + +## References + +See `docs/doctoring/performance-review-references.md`. diff --git a/docs/adr/README.md b/docs/adr/README.md index 099a21139..6046af7cd 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -16,3 +16,4 @@ | [0012](0012-governed-migration-handoff.md) | Governed migration handoff | Accepted on active implementation branch | | [0013](0013-governed-requisition-review-packet.md) | Governed requisition review packet | Accepted on active implementation branch | | [0014](0014-job-analysis-snapshot-persistence.md) | Persist governed job-analysis snapshots | Accepted on active implementation branch | +| [0018](0018-governed-performance-review.md) | Governed performance-review evidence packet | Proposed — active PR only | diff --git a/docs/doctoring/performance-review-references.md b/docs/doctoring/performance-review-references.md new file mode 100644 index 000000000..0e40e4a15 --- /dev/null +++ b/docs/doctoring/performance-review-references.md @@ -0,0 +1,9 @@ +# Performance-review references + +Retrieved August 19, 2026. These references support the active-PR governance boundary in ADR 0018. Orgmetra does not reproduce proprietary ISO text or claim certification. + +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. Office of Personnel Management. (n.d.). *Performance management cycle*. Retrieved August 19, 2026, from https://www.opm.gov/policy-data-oversight/performance-management/performance-management-cycle/ + +U.S. Office of Personnel Management. (n.d.). *Performance management roadmap: Best practices guide for supervisors*. Retrieved August 19, 2026, from https://www.opm.gov/policy-data-oversight/performance-management/performance-management-toolkit/best-practices/performance-management-roadmap-for-supervisors/ diff --git a/docs/traceability/performance-review.md b/docs/traceability/performance-review.md new file mode 100644 index 000000000..f16c4cfef --- /dev/null +++ b/docs/traceability/performance-review.md @@ -0,0 +1,28 @@ +# Performance review traceability + +Status: **active PR / proposed capability**, not protected-main truth. + +| Requirement | Evidence | Status | +|---|---|---| +| Correlate review with Employment and Job references without claiming relationship resolution | `PerformanceReviewPacket.employment_record_reference`, `job_profile_reference`, fixed `scope_verification_state=requires_authoritative_resolution` | Implemented on active PR | +| Preserve authoritative tenant interoperability without treating UUID syntax as opacity proof | `test_authoritative_uuid7_tenant_identity_is_accepted_by_builder_and_replace`; `test_rejects_uuid1_trust_references_through_builder_and_replace`; `test_unverified_uuidv4_reference_is_classified_as_potential_direct_identifier` | `tenant_record_id` follows the canonical non-sentinel Orgmetra core operational-UUID contract; namespaced packet references require canonical non-sentinel UUIDv4 shape and reject UUIDv1/non-v4 suffixes, while independently supplied references remain conservatively classified as potentially containing direct person identifier content until trusted provenance verifies opacity. | +| Require authoritative reference provenance plus Person↔Employment↔Job/cycle/evidence resolution before rating | immutable scope-verification state, conservative identifier-risk flag, and governed `next_action` | Enforced as downstream prerequisite on active PR | +| Bind exact performance-cycle and business review period | `performance_cycle_reference`, `review_period_start`, `review_period_end` | Implemented on active PR | +| Bind predetermined criteria and goals | `criterion_set_reference`/digest, `goal_plan_reference`/digest | Implemented on active PR | +| Bind exact outcome evidence without copying values | `criterion_observation_snapshot_reference`/digest | Implemented on active PR | +| Preserve optional development provenance | paired `development_plan_reference`/digest | Implemented on active PR | +| Reject caller-defined digest string subclasses at every evidence boundary | `_validate_digest` requires exact built-in `str`; `test_rejects_digest_string_subclass_at_all_digest_boundaries` covers criteria, goal-plan, criterion-observation, and development-plan digests | Implemented on active PR | +| Avoid unsupported no-direct-identifier claims for independently supplied references | `contains_personal_data=True`, `contains_direct_person_identifiers=True`; UUIDv4-shaped encoded-person regression | Implemented conservatively until authoritative issuer/resolver provenance is verified | +| Exclude rating values and free-form feedback/model output | immutable `contains_rating_value=False`, `contains_free_form_feedback=False`, and `contains_free_form_model_output=False`; no rating/narrative fields; `test_feedback_privacy.py` proves the feedback flag is canonicalized and cannot be weakened by dataclass replacement | Implemented on active PR | +| Require accountable human review | fixed `human_confirmation_required=True`, `decision_authority=human_review_only`, `review_state=requires_human_review` | Implemented on active PR | +| Version high-impact review evidence | bounded positive `evidence_version` is validated, serialized in canonical JSON, and changes SHA-256 correlation evidence | Implemented on active PR | +| Make system-recorded issuance chronology non-caller-controlled | public builder/dataclass signatures exclude `generated_at`; `__post_init__` reads `_system_recorded_at()` and freezes the result to UTC before sealing | `test_system_recorded_issuance_time_is_not_caller_supplied`; `test_system_recorded_timestamp_is_bound_to_canonical_evidence` | +| Preserve deterministic system-recorded chronology despite hostile clock/tz behavior | trusted clock result is resolved once to a built-in UTC instant; future instants, datetime subclasses, missing/raising offsets and normalization overflow fail closed; later canonical export accepts only that frozen UTC shape | `test_temporal_evidence_integrity.py` exercises the internal clock adapter, mutable/raising/missing timezone providers, future time, overflow, and post-construction non-UTC reinjection | +| Prevent a second canonical truth after issuance | Process-local HMAC issuance evidence is stored outside packet-writable slots over exact construction-time canonical JSON; a separate weak live-issued-identity registry makes issuance single-use for the live object's lifetime even if seal bytes are discarded, so valid-value mutation plus direct `__post_init__()` cannot renew evidence; export fails closed after mutation or missing seal state | `test_issuance_integrity.py`, including `test_reinitialization_cannot_renew_issuance_evidence_after_valid_value_rewrite` and `test_seal_loss_cannot_reset_live_issuance_lifecycle`; defense-in-depth only, not durable cross-process authorization/persistence | +| Preserve deterministic immutable correlation evidence | issuance-verified canonical JSON plus SHA-256 | Implemented on active PR | +| Exact 100% owned statement/branch coverage | `packages/performance-review/pyproject.toml`, `.github/workflows/performance-review-quality.yml` | Required on exact PR head | +| Keep package-quality evidence current after shared repository test/runtime configuration changes | `test_quality_workflow_retriggers_on_shared_test_configuration` requires the package workflow to retrigger on shared Python/test/clean-checkout configuration as well as package-owned paths | Implemented on active PR; supplements rather than replaces central required workflows | +| Canonical documentation discoverability | ADR 0018 is indexed; root README/CHANGELOG identify the capability as active-PR truth | Required before review completion | +| Standards/research basis | ADR 0018; `docs/doctoring/performance-review-references.md` | Documented on active PR | + +The packet does not persist or calculate a rating, carry free-form feedback, decide compensation, infer performance, prove cross-record scope consistency, prove reference opacity, or prove scientific validity/fairness/compliance. UUIDv4 is only a constrained namespaced identifier shape for packet-owned trust references; reference provenance/opacity and tenant UUID generation/version/privacy policy remain owned by authoritative HRIS boundaries. The process-local issuance registry is deliberately not a distributed attestation store: durable uniqueness, authorization, retention, and immutable audit/outbox remain responsibilities of authoritative Orgmetra host/persistence boundaries. Those claims require their own authoritative evidence and controls. diff --git a/manifest.json b/manifest.json index 97f2bab14..49a3e37fa 100644 --- a/manifest.json +++ b/manifest.json @@ -1 +1 @@ -{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"32cc4ef78d1eca557fa01731026840be01211a043eb0ada552e4e6cb9eace353","bytes":17295,"lines":76},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"1a9fc400d26d8137ae5911488794a6d3fa915957c95f27b36a48cef0fdf823c6","bytes":3785,"lines":81},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"f3b3b5ed3b3b31a40a0a3696abf0065e3c25879b6be50077f38ffae742b9d002","bytes":1838,"lines":18},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} +{"package":"orgmetra-foundation-pack","version":"0.1.0","generated_for_branch":"feat/audit-outbox-envelope","files":[{"path":".github/workflows/foundation-ci.yml","sha256":"12686a3bbd6445e6fdb202b4137dae118ddeeab1efb0c7f18ea6c8fa19d62537","bytes":4379,"lines":123},{"path":".github/workflows/job-analysis-api-quality.yml","sha256":"352dc78931dd94afea3e88912d38dcc4b562a004112f199f3d7a12d22b6d637a","bytes":4159,"lines":105},{"path":".gitignore","sha256":"145fda644f5209fa1fb3e3b40c9af9258bfac6d1a634bba2520fd08fe6d77a21","bytes":375,"lines":37},{"path":"AGENTS.md","sha256":"28f7b7bc010a7739cfdc3e793fb5d39a0e74b842ea9c190e9a251e2d0cbc3a16","bytes":2246,"lines":34},{"path":"ARCHITECTURE.md","sha256":"52d68786f7359c1a50d804996021e4c70e90accd2fff6f1a27c91de1dd8df850","bytes":7864,"lines":107},{"path":"CHANGELOG.md","sha256":"df558724edae8575b6cb0a026abfac857758460ea0b1a7370025b28e4d2c9cb5","bytes":17819,"lines":77},{"path":"CLAUDE.md","sha256":"add33884f466d324e20875388d103de41c6e062938a6e98727dc83a87ffe976f","bytes":1229,"lines":20},{"path":"LICENSE","sha256":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30","bytes":11358,"lines":202},{"path":"NOTICE","sha256":"34b4618e946bdd8d33407d6ac5279f0a0388f5e7c8f79d2e7d8c3c47d0266042","bytes":305,"lines":4},{"path":"README.md","sha256":"19582a342f13f44293581846d426ba6685618467bfdc2f43c993b95c08e86d61","bytes":4180,"lines":83},{"path":"database/migrations/0001_foundation_schema.sql","sha256":"ce2ae52fc66b2f99597ea5285df82c66f90caa46174fef4930d68a8b6177d0dd","bytes":38747,"lines":916},{"path":"database/migrations/0002_sealed_evidence_digest.sql","sha256":"93d659ca8e0e9293a83d5422d043be7b1022c5470a5b22670aa3416fa334a04c","bytes":6649,"lines":202},{"path":"database/migrations/0003_audit_outbox_persistence.sql","sha256":"2aa7bbb8220923ec584537c0cd46f0cba2b692d69d431f097b7df6db75235bfc","bytes":15417,"lines":423},{"path":"database/migrations/0004_outbox_delivery_claim.sql","sha256":"d4504acf7d58528a2a8f4f03d1584b868c8d3ba9046a007b9c2e7cfef993b2ef","bytes":9451,"lines":234},{"path":"database/migrations/0005_outbox_delivery_finalization.sql","sha256":"b7e8790595b288f752d6ef5cc6cbfe4e1b6712248f5b7a3a25fa60016b6a4961","bytes":6125,"lines":170},{"path":"database/migrations/0006_outbox_delivery_dead_letter.sql","sha256":"c1fb91cdf98169fd6684984e86cb0a14fa19c8f1226028d2346a2a069df2b3c7","bytes":24919,"lines":628},{"path":"database/migrations/0007_outbox_retry_exhaustion.sql","sha256":"812f50d70ca5929c7eba964d34a208aedee660d11cc7ffc09d67688c4737e0d5","bytes":19081,"lines":476},{"path":"database/migrations/0008_audit_outbox_review_hardening.sql","sha256":"c3713a12db9d00fdc10005df1f86c07965e9555eefad78ca67e994537a739d9b","bytes":17562,"lines":448},{"path":"database/migrations/0009_candidate_worker_conversion_governance.sql","sha256":"4030666629a6b8deb383b8337ead4f09d6a945969313def2577a38f31f06cda9","bytes":11537,"lines":281},{"path":"database/migrations/0010_validity_study_case_integrity.sql","sha256":"3f594810ac9e1a6747a2bb4838e5ce65b921cb6e3d36fcdc3ff08b4a7579ebd1","bytes":11979,"lines":313},{"path":"database/migrations/0011_criterion_observation_scope.sql","sha256":"f9fe7c35f1ee7b167e1c2ba75a50a84febda9a6ccf8123b4f5726f51968694f9","bytes":7444,"lines":165},{"path":"database/migrations/0012_people_mutation_idempotency.sql","sha256":"52dbbb9ec7f9be5291593ba88f228d7fffd736dcb99547a08c1d6cad076afb69","bytes":3162,"lines":76},{"path":"database/migrations/0013_job_analysis_snapshot.sql","sha256":"b6553a5a4c94c4aa9f341a474e13bbe34db63044eda2446b3ebee178995977ee","bytes":12713,"lines":260},{"path":"docs/API_CONTRACT.md","sha256":"63533dff785da62b89e585d742a158e2aeb05913644f2bf9fb6486f281c2e589","bytes":4555,"lines":76},{"path":"docs/DATA_MODEL.md","sha256":"6ad29731ae7ee7aa5bf3a2d0bfef88894a35a2550edb2be3244d6f143d76444a","bytes":13366,"lines":85},{"path":"docs/ERD.md","sha256":"546001aa85c4fe020e0c39d881dc860daf7f69090596666fdf9092487b0725fe","bytes":6964,"lines":70},{"path":"docs/OPERABILITY.md","sha256":"82b2d3e70cec371ef35e9e0f982ac40fef84351976bc04b863b81d27023d5a62","bytes":11189,"lines":71},{"path":"docs/PRD.md","sha256":"3ad85ae633cce0fc7a93af39b21d7a7c70bb2efa786da6b12f3c5327906e34f1","bytes":5490,"lines":111},{"path":"docs/SECURITY.md","sha256":"01918512d8882060e9cff0c4aa8206e0eccbdfb61cfd7f829331123c7a9fe6ac","bytes":11185,"lines":64},{"path":"docs/STORYBOARD.md","sha256":"6e4ffb0eb03a80343f50d363ffc43b34da9348a44232dd947a9ff416ea92a3d2","bytes":1342,"lines":28},{"path":"docs/STORYBOOK.md","sha256":"82f79029b3c2b7a45393bad5ba8fabe61014d4b6149c7d4e73f70ba447f885e9","bytes":1389,"lines":50},{"path":"docs/TEST_STRATEGY.md","sha256":"d0a0bc3b54ed0fc7973747987f1afb117d6144c390b51ed9370eb571972a33f8","bytes":16534,"lines":135},{"path":"docs/THREAT_MODEL.md","sha256":"f314f375c2e41252536de224c7bc7e4a10ab8f340cb86642724e7399e32f4252","bytes":6736,"lines":23},{"path":"docs/TRACEABILITY.md","sha256":"dbf6fd91375ea28e05456d2a0c9ba629506cbac6f52f5dfda61ae68db2395f7e","bytes":11462,"lines":40},{"path":"docs/TRD.md","sha256":"23697d88a4882698e1a2782b7da3f2ccd0d3cd2d6d1bffe89b6597dc16851077","bytes":9064,"lines":101},{"path":"docs/UML.md","sha256":"fe67c37aa88e5814ceb2db7e8f7d8d85ca27a994802efbb7c75164b387adf0a9","bytes":5528,"lines":122},{"path":"docs/USER_STORIES.md","sha256":"5535b39d8c71a36c81f78e2d6dbd90a2d32e6541790f0d28f6dd4baf3ea7b45f","bytes":2670,"lines":37},{"path":"docs/WIREFRAMES.md","sha256":"b03aa6419aeaf5d42a5698c4d43a434c1633b7ac6fd0b0bd0cda979077adc56e","bytes":2005,"lines":77},{"path":"docs/adr/0001-orgmetra-authoritative-hris-record.md","sha256":"0f8055b73c63d3130321415ad53233588ff952aabd1a88952b39c71747253572","bytes":6108,"lines":53},{"path":"docs/adr/0002-federated-cwl-integration-boundaries.md","sha256":"b77165f2aacfa6f4fde994baf77d5879c6da3e8dae4fd2db0ed912d60ae9b3b2","bytes":4072,"lines":44},{"path":"docs/adr/0003-bitemporal-hris-data-contract.md","sha256":"d7f2660616622c1a7994b28aa66d99d13836bcf755735595f9609a41282ab799","bytes":4453,"lines":47},{"path":"docs/adr/0004-employment-position-version-and-assignment-binding.md","sha256":"fee89e700414abe0b1cffec2acc687e5e014634db8f5ef9e8a92abba5c3cf182","bytes":1872,"lines":30},{"path":"docs/adr/0005-exclusive-employment-and-staffable-seats.md","sha256":"10f0eb409f4fa32d2c5bed2d583d8b43be8e61b5cbef0e927e5bebb5f5c8f85b","bytes":2091,"lines":34},{"path":"docs/adr/0006-governed-audit-outbox-envelope.md","sha256":"827298ddd997b47f78a89e89911ad8ea72e517b7714303637f0329b8cb52cabd","bytes":14100,"lines":66},{"path":"docs/adr/0007-governed-job-analysis-evidence.md","sha256":"953c6d2b9864a78b461b576092ec3f198f0b76709eaaaf7d0ed0182f95182c52","bytes":5653,"lines":57},{"path":"docs/adr/0008-purpose-bound-pii-authorization.md","sha256":"c5157d3bc58f3d8d29e03104dd15eb2911cc1bb66e2c92a935b26d7164648dc7","bytes":5988,"lines":55},{"path":"docs/adr/0009-performance-criterion-observation-scope.md","sha256":"1ac10bb2747b0a5b4d62f627825cfd7f978f3fa88d7575bffc23d56371240a64","bytes":7057,"lines":57},{"path":"docs/adr/0010-naruon-calendar-intent-boundary.md","sha256":"3e1050a964cc4ed76a1a0cf1e699ae5080acf8c9336f0decdd6d5229359db3c9","bytes":3917,"lines":35},{"path":"docs/adr/0011-bitemporal-workforce-composition.md","sha256":"1656ef8b57c836ef7936a8e9cb6a824681eb7563157a1ab0a29deb25849a457b","bytes":5568,"lines":53},{"path":"docs/adr/0012-governed-migration-handoff.md","sha256":"713855d670001d3964ecb36cc653830502fb1d82a58b9e39f564b6992dd2bd80","bytes":5965,"lines":59},{"path":"docs/adr/0013-governed-requisition-review-packet.md","sha256":"70bf2cbdf903a8793d6d8bc116a08331931090118341f42010236e09c6cc1802","bytes":4693,"lines":46},{"path":"docs/adr/0014-job-analysis-snapshot-persistence.md","sha256":"a7ab6fee50aaa63f7f407516a4cb39885faeb0fc6e5035ee8fc352ed73430105","bytes":5365,"lines":49},{"path":"docs/adr/README.md","sha256":"445aa6d59495b0387e189c7e4ea992e8f871052130fff99e191bcf1db02520aa","bytes":1962,"lines":19},{"path":"docs/doctoring/REFERENCES.md","sha256":"929f7ee36df16279f028f726fcf039982180deb377746fe3804f3c0d090778d5","bytes":6352,"lines":69},{"path":"docs/superpowers/plans/2026-08-15-orgmetra-foundation-implementation-plan.md","sha256":"b64f21abb19373e780db8b9e64deb8ba9a6219ccf9625a651f25407b8691fcbd","bytes":8227,"lines":226},{"path":"docs/superpowers/specs/2026-08-15-orgmetra-foundation-design.md","sha256":"4a0e1a7943e40d12bd3082db3757045b4085e5a089fea7bc0d8a1565ffcbcf1d","bytes":6237,"lines":187},{"path":"package.json","sha256":"59ae9e3e67c3fba9320cb18439692395cdfd16ae5c24e3c4cf30d77d63ebabb5","bytes":388,"lines":9},{"path":"packages/hris-kernel/src/orgmetra_hris_kernel/audit.py","sha256":"3e5b7190cf857dc8c1fc7e898cef303060f34aabee6c27a9034d4d9650e33190","bytes":7707,"lines":160},{"path":"packages/hris-kernel/tests/test_audit_outbox.py","sha256":"5928dd7b97fe38d6b7472ce62966437e339058a59c3b301a93a7b5c05432b40c","bytes":7556,"lines":200},{"path":"schemas/openapi.yaml","sha256":"09c1e43486779198574fe31b8bcabbd1c1f74beec7bf86245ae578061619838f","bytes":29503,"lines":1020},{"path":"scripts/foundation-contract-core.mjs","sha256":"595e8381dbd62e97093b11eef818af5f04d6473ac592d57e3985ffbc2210d445","bytes":28173,"lines":689},{"path":"scripts/foundation-contract.mjs","sha256":"5242dcdbe0935775edf074462c82600e9bc4927d9fdc50c47727af915fd4b23a","bytes":218,"lines":6},{"path":"tests/dispatcher-inventory.test.mjs","sha256":"09f5e64410e6b7a26bf8d6ce61c50b737da2ea85d955f91eba63aa21f1537261","bytes":1597,"lines":34},{"path":"tests/foundation-contract.test.mjs","sha256":"960306fd7cda7b982a52c4428a432d10a4f570430a5d39fb23aeca0b2ede0615","bytes":14860,"lines":386},{"path":"tests/openapi-contract.test.mjs","sha256":"80c1610ef1c189fa325e55389501e0e51531ddf61ee335bb94d9cb3aa55a9fdc","bytes":6438,"lines":195},{"path":"tests/test_audit_outbox_hardening_postgres.sh","sha256":"518ba2f37ba6292943e5abe22c2599452b2f031a42e453b2493aedf8714421a0","bytes":13396,"lines":333},{"path":"tests/test_audit_outbox_postgres.sh","sha256":"e57a04920a0ba97fa6a06752d15ea150016ab8d44099e998c5c4f4067592b4d2","bytes":13443,"lines":357},{"path":"tests/test_bitemporal_postgres.sh","sha256":"7684b8c2ff52c044c081135515bd5aabbfd00e2daad0d471b0868701af2df6cc","bytes":8209,"lines":230},{"path":"tests/test_candidate_worker_conversion_postgres.sh","sha256":"681cb74d6cfa859ed92c6c2439881ea20c430ef8df94ec662e2807761a377f90","bytes":14673,"lines":344},{"path":"tests/test_criterion_observation_scope_postgres.sh","sha256":"0ee9539ee57f840c27d08009f7868cdc8662669df78a01dbc8be39216b8f1a3d","bytes":17811,"lines":469},{"path":"tests/test_evidence_sealing_postgres.sh","sha256":"57d16b632a0c60ffdcb4842ceb1cfe25d19c54cefeeefb622ff4fa6e83441ad7","bytes":11349,"lines":370},{"path":"tests/test_job_analysis_snapshot_postgres.sh","sha256":"ca9c323a1dd68cfc520277efbbb7495e37fb3ca027890928c8624e5b4f57403f","bytes":13542,"lines":296},{"path":"tests/test_operational_uuid_postgres.sh","sha256":"7378f98f0d4b3000e8ea641d8701f1540dbad71410b3637d81d799969e0f6ff7","bytes":3346,"lines":101},{"path":"tests/test_outbox_claim_postgres.sh","sha256":"1027806d436ebfe34e108c25b6a4001f43b9550f1d70057c6c0d7974323b0c9b","bytes":14817,"lines":429},{"path":"tests/test_outbox_dead_letter_postgres.sh","sha256":"0d728d578e64252e6079f2d141ddaa7fa9cfbf9784e625832273596d69a6e13d","bytes":14008,"lines":377},{"path":"tests/test_people_mutation_idempotency_postgres.sh","sha256":"3f57e12f80bd1b034c9aac54b669d8530106e3e26b3795689671fb53807b3cd5","bytes":16191,"lines":381},{"path":"tests/test_tenant_isolation_postgres.sh","sha256":"dd649435ef8ab9e57f0609c101917e36656a6d40d63de9bcdbdac23d764f6c3a","bytes":15134,"lines":388},{"path":"tests/test_validity_study_case_postgres.sh","sha256":"0070ad58300323c7f9900c5645e0df3106b36ccd245ae686e982c2fd6fa4dc02","bytes":14708,"lines":301},{"path":"tests/validate_repository.py","sha256":"918cf92fd18d81572e9bd5f5daa7f033c32731e2e13f0d00661d1c1de30b12a9","bytes":27291,"lines":638}]} diff --git a/packages/performance-review/CHANGELOG.md b/packages/performance-review/CHANGELOG.md new file mode 100644 index 000000000..d0f45c439 --- /dev/null +++ b/packages/performance-review/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## Unreleased + +- Add a value-minimized, human-review-only performance-review evidence packet binding Employment/Job references while requiring downstream authoritative scope resolution before rating, together with performance cycle, criteria, goals, outcome evidence, optional development-plan provenance, and an accountable reviewer. +- Follow Orgmetra's authoritative canonical non-sentinel operational UUID contract for `tenant_record_id`, while namespaced packet-owned trust references require canonical non-sentinel UUIDv4-shaped values and reject UUIDv1/non-v4 suffixes. UUIDv4 syntax is no longer described as proof of opacity: until trusted issuer/resolver provenance verifies otherwise, independently supplied references are conservatively classified as potentially containing direct person identifier content. +- Restrict `reason_code` to the reviewed closed vocabulary (`scheduled_cycle_review`) so arbitrary lower-snake-case text cannot carry PII or ungoverned decision context into canonical evidence. +- Require exact built-in `str` values for every SHA-256 evidence digest before pattern validation and canonical binding, matching the strict runtime contract used by the other trust-bearing text fields. +- Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation evidence so high-impact review evidence versions are explicit and fail closed on invalid values. +- Make `generated_at` system-owned rather than caller-supplied: packet construction reads the trusted host clock, freezes it to a detached built-in UTC instant, rejects future/invalid clock results, and prevents callers from falsifying audit chronology by submitting arbitrary historical issuance times. +- Make the existing free-form-feedback exclusion machine-verifiable with immutable `contains_free_form_feedback=False` canonical evidence and fail-closed replacement validation, alongside the existing no-rating-value and no-free-form-model-output controls. +- Bind each live issued performance-review packet exactly once to its construction-time canonical bytes with a process-local HMAC seal stored outside packet-writable slots plus a weak live-issued-identity registry. Canonical export fails closed after valid-value rewrites or missing seal bytes, while seal loss does not reset the live issuance lifecycle and direct reinitialization cannot mint replacement evidence; durable uniqueness, authorization, and immutable audit/outbox remain authoritative host/persistence responsibilities. +- Make `Performance Review Quality` retrigger on shared repository Python/test/clean-checkout configuration and enforce that trigger surface with an executable regression so package-quality evidence cannot remain stale after shared tooling changes. diff --git a/packages/performance-review/README.md b/packages/performance-review/README.md new file mode 100644 index 000000000..edfb540a7 --- /dev/null +++ b/packages/performance-review/README.md @@ -0,0 +1,21 @@ +# Orgmetra Performance Review + +`orgmetra-performance-review` provides a small, transport-neutral evidence packet for preparing an accountable human performance review without copying rating values, free-form feedback, or model output into the governance envelope. + +The packet follows Orgmetra's authoritative canonical non-sentinel operational UUID contract for `tenant_record_id` and correlates Person and Employment references with a Job, performance cycle, governed criterion set, goal plan, exact criterion-observation snapshot, optional development plan, and reviewer. Packet-owned trust-bearing references require canonical non-sentinel UUIDv4-shaped namespaced values and reject UUIDv1/non-v4 suffixes so UUIDv1 timestamp/node metadata cannot enter these fields. **UUIDv4 syntax does not prove that independently supplied bytes are random, opaque, or free of encoded identifier content.** Until an authoritative issuer/resolver verifies reference provenance and opacity, the packet conservatively records `contains_direct_person_identifiers=True` and requires purpose-bound handling. The packet also does not assert that independently supplied references already resolve to one authoritative employment/performance scope: `scope_verification_state` remains `requires_authoritative_resolution` until the authoritative HRIS/performance boundary resolves that relationship before rating. + +Worker references are sensitive correlating metadata and are treated as potentially containing direct identifier content until trusted provenance verifies otherwise. Hosts must enforce purpose-bound authorization, least privilege, retention/export controls, encryption where appropriate, and immutable audit evidence around packet access. `reason_code` is not free-form metadata: the current reviewed vocabulary accepts only `scheduled_cycle_review`. New business reasons must be introduced through an explicit governed contract change rather than encoded into arbitrary lower-snake-case strings, preventing names, identifiers, or other unreviewed context from entering canonical evidence. + +Every packet carries a bounded positive integer `evidence_version` (default `1`). The version is part of canonical JSON and therefore changes the SHA-256 correlation digest when the reviewed evidence contract/version changes. Zero, negative, boolean, textual, and values above `2147483647` fail closed. The version identifies the review evidence envelope; it is not a rating, approval, or substitute for authoritative source-version verification. + +`generated_at` is system-recorded issuance evidence and is **not a caller argument**. Construction reads the host clock inside the packet boundary, resolves a concrete timezone offset once, converts the instant to a built-in UTC `datetime`, rejects future instants, and stores only that detached UTC value. Canonical evidence export therefore does not re-enter caller input or a mutable `tzinfo`; invalid clock adapters, missing offsets, provider exceptions, UTC-normalization overflow, datetime subclasses, and post-construction reinjection of a non-UTC timestamp fail closed before evidence emission. Tests replace the internal clock adapter only to make exact canonical bytes deterministic; production callers cannot supply `generated_at`. + +A frozen dataclass alone is not an issuance seal because low-level Python mutation can still rewrite otherwise valid fields. Each live issued packet is therefore bound exactly once to its construction-time canonical JSON by a process-local HMAC seal stored outside packet-writable slots. The runtime keeps a separate weak live-issued-identity registry from the current seal bytes: losing or deliberately discarding the seal makes export fail closed but does **not** reset that live object's issuance lifecycle. A second `__post_init__()` therefore cannot mint replacement evidence after seal loss or after a valid-value rewrite. `canonical_json()` snapshots the current canonical bytes once, verifies that exact snapshot against the external issuance seal, and returns the verified bytes without rereading packet fields. A valid-value rewrite, attempted reinitialization/reseal, or missing process-local issuance evidence fails closed. This is in-process defense-in-depth only; durable cross-process uniqueness, purpose authorization, and immutable audit/outbox remain responsibilities of authoritative Orgmetra host or persistence boundaries. + +## What this packet does not do + +It does not calculate or persist a rating, write narrative feedback, infer performance, make an employment decision, modify compensation, execute a development action, prove reference opacity by UUID syntax, or prove cross-record scope consistency. It does not replace the authoritative performance/criterion persistence boundary. Tenant UUID generation/version/privacy policy and trust-bearing reference provenance remain owned by authoritative host boundaries. Canonical JSON and SHA-256 provide correlation integrity only; they do not prove fairness, scientific validity, legal compliance, authoritative scope resolution, reference provenance, or that a human review actually occurred. + +## Required review state + +Every packet remains `requires_human_review`, with `decision_authority="human_review_only"`, `human_confirmation_required=True`, and `scope_verification_state="requires_authoritative_resolution"`. The fixed next action tells the reviewer to verify authoritative reference provenance and opacity, Employment/Job scope, review-period and performance-cycle alignment, governed criteria and goals, criterion-observation evidence, and any development-plan provenance before recording accountable human rating and feedback through the authoritative performance workflow. diff --git a/packages/performance-review/pyproject.toml b/packages/performance-review/pyproject.toml new file mode 100644 index 000000000..2366bdde5 --- /dev/null +++ b/packages/performance-review/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-performance-review" +version = "0.1.0" +description = "Governed human performance-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_performance_review", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/performance-review/src/orgmetra_performance_review/__init__.py b/packages/performance-review/src/orgmetra_performance_review/__init__.py new file mode 100644 index 000000000..249d74eb9 --- /dev/null +++ b/packages/performance-review/src/orgmetra_performance_review/__init__.py @@ -0,0 +1,5 @@ +"""Public governed performance-review contract.""" + +from .packet import PerformanceReviewPacket, build_performance_review_packet + +__all__ = ["PerformanceReviewPacket", "build_performance_review_packet"] diff --git a/packages/performance-review/src/orgmetra_performance_review/packet.py b/packages/performance-review/src/orgmetra_performance_review/packet.py new file mode 100644 index 000000000..1586b626d --- /dev/null +++ b/packages/performance-review/src/orgmetra_performance_review/packet.py @@ -0,0 +1,377 @@ +"""Governed, value-minimized human performance-review evidence. + +The packet correlates one proposed employee review to Employment and Job references, +a performance cycle, predetermined criteria and goals, an exact criterion-observation +snapshot, an optional development plan, and an accountable human reviewer. It does not +assert that those references resolve to one authoritative scope or that caller-supplied +UUIDv4-shaped references have already been proven opaque; that verification must occur +at the authoritative HRIS/performance boundary before rating. References are therefore +personal data and are conservatively classified as potentially containing direct person +identifier content until trusted provenance resolves them. Rating values, free-form +feedback, and free-form model output remain outside this envelope. +""" +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import date, datetime, timedelta, timezone +from hashlib import sha256 +import hmac +import json +import re +import secrets +from threading import RLock +from uuid import UUID +from weakref import WeakValueDictionary, finalize + +_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 = "performance_review" +_ALLOWED_REASON_CODES = frozenset({"scheduled_cycle_review"}) +_DECISION_AUTHORITY = "human_review_only" +_REVIEW_STATE = "requires_human_review" +_SCOPE_VERIFICATION_STATE = "requires_authoritative_resolution" +_NEXT_ACTION = ( + "Verify authoritative reference provenance and opacity, Employment/Job scope, " + "performance-cycle dates, governed criteria and goals, criterion-observation " + "evidence, and any development-plan provenance; then record accountable human " + "rating and feedback through the authoritative performance workflow." +) +_PROCESS_PACKET_SEAL_KEY = secrets.token_bytes(32) +_PACKET_SEALS: dict[int, str] = {} +_ISSUED_PACKET_IDENTITIES: WeakValueDictionary[int, object] = WeakValueDictionary() +_PACKET_SEALS_LOCK = RLock() + + +def _discard_packet_seal(packet_id: int) -> None: + """Discard seal bytes without resetting the live packet's issuance lifecycle.""" + with _PACKET_SEALS_LOCK: + _PACKET_SEALS.pop(packet_id, None) + + +def _register_packet_seal(packet: object, seal: str) -> None: + """Bind one live review-packet identity exactly once outside writable slots.""" + packet_id = id(packet) + with _PACKET_SEALS_LOCK: + if _ISSUED_PACKET_IDENTITIES.get(packet_id) is packet: + raise ValueError("performance review issuance evidence already exists") + _PACKET_SEALS[packet_id] = seal + _ISSUED_PACKET_IDENTITIES[packet_id] = packet + finalize(packet, _discard_packet_seal, packet_id) + + +def _authoritative_packet_seal(packet: object) -> str | None: + """Return process-local issuance evidence without trusting packet-owned state.""" + with _PACKET_SEALS_LOCK: + return _PACKET_SEALS.get(id(packet)) + + +def _seal_packet(payload_json: str) -> str: + """Bind one process-local issuance to exact canonical performance-review bytes.""" + return hmac.new(_PROCESS_PACKET_SEAL_KEY, payload_json.encode("utf-8"), "sha256").hexdigest() + + +def _system_recorded_at() -> datetime: + """Read the trusted host clock for one packet issuance.""" + return datetime.now(timezone.utc) + + +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: + 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 an expected namespace plus a canonical UUIDv4-shaped suffix.""" + error_message = f"{field_name} must be a canonical {prefix}: UUIDv4 reference" + if ( + type(value) is not str + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(error_message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(error_message) from exc + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(error_message) + + +def _validate_digest(value: str, field_name: str) -> None: + """Require exact built-in lowercase SHA-256 hexadecimal evidence text.""" + if type(value) is not str or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _freeze_timestamp(value: datetime) -> datetime: + """Resolve the trusted clock once and store one immutable UTC instant.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError("generated_at must be an exact timezone-aware datetime") + try: + offset = value.utcoffset() + except Exception as exc: + raise ValueError("generated_at must be an exact timezone-aware datetime") from exc + if type(offset) is not timedelta: + raise ValueError("generated_at must be an exact timezone-aware datetime") + try: + frozen = (value.replace(tzinfo=None) - offset).replace(tzinfo=timezone.utc) + except (OverflowError, ValueError) as exc: + raise ValueError("generated_at must be an exact timezone-aware datetime") from exc + if frozen > datetime.now(timezone.utc): + raise ValueError("generated_at must not be in the future") + return frozen + + +def _canonical_timestamp(value: datetime) -> str: + """Render one already-frozen UTC instant as precision-preserving RFC 3339 text.""" + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError("generated_at must be an exact timezone-aware datetime") + return value.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_reason_code(value: str) -> None: + """Require exact built-in text from the closed reviewed reason vocabulary.""" + if type(value) is not str or value not in _ALLOWED_REASON_CODES: + raise ValueError("reason_code must be an authorized performance-review reason code") + + +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, weakref_slot=True) +class PerformanceReviewPacket: + """Immutable value-minimized review packet awaiting authoritative resolution.""" + + tenant_record_id: str + performance_review_reference: str + person_record_reference: str + employment_record_reference: str + job_profile_reference: str + performance_cycle_reference: str + criterion_set_reference: str + criterion_set_digest: str + goal_plan_reference: str + goal_plan_digest: str + criterion_observation_snapshot_reference: str + criterion_observation_snapshot_digest: str + development_plan_reference: str | None + development_plan_digest: str | None + reviewer_reference: str + purpose_code: str + reason_code: str + review_period_start: date + review_period_end: date + generated_at: datetime = field(init=False) + evidence_version: int = 1 + contains_personal_data: bool = True + contains_direct_person_identifiers: bool = True + contains_rating_value: bool = False + contains_free_form_feedback: 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 + next_action: str = _NEXT_ACTION + + def __repr__(self) -> str: + """Return a representation that never emits worker/rating correlation evidence.""" + return "PerformanceReviewPacket()" + + 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.performance_review_reference, + "performance_review", + "performance_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.job_profile_reference, "job_profile", "job_profile_reference") + _validate_reference( + self.performance_cycle_reference, + "performance_cycle", + "performance_cycle_reference", + ) + _validate_reference(self.criterion_set_reference, "criterion_set", "criterion_set_reference") + _validate_digest(self.criterion_set_digest, "criterion_set_digest") + _validate_reference(self.goal_plan_reference, "performance_goal_plan", "goal_plan_reference") + _validate_digest(self.goal_plan_digest, "goal_plan_digest") + _validate_reference( + self.criterion_observation_snapshot_reference, + "criterion_observation_snapshot", + "criterion_observation_snapshot_reference", + ) + _validate_digest( + self.criterion_observation_snapshot_digest, + "criterion_observation_snapshot_digest", + ) + if (self.development_plan_reference is None) != (self.development_plan_digest is None): + raise ValueError("development plan reference and digest must be supplied together") + if self.development_plan_reference is not None: + _validate_reference( + self.development_plan_reference, + "development_plan", + "development_plan_reference", + ) + _validate_digest(self.development_plan_digest, "development_plan_digest") + _validate_reference(self.reviewer_reference, "actor", "reviewer_reference") + if type(self.purpose_code) is not str or self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain performance_review") + _validate_reason_code(self.reason_code) + _validate_business_date(self.review_period_start, "review_period_start") + _validate_business_date(self.review_period_end, "review_period_end") + if self.review_period_start > self.review_period_end: + raise ValueError("review period start must not be after review period end") + object.__setattr__(self, "generated_at", _freeze_timestamp(_system_recorded_at())) + _validate_evidence_version(self.evidence_version) + if self.contains_personal_data is not True: + raise ValueError("performance review packet contains personal data through worker references") + if self.contains_direct_person_identifiers is not True: + raise ValueError( + "unverified reference provenance must be treated as potentially containing direct person identifiers" + ) + if self.contains_rating_value is not False: + raise ValueError("performance review packet must not contain rating values") + if self.contains_free_form_feedback is not False: + raise ValueError("performance review packet must not contain free-form feedback") + if self.contains_free_form_model_output is not False: + raise ValueError("performance review packet must not contain free-form model output") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before performance rating") + if type(self.decision_authority) is not str or self.decision_authority != _DECISION_AUTHORITY: + raise ValueError("decision_authority must remain human_review_only") + if type(self.review_state) is not str or self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_review") + 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 type(self.next_action) is not str or self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed performance-review instruction") + _register_packet_seal(self, _seal_packet(_canonical_packet_json_unchecked(self))) + + def canonical_json(self) -> str: + """Return issuance-verified deterministic JSON for immutable audit correlation.""" + payload_json = _canonical_packet_json_unchecked(self) + authoritative_seal = _authoritative_packet_seal(self) + if authoritative_seal is None: + raise ValueError("performance review issuance evidence is unavailable") + if not hmac.compare_digest(authoritative_seal, _seal_packet(payload_json)): + raise ValueError("performance review evidence changed after issuance") + return payload_json + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact issuance-verified UTF-8 review packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def _canonical_packet_json_unchecked(packet: PerformanceReviewPacket) -> str: + """Render canonical bytes without consulting process-local issuance state.""" + payload = { + "contains_direct_person_identifiers": packet.contains_direct_person_identifiers, + "contains_free_form_feedback": packet.contains_free_form_feedback, + "contains_free_form_model_output": packet.contains_free_form_model_output, + "contains_personal_data": packet.contains_personal_data, + "contains_rating_value": packet.contains_rating_value, + "criterion_observation_snapshot_digest": packet.criterion_observation_snapshot_digest, + "criterion_observation_snapshot_reference": packet.criterion_observation_snapshot_reference, + "criterion_set_digest": packet.criterion_set_digest, + "criterion_set_reference": packet.criterion_set_reference, + "decision_authority": packet.decision_authority, + "development_plan_digest": packet.development_plan_digest, + "development_plan_reference": packet.development_plan_reference, + "employment_record_reference": packet.employment_record_reference, + "evidence_version": packet.evidence_version, + "generated_at": _canonical_timestamp(packet.generated_at), + "goal_plan_digest": packet.goal_plan_digest, + "goal_plan_reference": packet.goal_plan_reference, + "human_confirmation_required": packet.human_confirmation_required, + "job_profile_reference": packet.job_profile_reference, + "next_action": packet.next_action, + "performance_cycle_reference": packet.performance_cycle_reference, + "performance_review_reference": packet.performance_review_reference, + "person_record_reference": packet.person_record_reference, + "purpose_code": packet.purpose_code, + "reason_code": packet.reason_code, + "review_period_end": packet.review_period_end.isoformat(), + "review_period_start": packet.review_period_start.isoformat(), + "review_state": packet.review_state, + "reviewer_reference": packet.reviewer_reference, + "scope_verification_state": packet.scope_verification_state, + "tenant_record_id": packet.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + +def build_performance_review_packet( + *, + tenant_record_id: str, + performance_review_reference: str, + person_record_reference: str, + employment_record_reference: str, + job_profile_reference: str, + performance_cycle_reference: str, + criterion_set_reference: str, + criterion_set_digest: str, + goal_plan_reference: str, + goal_plan_digest: str, + criterion_observation_snapshot_reference: str, + criterion_observation_snapshot_digest: str, + development_plan_reference: str | None, + development_plan_digest: str | None, + reviewer_reference: str, + purpose_code: str, + reason_code: str, + review_period_start: date, + review_period_end: date, + evidence_version: int = 1, +) -> PerformanceReviewPacket: + """Build value-minimized performance-review evidence using system-recorded time.""" + return PerformanceReviewPacket( + tenant_record_id=tenant_record_id, + performance_review_reference=performance_review_reference, + person_record_reference=person_record_reference, + employment_record_reference=employment_record_reference, + job_profile_reference=job_profile_reference, + performance_cycle_reference=performance_cycle_reference, + criterion_set_reference=criterion_set_reference, + criterion_set_digest=criterion_set_digest, + goal_plan_reference=goal_plan_reference, + goal_plan_digest=goal_plan_digest, + criterion_observation_snapshot_reference=criterion_observation_snapshot_reference, + criterion_observation_snapshot_digest=criterion_observation_snapshot_digest, + development_plan_reference=development_plan_reference, + development_plan_digest=development_plan_digest, + reviewer_reference=reviewer_reference, + purpose_code=purpose_code, + reason_code=reason_code, + review_period_start=review_period_start, + review_period_end=review_period_end, + evidence_version=evidence_version, + ) diff --git a/packages/performance-review/tests/test_feedback_privacy.py b/packages/performance-review/tests/test_feedback_privacy.py new file mode 100644 index 000000000..c0156220b --- /dev/null +++ b/packages/performance-review/tests/test_feedback_privacy.py @@ -0,0 +1,19 @@ +"""Regression coverage for free-form feedback exclusion from review evidence.""" + +from dataclasses import replace +import json + +import pytest + +from test_packet import build_valid + + +def test_packet_explicitly_excludes_free_form_feedback() -> None: + """Keep free-form human feedback outside the immutable correlation envelope.""" + packet = build_valid() + + assert packet.contains_free_form_feedback is False + assert json.loads(packet.canonical_json())["contains_free_form_feedback"] is False + + with pytest.raises(ValueError, match="free-form feedback"): + replace(packet, contains_free_form_feedback=True) diff --git a/packages/performance-review/tests/test_issuance_integrity.py b/packages/performance-review/tests/test_issuance_integrity.py new file mode 100644 index 000000000..0b50c57ef --- /dev/null +++ b/packages/performance-review/tests/test_issuance_integrity.py @@ -0,0 +1,87 @@ +"""Regression tests for immutable performance-review issuance evidence.""" + +from __future__ import annotations + +from datetime import date + +import pytest + +from orgmetra_performance_review import build_performance_review_packet +from orgmetra_performance_review import packet as packet_module + + +def _build_packet(): + """Build one valid issued performance-review packet for tamper regressions.""" + return build_performance_review_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + performance_review_reference="performance_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", + job_profile_reference="job_profile:55555555-5555-4555-8555-555555555555", + performance_cycle_reference="performance_cycle:66666666-6666-4666-8666-666666666666", + criterion_set_reference="criterion_set:77777777-7777-4777-8777-777777777777", + criterion_set_digest="a" * 64, + goal_plan_reference="performance_goal_plan:88888888-8888-4888-8888-888888888888", + goal_plan_digest="b" * 64, + criterion_observation_snapshot_reference="criterion_observation_snapshot:99999999-9999-4999-8999-999999999999", + criterion_observation_snapshot_digest="c" * 64, + development_plan_reference="development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + development_plan_digest="d" * 64, + reviewer_reference="actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + purpose_code="performance_review", + reason_code="scheduled_cycle_review", + review_period_start=date(2026, 1, 1), + review_period_end=date(2026, 6, 30), + ) + + +def test_post_issuance_valid_value_rewrite_cannot_emit_new_canonical_truth() -> None: + """Reject a valid digest rewrite after the governed packet has been issued.""" + packet = _build_packet() + original = packet.canonical_json() + + object.__setattr__(packet, "goal_plan_digest", "f" * 64) + + with pytest.raises(ValueError, match="changed after issuance"): + packet.canonical_json() + with pytest.raises(ValueError, match="changed after issuance"): + packet.sha256_digest() + assert original != packet_module._canonical_packet_json_unchecked(packet) + + +def test_missing_process_local_issuance_evidence_fails_closed() -> None: + """Reject canonical export when process-local issuance evidence is unavailable.""" + packet = _build_packet() + packet_module._discard_packet_seal(id(packet)) + + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + packet.canonical_json() + + +def test_seal_loss_cannot_reset_live_issuance_lifecycle() -> None: + """Keep a live packet single-issued even after its process-local seal is lost.""" + packet = _build_packet() + original = packet.canonical_json() + + packet_module._discard_packet_seal(id(packet)) + object.__setattr__(packet, "goal_plan_digest", "f" * 64) + + with pytest.raises(ValueError, match="issuance evidence already exists"): + packet.__post_init__() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + packet.canonical_json() + assert original != packet_module._canonical_packet_json_unchecked(packet) + + +def test_reinitialization_cannot_renew_issuance_evidence_after_valid_value_rewrite() -> None: + """Keep one live packet identity bound to its original construction evidence.""" + packet = _build_packet() + original = packet.canonical_json() + + object.__setattr__(packet, "goal_plan_digest", "f" * 64) + + with pytest.raises(ValueError, match="issuance evidence already exists"): + packet.__post_init__() + with pytest.raises(ValueError, match="changed after issuance"): + packet.canonical_json() + assert original != packet_module._canonical_packet_json_unchecked(packet) diff --git a/packages/performance-review/tests/test_packet.py b/packages/performance-review/tests/test_packet.py new file mode 100644 index 000000000..b0b9f6a70 --- /dev/null +++ b/packages/performance-review/tests/test_packet.py @@ -0,0 +1,261 @@ +from dataclasses import replace +from datetime import date, datetime, timezone +from hashlib import sha256 +import inspect +import json + +import pytest + +import orgmetra_performance_review.packet as packet_module +from orgmetra_performance_review import ( + PerformanceReviewPacket, + build_performance_review_packet, +) + +TENANT = "11111111-1111-4111-8111-111111111111" +PERFORMANCE_REVIEW = "performance_review:22222222-2222-4222-8222-222222222222" +PERSON = "person_record:33333333-3333-4333-8333-333333333333" +EMPLOYMENT = "employment_record:44444444-4444-4444-8444-444444444444" +JOB = "job_profile:55555555-5555-4555-8555-555555555555" +CYCLE = "performance_cycle:66666666-6666-4666-8666-666666666666" +CRITERION_SET = "criterion_set:77777777-7777-4777-8777-777777777777" +GOAL_PLAN = "performance_goal_plan:88888888-8888-4888-8888-888888888888" +OBSERVATION_SNAPSHOT = "criterion_observation_snapshot:99999999-9999-4999-8999-999999999999" +DEVELOPMENT_PLAN = "development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" +REVIEWER = "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +GENERATED_AT = datetime(2026, 8, 19, 5, 15, 30, 123456, tzinfo=timezone.utc) + + +@pytest.fixture(autouse=True) +def fixed_system_recorded_at(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep packet digests deterministic while production owns issuance time.""" + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: GENERATED_AT) + + +def build_valid(**overrides: object) -> PerformanceReviewPacket: + values: dict[str, object] = { + "tenant_record_id": TENANT, + "performance_review_reference": PERFORMANCE_REVIEW, + "person_record_reference": PERSON, + "employment_record_reference": EMPLOYMENT, + "job_profile_reference": JOB, + "performance_cycle_reference": CYCLE, + "criterion_set_reference": CRITERION_SET, + "criterion_set_digest": DIGEST_A, + "goal_plan_reference": GOAL_PLAN, + "goal_plan_digest": DIGEST_B, + "criterion_observation_snapshot_reference": OBSERVATION_SNAPSHOT, + "criterion_observation_snapshot_digest": DIGEST_C, + "development_plan_reference": DEVELOPMENT_PLAN, + "development_plan_digest": DIGEST_D, + "reviewer_reference": REVIEWER, + "purpose_code": "performance_review", + "reason_code": "scheduled_cycle_review", + "review_period_start": date(2026, 1, 1), + "review_period_end": date(2026, 6, 30), + } + values.update(overrides) + return build_performance_review_packet(**values) + + +def test_builds_value_minimized_human_review_packet() -> None: + packet = build_valid() + assert packet.contains_personal_data is True + assert packet.contains_direct_person_identifiers is True + assert packet.contains_rating_value is False + assert packet.contains_free_form_model_output is False + assert packet.human_confirmation_required is True + assert packet.decision_authority == "human_review_only" + assert packet.review_state == "requires_human_review" + assert packet.scope_verification_state == "requires_authoritative_resolution" + assert "reference provenance and opacity" in packet.next_action + assert "record accountable human rating and feedback" in packet.next_action + + +def test_canonical_json_and_digest_are_deterministic() -> None: + packet = build_valid() + canonical = packet.canonical_json() + payload = json.loads(canonical) + assert payload["person_record_reference"] == PERSON + assert payload["contains_personal_data"] is True + assert payload["contains_direct_person_identifiers"] is True + assert payload["scope_verification_state"] == "requires_authoritative_resolution" + assert payload["generated_at"] == "2026-08-19T05:15:30.123456Z" + assert packet.sha256_digest() == sha256(canonical.encode("utf-8")).hexdigest() + assert canonical == build_valid().canonical_json() + + +def test_evidence_version_is_bound_to_canonical_evidence() -> None: + """Require review evidence versioning to change the immutable correlation digest.""" + first = build_valid() + assert first.evidence_version == 1 + assert json.loads(first.canonical_json())["evidence_version"] == 1 + + second = build_valid(evidence_version=2) + assert second.evidence_version == 2 + assert second.canonical_json() != first.canonical_json() + assert second.sha256_digest() != first.sha256_digest() + + +@pytest.mark.parametrize("evidence_version", [0, -1, True, "1", 2_147_483_648]) +def test_rejects_invalid_evidence_version(evidence_version: object) -> None: + """Reject unbounded or non-integer performance-review evidence versions.""" + with pytest.raises(ValueError, match="evidence_version"): + build_valid(evidence_version=evidence_version) + + +def test_system_recorded_timestamp_is_bound_to_canonical_evidence() -> None: + packet = build_valid() + assert packet.generated_at == GENERATED_AT + assert packet.generated_at.tzinfo is timezone.utc + assert json.loads(packet.canonical_json())["generated_at"] == "2026-08-19T05:15:30.123456Z" + + +def test_optional_development_plan_may_be_absent_as_a_pair() -> None: + packet = build_valid(development_plan_reference=None, development_plan_digest=None) + assert packet.development_plan_reference is None + assert packet.development_plan_digest is None + + +@pytest.mark.parametrize("tenant", ["not-a-uuid", "00000000-0000-0000-0000-000000000000", "11111111-1111-4111-8111-11111111111A", None]) +def test_rejects_noncanonical_tenant_identity(tenant: object) -> None: + with pytest.raises(ValueError, match="tenant_record_id"): + build_valid(tenant_record_id=tenant) + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("performance_review_reference", "performance_review:Jane-Doe"), + ("person_record_reference", "candidate_profile:33333333-3333-4333-8333-333333333333"), + ("employment_record_reference", "employment_record:00000000-0000-0000-0000-000000000000"), + ("job_profile_reference", "job_profile:not-a-uuid"), + ("performance_cycle_reference", "performance_cycle:66666666-6666-4666-8666-66666666666A"), + ("criterion_set_reference", "criterion:77777777-7777-4777-8777-777777777777"), + ("goal_plan_reference", "performance_goal_plan:salary"), + ("criterion_observation_snapshot_reference", "criterion_observation_snapshot:score-4"), + ("reviewer_reference", "actor:reviewer-name"), + ], +) +def test_rejects_noncanonical_or_wrong_namespace_references(field: str, value: str) -> None: + with pytest.raises(ValueError, match=field): + build_valid(**{field: value}) + + +@pytest.mark.parametrize( + ("field", "prefix"), + [ + ("performance_review_reference", "performance_review"), + ("person_record_reference", "person_record"), + ("employment_record_reference", "employment_record"), + ("job_profile_reference", "job_profile"), + ("performance_cycle_reference", "performance_cycle"), + ("criterion_set_reference", "criterion_set"), + ("goal_plan_reference", "performance_goal_plan"), + ("criterion_observation_snapshot_reference", "criterion_observation_snapshot"), + ("development_plan_reference", "development_plan"), + ("reviewer_reference", "actor"), + ], +) +def test_rejects_uuid1_trust_references_through_builder_and_replace( + field: str, + prefix: str, +) -> None: + value = f"{prefix}:{UUID1_ID}" + with pytest.raises(ValueError, match=field): + build_valid(**{field: value}) + + with pytest.raises(ValueError, match=field): + replace(build_valid(), **{field: value}) + + +@pytest.mark.parametrize("field", ["criterion_set_digest", "goal_plan_digest", "criterion_observation_snapshot_digest", "development_plan_digest"]) +def test_rejects_malformed_evidence_digests(field: str) -> None: + with pytest.raises(ValueError, match=field): + build_valid(**{field: "ABC"}) + + +def test_requires_development_reference_and_digest_as_a_pair() -> None: + with pytest.raises(ValueError, match="development plan reference and digest"): + build_valid(development_plan_reference=None) + with pytest.raises(ValueError, match="development plan reference and digest"): + build_valid(development_plan_digest=None) + + +@pytest.mark.parametrize("purpose", ["selection_review", "performance", "Performance_Review"]) +def test_purpose_is_fixed(purpose: str) -> None: + with pytest.raises(ValueError, match="purpose_code must remain performance_review"): + build_valid(purpose_code=purpose) + + +@pytest.mark.parametrize("reason", ["singleword", "Upper_case", "a" * 65]) +def test_reason_code_is_bounded_descriptive_snake_case(reason: str) -> None: + with pytest.raises(ValueError, match="reason_code"): + build_valid(reason_code=reason) + + +@pytest.mark.parametrize( + "reason", + ["employee_jane_doe", "ssn_123_45_6789", "manager_override"], +) +def test_reason_code_rejects_ungoverned_free_form_values(reason: str) -> None: + with pytest.raises(ValueError, match="reason_code must be an authorized"): + build_valid(reason_code=reason) + + +def test_review_period_must_be_real_dates_in_order() -> None: + with pytest.raises(ValueError, match="review_period_start"): + build_valid(review_period_start="2026-01-01") + with pytest.raises(ValueError, match="review_period_end"): + build_valid(review_period_end="2026-06-30") + with pytest.raises(ValueError, match="review period"): + build_valid(review_period_start=date(2026, 7, 1), review_period_end=date(2026, 6, 30)) + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("contains_personal_data", False, "personal data"), + ("contains_direct_person_identifiers", False, "unverified reference provenance"), + ("contains_rating_value", True, "must not contain rating values"), + ("contains_free_form_model_output", True, "must not contain free-form model output"), + ("human_confirmation_required", 1, "human confirmation is mandatory"), + ("decision_authority", "model_decision", "decision_authority"), + ("review_state", "approved", "review_state"), + ("scope_verification_state", "verified", "scope_verification_state"), + ("next_action", "Auto-rate the employee.", "next_action"), + ], +) +def test_direct_construction_cannot_weaken_governance(field: str, value: object, message: str) -> None: + with pytest.raises(ValueError, match=message): + replace(build_valid(), **{field: value}) + + +def test_direct_construction_revalidates_reference_and_digest() -> None: + with pytest.raises(ValueError, match="person_record_reference"): + replace(build_valid(), person_record_reference="person_record:Jane-Doe") + with pytest.raises(ValueError, match="criterion_set_digest"): + replace(build_valid(), criterion_set_digest="0" * 63) + + +def test_builder_returns_same_public_type_as_direct_contract() -> None: + packet = build_valid() + assert isinstance(packet, PerformanceReviewPacket) + + +def test_system_recorded_issuance_time_is_not_caller_supplied() -> None: + """System-recorded audit time must not be accepted from the packet caller.""" + assert "generated_at" not in inspect.signature(build_performance_review_packet).parameters + assert "generated_at" not in inspect.signature(PerformanceReviewPacket).parameters + + +def test_unverified_uuidv4_reference_is_classified_as_potential_direct_identifier() -> None: + """UUIDv4 syntax alone must not justify a no-direct-identifier assertion.""" + encoded_person = "person_record:4a616e65-2d44-4f65-8065-2d53534e3132" + packet = build_valid(person_record_reference=encoded_person) + assert packet.contains_direct_person_identifiers is True diff --git a/packages/performance-review/tests/test_quality_workflow_trigger.py b/packages/performance-review/tests/test_quality_workflow_trigger.py new file mode 100644 index 000000000..9df32f885 --- /dev/null +++ b/packages/performance-review/tests/test_quality_workflow_trigger.py @@ -0,0 +1,27 @@ +"""Regression tests for the performance-review quality-gate trigger surface.""" + +from pathlib import Path + + +_WORKFLOW_PATH = Path(".github/workflows/performance-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 " + "Performance Review Quality" + ) diff --git a/packages/performance-review/tests/test_repr_privacy.py b/packages/performance-review/tests/test_repr_privacy.py new file mode 100644 index 000000000..bcf78d1f5 --- /dev/null +++ b/packages/performance-review/tests/test_repr_privacy.py @@ -0,0 +1,35 @@ +from datetime import date + +from orgmetra_performance_review import build_performance_review_packet + + +def test_repr_redacts_worker_rating_scope_and_evidence() -> None: + packet = build_performance_review_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + performance_review_reference="performance_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", + job_profile_reference="job_profile:55555555-5555-4555-8555-555555555555", + performance_cycle_reference="performance_cycle:66666666-6666-4666-8666-666666666666", + criterion_set_reference="criterion_set:77777777-7777-4777-8777-777777777777", + criterion_set_digest="a" * 64, + goal_plan_reference="performance_goal_plan:88888888-8888-4888-8888-888888888888", + goal_plan_digest="b" * 64, + criterion_observation_snapshot_reference="criterion_observation_snapshot:99999999-9999-4999-8999-999999999999", + criterion_observation_snapshot_digest="c" * 64, + development_plan_reference="development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + development_plan_digest="d" * 64, + reviewer_reference="actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + purpose_code="performance_review", + reason_code="scheduled_cycle_review", + review_period_start=date(2026, 1, 1), + review_period_end=date(2026, 6, 30), + ) + + rendered = repr(packet) + assert rendered == "PerformanceReviewPacket()" + assert packet.tenant_record_id not in rendered + assert packet.person_record_reference not in rendered + assert packet.employment_record_reference not in rendered + assert packet.reviewer_reference not in rendered + assert packet.criterion_observation_snapshot_digest not in rendered diff --git a/packages/performance-review/tests/test_string_runtime_evidence_integrity.py b/packages/performance-review/tests/test_string_runtime_evidence_integrity.py new file mode 100644 index 000000000..9f0ac3683 --- /dev/null +++ b/packages/performance-review/tests/test_string_runtime_evidence_integrity.py @@ -0,0 +1,136 @@ +"""Regression coverage for string-subclass evidence-boundary integrity.""" + +from __future__ import annotations + +from dataclasses import replace +from datetime import date + +import pytest + +from orgmetra_performance_review import build_performance_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] + 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 equality and closed-vocabulary membership.""" + + def __eq__(self, other): # type: ignore[no-untyped-def] + return True + + def __ne__(self, other): # type: ignore[no-untyped-def] + return False + + def __hash__(self) -> int: + return hash("scheduled_cycle_review") + + +class ForgedDigest(str): + """Valid-looking digest subclass that must not cross the evidence boundary.""" + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid performance-review packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "performance_review_reference": "performance_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", + "job_profile_reference": "job_profile:55555555-5555-4555-8555-555555555555", + "performance_cycle_reference": "performance_cycle:66666666-6666-4666-8666-666666666666", + "criterion_set_reference": "criterion_set:77777777-7777-4777-8777-777777777777", + "criterion_set_digest": "a" * 64, + "goal_plan_reference": "performance_goal_plan:88888888-8888-4888-8888-888888888888", + "goal_plan_digest": "b" * 64, + "criterion_observation_snapshot_reference": "criterion_observation_snapshot:99999999-9999-4999-8999-999999999999", + "criterion_observation_snapshot_digest": "c" * 64, + "development_plan_reference": "development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "development_plan_digest": "d" * 64, + "reviewer_reference": "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "purpose_code": "performance_review", + "reason_code": "scheduled_cycle_review", + "review_period_start": date(2026, 1, 1), + "review_period_end": date(2026, 6, 30), + } + + +def test_rejects_reference_string_subclass_that_can_forge_namespace_validation() -> None: + kwargs = valid_kwargs() + kwargs["performance_review_reference"] = ForgedReference("evil:payload") + with pytest.raises(ValueError, match="performance_review_reference"): + build_performance_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_performance_review_packet(**kwargs) + + +def test_rejects_reason_code_string_subclass_that_can_forge_allow_list_membership() -> None: + kwargs = valid_kwargs() + kwargs["reason_code"] = ForgedGovernanceText("attacker_controlled_reason") + with pytest.raises(ValueError, match="reason_code"): + build_performance_review_packet(**kwargs) + + +def test_rejects_purpose_code_string_subclass_that_can_forge_fixed_code_check() -> None: + kwargs = valid_kwargs() + kwargs["purpose_code"] = ForgedGovernanceText("attacker_controlled_purpose") + with pytest.raises(ValueError, match="purpose_code"): + build_performance_review_packet(**kwargs) + + +@pytest.mark.parametrize( + "field_name", + ( + "criterion_set_digest", + "goal_plan_digest", + "criterion_observation_snapshot_digest", + "development_plan_digest", + ), +) +def test_rejects_digest_string_subclass_at_all_digest_boundaries(field_name: str) -> None: + """Every trust-bearing SHA-256 field requires exact built-in text.""" + kwargs = valid_kwargs() + kwargs[field_name] = ForgedDigest(str(kwargs[field_name])) + with pytest.raises(ValueError, match=field_name): + build_performance_review_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "message"), + ( + ("decision_authority", "decision_authority"), + ("review_state", "review_state"), + ("scope_verification_state", "scope_verification_state"), + ("next_action", "next_action"), + ), +) +def test_rejects_forged_direct_construction_constant_text(field_name: str, message: str) -> None: + packet = build_performance_review_packet(**valid_kwargs()) + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: ForgedGovernanceText("attacker_controlled_text")}) diff --git a/packages/performance-review/tests/test_temporal_evidence_integrity.py b/packages/performance-review/tests/test_temporal_evidence_integrity.py new file mode 100644 index 000000000..d3e732cf2 --- /dev/null +++ b/packages/performance-review/tests/test_temporal_evidence_integrity.py @@ -0,0 +1,178 @@ +"""Regression coverage for system-recorded performance-review time integrity.""" + +from __future__ import annotations + +from datetime import date, datetime, timedelta, timezone, tzinfo + +import pytest + +import orgmetra_performance_review.packet as packet_module +from orgmetra_performance_review import build_performance_review_packet + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical evidence rendering.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the 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 evidence instant.""" + return "2099-12-31T23:59:59+00:00" + + +class MutableTimezone(tzinfo): + """Clock timezone provider whose offset changes after evidence issuance.""" + + def __init__(self, offset: timedelta) -> None: + """Store one mutable offset.""" + self.offset = offset + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Return the current mutable offset.""" + return self.offset + + def dst(self, dt: datetime | None) -> timedelta: + """Expose no daylight-saving adjustment.""" + return timedelta(0) + + +class NullOffsetTimezone(tzinfo): + """Clock timezone provider with no concrete UTC offset.""" + + def utcoffset(self, dt: datetime | None) -> None: + """Return no usable offset.""" + return None + + def dst(self, dt: datetime | None) -> None: + """Return no daylight-saving offset.""" + return None + + +class RaisingTimezone(tzinfo): + """Clock timezone provider that raises while resolving UTC offset.""" + + def utcoffset(self, dt: datetime | None) -> timedelta: + """Raise at the recorded-time trust boundary.""" + raise RuntimeError("provider failure") + + def dst(self, dt: datetime | None) -> timedelta: + """Expose no daylight-saving adjustment.""" + return timedelta(0) + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid performance-review packet input.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "performance_review_reference": "performance_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", + "job_profile_reference": "job_profile:55555555-5555-4555-8555-555555555555", + "performance_cycle_reference": "performance_cycle:66666666-6666-4666-8666-666666666666", + "criterion_set_reference": "criterion_set:77777777-7777-4777-8777-777777777777", + "criterion_set_digest": "a" * 64, + "goal_plan_reference": "performance_goal_plan:88888888-8888-4888-8888-888888888888", + "goal_plan_digest": "b" * 64, + "criterion_observation_snapshot_reference": "criterion_observation_snapshot:99999999-9999-4999-8999-999999999999", + "criterion_observation_snapshot_digest": "c" * 64, + "development_plan_reference": "development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "development_plan_digest": "d" * 64, + "reviewer_reference": "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "purpose_code": "performance_review", + "reason_code": "scheduled_cycle_review", + "review_period_start": date(2026, 1, 1), + "review_period_end": date(2026, 6, 30), + } + + +def test_rejects_datetime_subclasses_from_trusted_clock_adapter( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Canonical audit evidence must not invoke overridable datetime methods.""" + forged = ForgedDateTime(2026, 8, 19, 5, 15, 30, tzinfo=timezone.utc) + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: forged) + + with pytest.raises(ValueError, match="generated_at"): + build_performance_review_packet(**valid_kwargs()) + + +def test_detaches_mutable_clock_timezone_from_recorded_time_evidence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A mutable clock timezone provider must not rewrite issued evidence.""" + provider = MutableTimezone(timedelta(hours=9)) + recorded_at = datetime(2026, 8, 19, 14, 15, 30, tzinfo=provider) + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: recorded_at) + + packet = build_performance_review_packet(**valid_kwargs()) + before = packet.canonical_json() + provider.offset = timedelta(hours=-7) + + assert packet.canonical_json() == before + assert packet.generated_at == datetime(2026, 8, 19, 5, 15, 30, tzinfo=timezone.utc) + assert packet.generated_at.tzinfo is timezone.utc + + +def test_rejects_future_system_recorded_time(monkeypatch: pytest.MonkeyPatch) -> None: + """Do not seal evidence when the trusted clock reports a future instant.""" + monkeypatch.setattr( + packet_module, + "_system_recorded_at", + lambda: datetime(2099, 1, 1, tzinfo=timezone.utc), + ) + + with pytest.raises(ValueError, match="generated_at must not be in the future"): + build_performance_review_packet(**valid_kwargs()) + + +def test_rejects_clock_timezone_without_concrete_offset( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Reject clock adapters that cannot resolve a concrete UTC offset.""" + recorded_at = datetime(2026, 8, 19, 5, 15, 30, tzinfo=NullOffsetTimezone()) + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: recorded_at) + + with pytest.raises(ValueError, match="generated_at"): + build_performance_review_packet(**valid_kwargs()) + + +def test_normalizes_clock_timezone_provider_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """Do not leak clock timezone exceptions across the evidence boundary.""" + recorded_at = datetime(2026, 8, 19, 5, 15, 30, tzinfo=RaisingTimezone()) + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: recorded_at) + + with pytest.raises(ValueError, match="generated_at"): + build_performance_review_packet(**valid_kwargs()) + + +def test_rejects_clock_timezone_normalization_overflow( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Fail closed when a clock instant cannot be represented as UTC.""" + recorded_at = datetime.min.replace(tzinfo=timezone(timedelta(hours=14))) + monkeypatch.setattr(packet_module, "_system_recorded_at", lambda: recorded_at) + + with pytest.raises(ValueError, match="generated_at"): + build_performance_review_packet(**valid_kwargs()) + + +def test_rejects_post_construction_timezone_reinjection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Do not emit evidence after low-level replacement of the frozen UTC instant.""" + monkeypatch.setattr( + packet_module, + "_system_recorded_at", + lambda: datetime(2026, 8, 19, 5, 15, 30, tzinfo=timezone.utc), + ) + packet = build_performance_review_packet(**valid_kwargs()) + object.__setattr__( + packet, + "generated_at", + datetime(2026, 8, 19, 14, 15, 30, tzinfo=timezone(timedelta(hours=9))), + ) + + with pytest.raises(ValueError, match="generated_at"): + packet.canonical_json() diff --git a/packages/performance-review/tests/test_tenant_identity_privacy.py b/packages/performance-review/tests/test_tenant_identity_privacy.py new file mode 100644 index 000000000..96494d6a8 --- /dev/null +++ b/packages/performance-review/tests/test_tenant_identity_privacy.py @@ -0,0 +1,62 @@ +"""Privacy and interoperability regression for performance-review tenant identity.""" +from dataclasses import replace +from datetime import date + +from orgmetra_performance_review import build_performance_review_packet + +UUID7_TENANT = "10000000-0000-7000-8000-000000000001" + + +def _build(): + """Build one valid value-minimized performance-review packet.""" + return build_performance_review_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + performance_review_reference="performance_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", + job_profile_reference="job_profile:55555555-5555-4555-8555-555555555555", + performance_cycle_reference="performance_cycle:66666666-6666-4666-8666-666666666666", + criterion_set_reference="criterion_set:77777777-7777-4777-8777-777777777777", + criterion_set_digest="a" * 64, + goal_plan_reference="performance_goal_plan:88888888-8888-4888-8888-888888888888", + goal_plan_digest="b" * 64, + criterion_observation_snapshot_reference="criterion_observation_snapshot:99999999-9999-4999-8999-999999999999", + criterion_observation_snapshot_digest="c" * 64, + development_plan_reference="development_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + development_plan_digest="d" * 64, + reviewer_reference="actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + purpose_code="performance_review", + reason_code="scheduled_cycle_review", + review_period_start=date(2026, 1, 1), + review_period_end=date(2026, 6, 30), + ) + + +def test_authoritative_uuid7_tenant_identity_is_accepted_by_builder_and_replace() -> None: + """Accept tenant UUIDs already valid at the authoritative Orgmetra core boundary.""" + packet = _build() + replaced = replace(packet, tenant_record_id=UUID7_TENANT) + + kwargs = { + field: getattr(packet, field) + for field in packet.__dataclass_fields__ + if field + not in { + "generated_at", + "contains_personal_data", + "contains_direct_person_identifiers", + "contains_rating_value", + "contains_free_form_feedback", + "contains_free_form_model_output", + "human_confirmation_required", + "decision_authority", + "review_state", + "scope_verification_state", + "next_action", + } + } + kwargs["tenant_record_id"] = UUID7_TENANT + rebuilt = build_performance_review_packet(**kwargs) + + assert replaced.tenant_record_id == UUID7_TENANT + assert rebuilt.tenant_record_id == UUID7_TENANT