diff --git a/.github/requirements/foundation-test.txt b/.github/requirements/foundation-test.txt index 40d926005..8bfe48ae1 100644 --- a/.github/requirements/foundation-test.txt +++ b/.github/requirements/foundation-test.txt @@ -1,6 +1,9 @@ -# Reviewed Foundation CI test toolchain for CPython 3.14 on GitHub-hosted Ubuntu x86_64. +# Reviewed Foundation CI test toolchain for CPython 3.12-3.14 on GitHub-hosted Ubuntu x86_64. # Version and artifact hash changes must be reverified against the official PyPI release JSON. -coverage==7.14.2 --hash=sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64 +coverage==7.14.2 \ + --hash=sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240 \ + --hash=sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e \ + --hash=sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64 iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 diff --git a/.github/workflows/interview-plan-quality.yml b/.github/workflows/interview-plan-quality.yml new file mode 100644 index 000000000..aafb4c4ed --- /dev/null +++ b/.github/workflows/interview-plan-quality.yml @@ -0,0 +1,104 @@ +name: Structured Interview Plan Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/interview-plan/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/interview-plan-quality.yml" + - ".gitignore" + - ".python-version" + - "conftest.py" + - "packages/conftest.py" + - "pyproject.toml" + - "pytest.ini" + - "setup.cfg" + - "tox.ini" + - "docs/adr/0015-governed-structured-interview-plan.md" + - "docs/doctoring/structured-interview-plan-references.md" + - "docs/traceability/structured-interview-plan.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: structured-interview-plan-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Structured interview plan 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 structured interview plan package + run: python -m compileall -q packages/interview-plan/src packages/interview-plan/tests + - name: Test structured interview plan with exact statement and branch coverage + env: + PYTHONPATH: packages/interview-plan/src + COVERAGE_FILE: /tmp/orgmetra-structured-interview-plan.coverage + run: python -m pytest -c packages/interview-plan/pyproject.toml packages/interview-plan/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" + + compatibility: + name: Python ${{ matrix.python-version }} compatibility and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + 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: ${{ matrix.python-version }} + 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 structured interview plan package + run: python -m compileall -q packages/interview-plan/src packages/interview-plan/tests + - name: Test supported Python with exact statement and branch coverage + env: + PYTHONPATH: packages/interview-plan/src + COVERAGE_FILE: /tmp/orgmetra-structured-interview-plan-${{ matrix.python-version }}.coverage + run: python -m pytest -c packages/interview-plan/pyproject.toml packages/interview-plan/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/docs/adr/0015-governed-structured-interview-plan.md b/docs/adr/0015-governed-structured-interview-plan.md new file mode 100644 index 000000000..2014cc3dc --- /dev/null +++ b/docs/adr/0015-governed-structured-interview-plan.md @@ -0,0 +1,72 @@ +# ADR 0015: Govern structured-interview plans as candidate-neutral evidence + +- **Status:** Proposed — active PR only +- **Date:** 2026-08-18 + +## Context + +Orgmetra already separates authoritative Job/Position/Assignment truth, governed requisition review, selection evidence, and accountable human employment decisions. A buyer still needs a defensible boundary between an approved opening and the interview that will be used as a selection procedure. + +A structured interview is stronger when assessed competencies come from current job analysis, candidates receive the same predetermined questions, and responses are evaluated against common rating standards. A question count cannot prove that each governed competency is represented, so the approved question-to-competency mapping needs its own immutable evidence identity. Candidate identity, assessment values, and semantic/value-bearing labels are unnecessary at this pre-use boundary and would increase privacy risk. Packet-owned trust references therefore use UUIDv4; the authoritative tenant identifier instead follows Orgmetra core's canonical non-sentinel operational UUID contract. + +Opaque identities and artifact digests identify evidence but do not prove tenant ownership, requisition-to-Job-to-job-analysis relationships, or distinct human identities. Those relationships must be re-resolved at authoritative owner boundaries immediately before activation. A prose-only `next_action` is insufficient: the package needs an executable host boundary that cannot issue activation evidence when authoritative checks reject or when returned verification evidence belongs to another plan, actor, or approval instant. + +Plan-generation time and approval time are trust-bearing evidence. Caller-controlled mutable timezone state must not make one governed instant later represent a different UTC instant. Caller-owned `tzinfo` implementations are executable code and may raise arbitrary exceptions while `utcoffset()` is evaluated; such failures and unrepresentable UTC normalization must become field-specific governed validation before plan issuance, authority side effects, verification acceptance, or canonical export. Constructor provenance must also not remain ambient while such caller code runs: otherwise a reentrant timezone callback can invoke the plan allocator, retain a second object that inherits constructor eligibility, populate it later with otherwise valid fields, and mint issuance evidence without a normal class construction. + +Python `frozen=True` is not an adversarial immutability or authorization boundary. `object.__setattr__` can rewrite dataclass fields, and low-level allocation can create a dataclass-shaped instance without completing its governed constructor. Merely executing a class `__new__` method is also not proof of construction because callers can invoke that allocator directly. A module-private constructor token is still reachable by Python callers. Therefore receipt shape construction must not itself confer human-approval authority, and plan issuance must prove that the exact live object entered through the normal full `StructuredInterviewPlan(...)` construction path before `__post_init__` may register creation evidence. Plan construction may register process-local integrity evidence because construction is the governed plan-issuance boundary, but activation-receipt issuance evidence must be registered only by the verified activation factory after authoritative host checks and exact-scope matching have completed. + +## Decision + +Add a transport-neutral `StructuredInterviewPlan` value object that binds: + +- canonical non-sentinel Orgmetra tenant identity and one UUIDv4-backed opaque interview-plan reference; +- UUIDv4-backed requisition and authoritative Job references; +- exact job-analysis, question-set, question-to-competency mapping, and rating-anchor references plus independent SHA-256 digests; +- sorted, unique job-related competency references and a bounded 2–8 actor interviewer panel; +- a bounded question count at least as large as the governed competency count, while the separately bound mapping artifact supplies actual coverage evidence; and +- fixed purpose `structured_interview_plan`, closed reason `approved_requisition_interview`, bounded positive `evidence_version`, precision-preserving UTC time, mandatory human confirmation, and `requires_human_approval` state. + +`tenant_record_id` follows the authoritative operational UUID contract. Packet-owned trust-bearing references require canonical non-sentinel UUIDv4 plus their expected namespace. Names, labels, compensation/protected-attribute values, and other semantic suffixes fail closed. Direct construction, builder construction, and `dataclasses.replace(...)` share the same plan validation. `evidence_version` is serialized canonically and changes immutable SHA-256 correlation when revised. Routine plan representation is fully redacted. + +Before plan issuance evidence is registered, detach caller-owned `generated_at` into one built-in UTC `datetime` using one concrete offset read from the original aware value. Treat offset evaluation as an untrusted-code boundary: exceptions and offset arithmetic beyond Python's representable `datetime` range become the same field-specific `ValueError`. Store the built-in UTC snapshot rather than caller-owned `tzinfo` state. Canonical timestamp rendering reuses this fail-closed detachment. + +A private metaclass arms a context-local **one-shot allocator ticket** immediately before the normal `StructuredInterviewPlan(...)` class construction. The exact `StructuredInterviewPlan.__new__()` invocation consumes that ticket before any field validation, `tzinfo.utcoffset()` call, or other caller-controlled callback can execute, then records construction eligibility only for that exact live object. Successful `__post_init__()` requires and consumes that provenance, computes a process-local HMAC over the canonical plan payload, registers the seal outside plan-writable slots, and records the exact identity as issued. Registration remains single-use for one live plan identity. `canonical_json()` requires exact issued-identity membership plus creation-bound HMAC evidence and uses constant-time comparison before returning bytes; `sha256_digest()` is downstream. An `object.__new__` clone, direct `StructuredInterviewPlan.__new__(StructuredInterviewPlan)` allocation, or allocator call reached reentrantly from caller-owned timezone code cannot call `__post_init__()` to mint fresh issuance evidence because none receives or retains the already-consumed constructor ticket. Low-level mutation, copied/reconstructed identities, missing issuance evidence, and attempted plan resealing fail closed. These context/identity/HMAC controls are same-process integrity evidence only, not a hostile-interpreter capability boundary, persisted signing scheme, portable signature, or replacement for immutable audit/outbox evidence. + +Make authoritative activation executable through `StructuredInterviewActivationAuthority` and `activate_structured_interview_plan(...)`. Before authority work, activation requires the exact governed `StructuredInterviewPlan` runtime type, obtains creation-bound canonical plan JSON, derives tenant/interview-plan scope and SHA-256 from those bytes, detaches caller-owned `approved_at` into built-in UTC, validates the approving actor, and rejects chronology before plan generation. The authority receives only detached canonical plan JSON, its exact digest, approving actor, and normalized approval instant—never the live plan object. A retained alias therefore cannot change what the authority reviews through a temporary change-and-restore cycle; non-restored mutation is still rejected by the post-authority plan integrity check. + +Implement `StructuredInterviewActivationVerification` as an exact `NamedTuple` carrying tenant, interview-plan reference, plan digest, approving actor, authority-evidence reference/digest, and reviewed `approved_at`. Activation rejects subclasses, unpacks the exact tuple once, normalizes returned approval time through the same fail-closed UTC helper, validates returned values, and compares the complete tenant/plan/digest/actor/time scope against the pre-call request. + +`StructuredInterviewActivationReceipt` is a value-minimized receipt shape, not an authorization primitive. Its dataclass constructor validates tenant/reference/digest/time/fixed-governance values but **does not register issuance evidence**. Direct construction and `dataclasses.replace(...)` therefore produce unissued values whose `canonical_json()` and `sha256_digest()` fail closed. The constructor has no issuance-token parameter; a module-private legacy sentinel confers no authority and is retained only as a regression target proving that callers cannot mint issued receipts by importing a private module attribute. + +Only after `activate_structured_interview_plan(...)` has accepted exact verification evidence, normalized the returned approval instant, validated all returned fields, and matched tenant, interview-plan reference, plan digest, approving actor, and approval time does it construct the receipt and register a process-local HMAC seal over that exact canonical payload. Issued receipt canonical export recomputes and constant-time compares the seal. Any low-level post-issuance rewrite or missing issuance evidence fails closed. The seal is same-process integrity evidence only; it is not a durable signing key, portable attestation, cross-process rehydration credential, or substitute for the host's immutable audit/outbox record. + +The issued receipt records the exact plan digest, accountable UUIDv4 approving actor, authority-verification reference/digest, fixed purpose `structured_interview_activation`, fixed reason `human_approved_plan_activation`, bounded positive evidence version, detached precision-preserving UTC approval time, `human_confirmation=True`, and fixed `approved_for_use` state. Routine receipt and verification representations are fully redacted. The plan and receipt remain candidate-neutral: they contain no candidate identity, response, score, demographic attribute, compensation value, free-form model output, provider credential, or final selection recommendation. + +## Consequences + +### Positive + +- Buyers can prove which Job Analysis, competencies, questions, mapping, rating anchors, panel, and evidence revision were reviewed before candidate use. +- Caller-controlled timezone failures and mutable timezone state cannot silently redefine governed plan or approval instants. +- Constructor-bypassing `object.__new__` clones, direct class-allocator calls, and reentrant allocator calls from caller-owned timezone callbacks cannot mint creation-bound plan issuance evidence merely by copying valid fields and invoking `__post_init__()`. +- The authoritative adapter reviews detached creation-bound plan evidence rather than a caller-owned live plan object. +- Authority verification fields are tuple-immutable at runtime and exact-type checked before one-time unpacking. +- A caller cannot mint an `approved_for_use` evidence artifact by importing a private constructor sentinel: direct and replaced receipt values remain unissued and cannot export canonical evidence. +- Receipt issuance is causally ordered after authoritative host verification and exact tenant/plan/digest/actor/time matching. +- Post-issuance receipt mutation and missing process-local issuance evidence fail closed before canonical export. +- Candidate PII and assessment values remain outside planning and activation artifacts. +- The authority protocol preserves standalone operation and later MSA extraction without cross-service application-table SQL or duplicated foreign-service state. + +### Costs and constraints + +- The package does not persist requisitions, Job Analysis, interview questions/mappings, responses, scores, or authoritative relationship-resolution results. +- The authority protocol is not proof that a concrete production adapter performs tenant/database/API checks correctly; production adapters still need executable integration evidence and immutable authority/audit records. +- Plan and activation-receipt HMAC seals and live-identity provenance exist only for the lifetime of their in-process objects. They are not portable signatures, durable verification credentials, or key-management facilities. +- Directly constructed receipt values are intentionally unusable as authoritative evidence until a supported future rehydration/issuance contract exists. +- Human approval remains mandatory; model output cannot activate or approve the plan. +- UUID/digest metadata and reference inequality do not establish tenant ownership, identity separation, scientific validity, fairness, or legal compliance. +- This ADR remains proposed until its exact PR head merges into protected `develop`. + +## References + +See `docs/doctoring/structured-interview-plan-references.md`. diff --git a/docs/doctoring/structured-interview-plan-references.md b/docs/doctoring/structured-interview-plan-references.md new file mode 100644 index 000000000..0c222236a --- /dev/null +++ b/docs/doctoring/structured-interview-plan-references.md @@ -0,0 +1,17 @@ +# Structured interview plan references + +These sources inform the active-PR structured-interview planning contract. They do not establish certification or replace organization-specific legal review, job analysis, validation, or adverse-impact monitoring. + +## APA 7 references + +International Organization for Standardization. (2023). *ISO 30405:2023 human resource management — Guidelines on recruitment* (2nd ed.). https://www.iso.org/standard/79488.html + +U.S. Equal Employment Opportunity Commission. (1979, March 1). *Questions and answers to clarify and provide a common interpretation of the Uniform Guidelines on Employee Selection Procedures*. https://www.eeoc.gov/laws/guidance/questions-and-answers-clarify-and-provide-common-interpretation-uniform-guidelines + +U.S. Office of Personnel Management. (n.d.). *Structured interviews*. Retrieved August 18, 2026, from https://www.opm.gov/policy-data-oversight/assessment-and-selection/structured-interviews/ + +U.S. Office of Personnel Management. (n.d.). *How do I select the competencies, or content areas, I want to assess with the structured interview?* Retrieved August 18, 2026, from https://www.opm.gov/frequently-asked-questions/assessment-policy-faq/structured-interviews/how-do-i-select-the-competencies-or-content-areas-i-want-to-assess-with-the-structured-interview/ + +## Applied boundary + +OPM describes structured interviews as standardized, job-related assessment methods using predetermined questions and common rating standards, with competencies selected from job analysis and confirmed by subject-matter experts. The UGESP guidance emphasizes documenting job relatedness and the basis for selection procedures. ISO 30405:2023 provides current recruitment-process guidance covering assessment and stakeholder management. Orgmetra therefore binds the interview plan to exact job-analysis evidence, predetermined question/rating artifacts, and accountable human review before candidate use. diff --git a/docs/traceability/structured-interview-plan.md b/docs/traceability/structured-interview-plan.md new file mode 100644 index 000000000..89fd59d6b --- /dev/null +++ b/docs/traceability/structured-interview-plan.md @@ -0,0 +1,61 @@ +# Structured interview plan traceability + +## Truth status + +**Active PR only.** Protected `develop` does not contain this structured-interview capability until the exact integrated PR head passes all required gates and merges. The active PR contains both the candidate-neutral plan contract and a transport-neutral executable activation boundary; it still does not claim that a concrete production authority adapter is already deployed. + +## Current trust-boundary repairs + +Caller-owned `tzinfo` implementations are treated as untrusted executable code. Plan generation, activation approval-time normalization, verification-time normalization, and canonical timestamp rendering convert exceptions raised by `tzinfo.utcoffset()` into field-specific governed `ValueError` and stop before authority side effects or evidence export. `test_plan_normalizes_hostile_timezone_failure_to_validation_error`, `test_activation_normalizes_hostile_timezone_failure_before_authority`, and `test_activation_receipt_normalizes_hostile_timezone_failure` bind this behavior; mutable-offset and representable-range regressions continue to prove UTC detachment and arithmetic fail-closure. + +Plan issuance distinguishes a normal full `StructuredInterviewPlan(...)` class call from direct or reentrant allocator invocation. The private metaclass arms a context-local **one-shot allocator ticket** immediately before normal class construction; the exact `__new__()` invocation consumes that ticket before field validation or caller-owned timezone callbacks can execute, and only that live identity becomes construction-eligible. `__post_init__()` must then consume that exact provenance before it can register issuance evidence. `test_object_new_clone_cannot_acquire_plan_issuance_evidence`, `test_direct_class_new_clone_cannot_acquire_plan_issuance_evidence`, and `test_timezone_callback_cannot_mint_plan_constructor_provenance` prove that `object.__new__`, direct class-allocator invocation, and allocator reentrancy from `tzinfo.utcoffset()` cannot copy valid fields and mint a new issued plan. + +Receipt construction is no longer an authorization mechanism. A directly constructed or `dataclasses.replace(...)`-created `StructuredInterviewActivationReceipt` may validate as a value shape, but it remains unissued and cannot export canonical evidence. Only `activate_structured_interview_plan(...)`, after authoritative verification and exact tenant/plan/digest/actor/time scope matching, registers the process-local receipt issuance seal. `test_private_module_sentinel_cannot_mint_verified_receipt_directly`, `test_activation_receipt_cannot_be_minted_without_verified_factory_path`, and the replacement regression bind this distinction. + +## Buyer requirement → executable evidence + +| Requirement | Contract | Evidence | +|---|---|---| +| Interview content is tied to job analysis | UUIDv4-backed exact `job_analysis_reference` + lowercase SHA-256 digest | deterministic-plan test plus wrong-namespace/value-bearing/sentinel/noncanonical/version reference and digest regressions | +| Authoritative tenant and Job scope is not inferred from identifiers | canonical non-sentinel `tenant_record_id` following the Orgmetra core operational-UUID contract; activation authority must re-resolve every plan reference in that tenant and prove requisition-to-Job-to-job-analysis binding before returning verification evidence | authoritative UUIDv7 tenant interoperability regression plus `test_authority_rejection_blocks_activation` and exact verification-scope mismatch regressions | +| Predetermined questions, their competency mapping, and rating anchors cannot drift silently | UUIDv4-backed question-set, question-to-competency-map, and rating-anchor references plus independent digests; activation authority is required to verify their authoritative provenance | invalid/value-bearing/UUIDv1-reference and digest regressions, deterministic SHA-256 test, authority rejection/mismatch regressions | +| Evidence revisions remain distinguishable and creation-bound | bounded positive plan `evidence_version` in canonical JSON; one-shot full-constructor allocator provenance plus plan construction bind a process-local creation seal, while activation receipt issuance is registered only by the verified factory after exact-scope authority checks | plan evidence-version regressions, `test_plan_issuance_integrity.py` including timezone-callback reentrancy, `test_activation_receipt_cannot_be_minted_without_verified_factory_path`, `test_private_module_sentinel_cannot_mint_verified_receipt_directly`, replacement/post-issuance rewrite, and missing-issuance-evidence fail-closed regressions | +| Every governed competency has auditable coverage evidence | exact built-in tuple containing sorted unique 1–12 canonical UUIDv4-backed competency references; `question_count >= competency_count`; separately identified and digest-bound question-to-competency mapping artifact | collection shape/order/duplicate/opacity, UUIDv1 rejection, tuple-subclass switching-evidence rejection, question-count regressions, and mapping-reference/digest regressions | +| Interview panel is accountable and bounded | exact built-in tuple containing sorted unique 2–8 canonical UUIDv4-backed `actor:` references; activation authority must re-resolve panel actors, prove resolved identities distinct, and verify eligibility/training before returning evidence | panel size/type/order/duplicate/namespace/value-bearing/UUIDv1 regressions, tuple-subclass switching-evidence rejection, plus fail-closed authority rejection path | +| High-impact activation has an accountable human actor | `StructuredInterviewActivationReceipt` binds one canonical UUIDv4 `approving_actor_reference`, fixed purpose/reason, mandatory `human_confirmation=True`, exact detached UTC approval time, and fixed `approved_for_use` state; canonical export is available only after verified-factory issuance; `StructuredInterviewActivationVerification` must explicitly return the same reviewed instant | `test_activation_executes_authority_and_returns_immutable_human_receipt`, `test_private_module_sentinel_cannot_mint_verified_receipt_directly`, `test_activation_sends_approval_time_through_authoritative_verification`, `test_verification_contract_explicitly_binds_reviewed_approval_time`, and `test_activation_rejects_verification_for_different_approval_time` | +| Activation accepts only a fully validated governed plan object | `activate_structured_interview_plan(...)` requires the exact `StructuredInterviewPlan` runtime type and requires creation-bound canonical plan evidence before authority work; duck-typed, subclassed, copied, rewritten, allocator-bypassed, reentrantly allocated, or otherwise unissued plan-shaped objects cannot bypass plan construction/issuance invariants | `test_activation_rejects_duck_typed_plan_before_authority_work`, `test_object_new_clone_cannot_acquire_plan_issuance_evidence`, `test_direct_class_new_clone_cannot_acquire_plan_issuance_evidence`, `test_timezone_callback_cannot_mint_plan_constructor_provenance`, plus remaining plan issuance-integrity regressions | +| Constructed plan evidence cannot be silently rewritten or resealed | only the normal full class-construction path can arm one allocator ticket, and the exact `__new__()` consumes it before caller callbacks; each successful `StructuredInterviewPlan` construction detaches `generated_at` to a built-in UTC instant, consumes exact live-object provenance, then registers a process-local HMAC seal outside plan-writable slots exactly once; canonical JSON and SHA-256 reject changed fields, discarded evidence, copied/allocator-bypassed identities, and repeated initialization that attempts to overwrite issuance evidence | `test_plan_detaches_mutable_generated_at_timezone_before_sealing`, `test_plan_canonical_evidence_fails_closed_after_low_level_mutation`, `test_missing_process_local_plan_issuance_evidence_fails_closed`, `test_copied_plan_has_no_transferable_process_local_issuance_evidence`, `test_object_new_clone_cannot_acquire_plan_issuance_evidence`, `test_direct_class_new_clone_cannot_acquire_plan_issuance_evidence`, `test_timezone_callback_cannot_mint_plan_constructor_provenance`, and duplicate-registration/reinitialization regressions | +| Authority review cannot observe a temporary live-plan revision | activation captures creation-bound canonical plan JSON and its SHA-256 before the call and supplies only those detached built-in values to the authority; the caller's live `StructuredInterviewPlan` never crosses the authority contract, so change-and-restore (ABA) mutation cannot alter the reviewed revision; non-restored mutation still fails the post-call creation-seal check | `test_activation_authority_receives_detached_creation_bound_plan_evidence` plus `test_activation_detaches_plan_evidence_from_authority_time_aba_mutation` | +| Plan generation time has one stable audit meaning | caller-owned `generated_at` is detached into a built-in UTC datetime during plan construction before creation-seal registration; constructor privilege has already been consumed before offset evaluation; caller-controlled offset evaluation failures, reentrant allocator attempts, naive/unknown-offset values, and out-of-range UTC normalization fail closed, and later mutation of caller-owned `tzinfo` state cannot change or invalidate the issued instant | `test_plan_detaches_mutable_generated_at_timezone_before_sealing`, `test_plan_normalizes_hostile_timezone_failure_to_validation_error`, `test_timezone_callback_cannot_mint_plan_constructor_provenance`, `test_plan_rejects_utc_normalization_beyond_datetime_min_as_validation_error`, plus naive/unknown-offset/offset/fractional-time plan regressions | +| Approval time has one stable audit meaning | caller-owned `approved_at` is detached into a built-in UTC datetime before chronology and authority work; caller-controlled offset evaluation failures, naive/unknown-offset values, and out-of-range UTC normalization fail closed before authority side effects; the same snapshot crosses the authority and receipt boundaries, and canonical rendering reuses the same fail-closed detachment | `test_activation_normalizes_hostile_timezone_failure_before_authority`, `test_activation_receipt_normalizes_hostile_timezone_failure`, `test_activation_rejects_naive_approval_time_before_authority_work`, `test_activation_rejects_approval_time_with_unknown_offset`, `test_activation_rejects_utc_normalization_beyond_datetime_max_before_authority`, `test_activation_freezes_mutable_timezone_before_authority_and_receipt`, and pre-generation chronology regression | +| Authority evidence cannot be replayed across plan/actor/time scope | authority result must match the plan tenant, interview-plan reference, exact plan SHA-256 digest, approving actor, and normalized approval instant supplied to activation before receipt issuance is registered | parameterized `test_activation_rejects_authority_evidence_for_other_scope` plus `test_activation_rejects_verification_for_different_approval_time` | +| Authority verification cannot mix revisions between field reads | the exact verification contract is a runtime-immutable `NamedTuple`; exact-type enforcement rejects behavioral subclasses, `object.__setattr__` cannot rewrite tuple fields, and activation unpacks the tuple once before validation/scope comparison/receipt issuance | `test_verification_contract_cannot_be_rewritten_with_object_setattr` plus `test_activation_rejects_verification_subclass_before_evidence_reads_can_diverge` | +| Authority evidence itself is value-minimized and integrity-bound | canonical UUIDv4 `activation_verification:` reference + lowercase SHA-256 digest + explicit reviewed UTC approval instant; verification and receipt representations are fully redacted | `test_activation_rejects_untrusted_authority_evidence_shape`, approval-time binding regressions, plus exact verification/receipt repr and canonical JSON assertions | +| Portable governance metadata is value-minimized without duplicating tenant identity policy | authoritative `tenant_record_id` must be canonical/non-sentinel under the core HRIS contract; package-owned trust references require canonical non-sentinel UUIDv4 plus their expected prefix; reason vocabularies are closed; fixed `review_state` and `next_action` require exact built-in strings | authoritative UUIDv7 tenant interoperability regression, scalar/collection privacy regressions, UUIDv1 reference regressions, activation evidence-shape regressions, fixed-governance string-subclass regressions, and `dataclasses.replace(...)` bypass regressions | +| Routine logs do not reveal plan or activation correlations | custom redacted `StructuredInterviewPlan.__repr__`, `StructuredInterviewActivationVerification.__repr__`, and `StructuredInterviewActivationReceipt.__repr__` | exact repr regressions prove references, evidence digests, and reviewed time are absent | +| Planning and activation evidence remain candidate-neutral | neither plan nor activation receipt has candidate identity, response, score, demographic attribute, compensation value, or model-output fields | canonical JSON regressions plus contract surface review | +| Generated evidence cannot self-approve a plan | plan remains `requires_human_approval`; activation requires the injected authoritative host boundary and a distinct explicit approving-actor parameter; receipt canonical export requires issuance registration performed only after successful exact-scope host verification | scalar fail-closed plan regressions, `test_authority_rejection_blocks_activation`, non-verification-result regression, direct-unissued-receipt regression, and private-sentinel regression | +| Audit correlation is deterministic without losing temporal precision | caller-owned plan-generation and approval times are detached to built-in UTC instants before their respective trust boundaries; hostile offset evaluation, reentrant plan-allocation attempts, and unrepresentable UTC normalization are rejected as governed validation/integrity failures; canonical JSON preserves fractional precision; exact SHA-256 binds plan and factory-issued activation receipt evidence | plan hostile-timezone/reentrant-timezone/mutable-timezone/naive/unknown-offset/range-boundary/offset/fractional-time regressions, plan issuance-integrity regressions, activation hostile-timezone/UTC-snapshot/range-boundary regressions, and canonical/digest assertions | +| Direct construction or post-issuance in-memory rewriting cannot bypass invariants | plan and receipt shapes re-run value validation; direct/replaced receipt objects remain unissued and cannot export canonical evidence; exact runtime types protect plan collections/governance text; direct or reentrant plan allocator calls cannot acquire/retain full-constructor provenance; issued plans and receipts verify process-local issuance evidence before canonical export | direct constructor/allocator/reentrant-allocator, private-sentinel, tuple-subclass switching-evidence, fixed-governance string-subclass, `dataclasses.replace(...)`, plan low-level rewrite/copy/missing/reseal, and receipt hostile-timezone/low-level rewrite/missing-seal regressions | + +## Evidence boundary + +The plan object is creation-bound before activation begins. A private metaclass arms a context-local one-shot allocator ticket immediately before the normal full `StructuredInterviewPlan(...)` class call. The exact `__new__()` that begins that construction consumes the ticket before any field validation or caller-owned `tzinfo.utcoffset()` callback can execute, then records construction eligibility only for that exact live object. Direct `object.__new__`, direct `StructuredInterviewPlan.__new__(StructuredInterviewPlan)`, and allocator calls reached reentrantly from caller timezone code therefore cannot manufacture or retain constructor provenance by copying valid fields and manually invoking `__post_init__()`. Successful full construction then validates and detaches caller-owned `generated_at` using one concrete offset into a built-in UTC `datetime`, so later changes to the original mutable `tzinfo` object cannot alter or invalidate the issued plan instant. Offset evaluation itself is a trust boundary: arbitrary exceptions raised by caller-owned `tzinfo.utcoffset()` are converted into the field-specific governed `ValueError` and stop construction before issuance. If offset arithmetic would cross `datetime.min` or `datetime.max`, construction converts the arithmetic overflow into the same field-specific governed `ValueError` and stops before issuance-seal registration. Construction consumes the exact live-object provenance, computes an HMAC over the exact canonical payload, and registers it in process-local state outside plan-writable slots. Registration for one live identity is single-use; repeated `__post_init__()` cannot overwrite the original issuance record after low-level field mutation. `canonical_json()` renders the current payload once, requires issued identity for that exact live object, and verifies the creation seal with constant-time comparison before returning bytes; `sha256_digest()` is downstream of the same guard. Low-level `object.__setattr__` rewriting therefore cannot silently redefine the plan after construction, and `copy.copy`/other reconstructed identities do not inherit issuance authority merely by carrying the same fields. Missing, mismatched, duplicate, allocator-bypassed, or reentrantly allocated issuance evidence fails closed. The allocator ticket, live-identity registry, and HMAC are intentionally same-process runtime integrity evidence only—not a hostile-interpreter capability boundary, durable audit record, portable signature, rehydration protocol, or substitute for the host immutable audit/outbox boundary. + +The active PR implements an executable activation orchestration boundary, not merely a `next_action` string. `activate_structured_interview_plan(...)` first requires the exact governed `StructuredInterviewPlan` runtime type and obtains creation-bound canonical JSON. Tenant/interview-plan scope, canonical `generated_at`, and SHA-256 are derived from that same string. Caller-owned `approved_at` is detached using one concrete UTC offset into a built-in UTC datetime; hostile/broken offset evaluation, naive or unknown-offset values, and UTC normalization beyond the representable datetime range all fail as field-specific validation before authority work. The injected `StructuredInterviewActivationAuthority` receives the exact built-in canonical JSON string, its exact digest, the approving actor, and the built-in UTC approval snapshot. It never receives the caller's live plan object. This removes the ABA window in which an authority could observe a temporary modified plan and restore it before a post-call equality/seal check. A retained external live-plan alias may still be mutated by untrusted code, but it cannot change the detached evidence reviewed through this contract; any mutation left in place is additionally caught by the post-authority creation-seal check. + +`StructuredInterviewActivationVerification` explicitly carries the reviewed approval instant and is implemented as a runtime-immutable `NamedTuple`, not a merely frozen dataclass. Activation requires the exact verification runtime type, rejects behavioral subclasses before evidence reads, unpacks the tuple once, normalizes the returned approval time through the same fail-closed timezone boundary, validates the unpacked values, and compares tenant, plan reference, plan digest, approving actor, and approval time against the pre-call request. Tuple field descriptors reject `object.__setattr__`, closing the mixed-revision window where an authority-retained alias could previously rewrite one valid field between sequential reads. + +`StructuredInterviewActivationReceipt` construction validates the receipt's shape but does not create authoritative issuance evidence. Direct construction and `dataclasses.replace(...)` therefore produce unissued values whose `canonical_json()` and `sha256_digest()` fail closed because no process-local issuance seal exists. A module-private legacy sentinel is deliberately absent from the constructor and carries no authority. Only after `activate_structured_interview_plan(...)` has accepted exact runtime verification evidence, normalized its returned approval instant, validated every returned field, and matched tenant, plan reference, digest, approving actor, and approval time to the request does the factory create the receipt and register its process-local HMAC seal. Canonical export then recomputes the seal over the current payload using constant-time comparison. Any low-level post-issuance rewrite or loss of issuance evidence fails closed. This process-local seal is runtime integrity evidence only: it is not a durable audit store, signing key, cross-process verification format, or substitute for the host's immutable authoritative audit/outbox record. + +The plan boundary also requires exact built-in tuple containers for `competency_references` and `panel_actor_references`, plus exact built-in strings for fixed `review_state` and `next_action` evidence. This closes a Python runtime-subclass gap where caller-controlled iteration or equality behavior could satisfy construction checks and then serialize different immutable evidence later. + +The authority protocol is intentionally not a second data store or cross-service SQL path. A concrete production adapter remains responsible for authoritative tenant-scoped re-resolution of every reference represented by the detached canonical plan evidence, requisition-to-Job-to-job-analysis relationship checks, question/mapping/rating provenance, panel identity separation, eligibility, training, and review of the exact approval instant. Its verification evidence must return that reviewed normalized instant and bind it through the owner's immutable evidence/audit implementation; the adapter must raise rather than return verification evidence when any required check fails. The current tests prove orchestration fail-closure, exact-plan runtime boundary, creation-bound plan integrity, stable and representable plan-generation time, caller-timezone exception normalization, constructor-provenance reentrancy fail-closure, detached immutable authority inputs, approval-time semantics and range fail-closure, runtime-immutable verification evidence, factory-bound receipt issuance, and exact evidence binding; they do **not** prove that a particular deployed adapter already performs database/API resolution correctly. + +The mapping reference/digest proves which approved mapping artifact was bound to the plan and detects later artifact drift. Plan `evidence_version` identifies the canonical plan-evidence revision; the activation receipt separately binds the exact plan digest, approving actor, authority-verification evidence, approval time, purpose/reason, confirmation, state, and its own evidence version. Package-owned UUIDv4 trust references keep timestamp/node-bearing UUIDv1 suffixes outside portable evidence, while `tenant_record_id` deliberately inherits the authoritative Orgmetra operational-UUID contract so the leaf package does not reject valid existing tenants. + +Neither UUID form, reference inequality, digest metadata, runtime issuance seals, nor the authority protocol by itself proves tenant ownership, relationship validity, panel identity separation, eligibility, training, scientific validity, fairness, or legal compliance. Production hosts must satisfy those obligations at the authoritative boundary and preserve purpose-bound authorization, least privilege, retention/export controls, and immutable audit evidence. + +## Out of scope + +This slice does not persist interview plans, questions, mappings, responses, scores, candidate PII, authoritative identity-resolution results, adverse-impact statistics, validity-study results, or final selection decisions. It does not ship a concrete production authority adapter or claim that a structured interview is legally compliant or scientifically validated merely because a plan or activation receipt exists. The process-local seals do not authorize cross-process reconstruction or replace persisted authoritative audit evidence. Those claims require separate job-analysis, selection-validation, fairness, accessibility/accommodation, operational, deployment, and human-decision evidence. diff --git a/packages/interview-plan/CHANGELOG.md b/packages/interview-plan/CHANGELOG.md new file mode 100644 index 000000000..58adfcb1c --- /dev/null +++ b/packages/interview-plan/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +## 0.1.0 - Unreleased + +### Added + +- Candidate-neutral `StructuredInterviewPlan` binding an approved requisition and Job to exact job-analysis, question-set, question-to-competency mapping, rating-anchor, competency, and interviewer-panel evidence. +- Fail-closed direct-construction validation, deterministic canonical JSON/SHA-256 audit correlation, explicit human approval state, and 100% owned statement/branch regression coverage. +- Bounded positive `evidence_version` in canonical evidence so materially revised plans have explicit immutable revision identity. +- Executable `StructuredInterviewActivationAuthority` / `activate_structured_interview_plan(...)` boundary that requires exact-scope authoritative verification before emitting any approval evidence. +- Value-minimized `StructuredInterviewActivationReceipt` binding the exact plan digest, accountable approving actor, authority-verification reference/digest, fixed purpose/reason, evidence version, precision-preserving approval time, mandatory human confirmation, and fixed `approved_for_use` state. + +### Changed + +- Require a separately identified and SHA-256-bound question-to-competency mapping artifact so question count alone cannot be treated as proof that every governed competency is assessed. +- Revalidate evidence-version changes through direct construction and `dataclasses.replace(...)`; changing the version changes canonical SHA-256 correlation. +- Keep package-owned trust-bearing reference suffixes canonical non-sentinel UUIDv4, while `tenant_record_id` follows Orgmetra's authoritative canonical non-sentinel operational UUID contract so valid core tenant identities are not rejected by this leaf package. +- Replace prose-only activation assurance with executable fail-closed orchestration: the injected host authority must reject failed tenant/relationship/provenance/panel checks, and returned evidence must match the exact tenant, interview-plan reference, plan digest, approving actor, and reviewed approval instant before a receipt can be issued. +- Detach caller-owned plan `generated_at` into one built-in UTC snapshot before creation-seal registration so later mutation of a custom `tzinfo` cannot change or invalidate an already-issued plan instant. +- Detach caller-owned `approved_at` into one built-in UTC snapshot before chronology or authority work, pass that snapshot to the authority, require the returned verification to carry the same reviewed instant, and write only that immutable snapshot into the receipt. +- Normalize caller-controlled `tzinfo.utcoffset()` failures into field-specific `ValueError` at plan-generation, activation, verification-time normalization, and canonical timestamp boundaries so arbitrary timezone exceptions cannot escape governed APIs or reach authority side effects. +- Normalize UTC-offset arithmetic that would cross Python `datetime` bounds into field-specific `ValueError` for both plan generation and approval time, failing before plan issuance or activation authority side effects instead of leaking `OverflowError`. +- Require the exact governed `StructuredInterviewPlan` runtime type before any activation authority work, preventing duck-typed or subclassed plan-shaped objects from bypassing construction invariants and producing approval evidence. +- Pass only creation-bound canonical plan JSON plus its exact SHA-256 digest across `StructuredInterviewActivationAuthority`; the authority no longer receives the caller's live plan object, so temporary change-and-restore (ABA) mutation cannot change the plan revision actually reviewed. +- Derive activation tenant/interview-plan scope from the same canonical plan bytes supplied to the authority and retain the post-authority creation-seal check for any non-restored live-object mutation. +- Make `StructuredInterviewActivationVerification` a runtime-immutable `NamedTuple`, reject subclasses, and unpack its exact tuple once before validation so `object.__setattr__` cannot create mixed authority-evidence revisions between field reads. +- Bind every constructed `StructuredInterviewPlan` to a single-registration process-local creation seal outside plan-writable slots; canonical JSON and SHA-256 export now fail closed if low-level mutation changes the plan, if copied/reconstructed objects lack creation-bound issuance evidence, or if the same live identity attempts to renew its seal through repeated initialization. +- Make plan-construction provenance a one-shot metaclass-mediated allocator ticket consumed by the exact `StructuredInterviewPlan.__new__()` call before field validation or caller-controlled timezone callbacks can run; `object.__new__`, direct class-allocator calls, and reentrant allocator calls from `tzinfo.utcoffset()` therefore cannot copy otherwise valid fields and mint fresh issuance evidence by manually invoking initialization. +- Remove constructor-token authorization from `StructuredInterviewActivationReceipt`: direct construction and `dataclasses.replace(...)` create unissued value objects that cannot export canonical evidence, while only `activate_structured_interview_plan(...)` registers the process-local receipt seal after authoritative verification and exact-scope matching succeed. A module-private sentinel no longer appears in the receipt constructor and cannot mint approval evidence. +- Expand Structured Interview Plan Quality path triggers to cover repository-level Python/test configuration and `.gitignore` inputs that can change test collection, execution, or clean-checkout behavior, while retaining package, dependency-lock, workflow, ADR, doctoring, and traceability triggers. + +### Security and privacy + +- Reject timestamp/node-bearing UUIDv1 values in package-owned trust references as well as human-readable/value-bearing reference metadata before serialization; tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. +- Close plan `reason_code` to `approved_requisition_interview` and activation governance to fixed `structured_interview_activation` / `human_approved_plan_activation` codes. +- Require exact built-in tuple containers for competency/panel reference collections and exact built-in strings for fixed `review_state` / `next_action` evidence before canonicalization, preventing caller-controlled runtime subclasses from passing validation and later switching serialized immutable evidence. +- Treat caller-owned timezone implementations as untrusted code: offset evaluation and canonical-time rendering fail closed to governed field-specific validation errors rather than leaking arbitrary exceptions across plan, activation, or receipt boundaries. +- Normalize both plan-generation and approval-time evidence before creation sealing or authority review so caller-controlled mutable `tzinfo` state cannot make one governed instant later represent a different UTC instant. +- Consume constructor provenance before any caller-owned `tzinfo` callback is invoked so reentrant timezone code cannot retain an allocator-created plan with construction eligibility and later turn copied fields into a second issued plan. +- Prevent callers from converting a module-visible private sentinel into human-approval authority: receipt issuance evidence is now registered exclusively inside the verified activation factory after all host-verification and exact-scope checks pass. +- Redact `StructuredInterviewPlan`, `StructuredInterviewActivationVerification`, and `StructuredInterviewActivationReceipt` representations so routine logs and assertion failures do not expose sensitive correlations or evidence digests. +- Treat the process-local plan and activation-receipt seals plus live-identity provenance strictly as in-memory issuance-integrity evidence, not as durable audit stores, portable signatures, cross-process verification keys, or substitutes for the host's immutable audit/outbox contract. +- State explicitly that UUID/digest correlation, reference-string inequality, runtime issuance seals, and the authority protocol do not by themselves prove tenant ownership, authoritative relationship validity, actor identity separation, scientific validity, fairness, or legal compliance. diff --git a/packages/interview-plan/README.md b/packages/interview-plan/README.md new file mode 100644 index 000000000..72ee8139f --- /dev/null +++ b/packages/interview-plan/README.md @@ -0,0 +1,27 @@ +# Orgmetra structured interview plan + +`orgmetra-interview-plan` creates candidate-neutral evidence for approving a structured interview **before** it is used with applicants and exposes a fail-closed activation boundary that records accountable human approval only after authoritative host verification succeeds. + +The plan binds one requisition and authoritative Job to versioned job-analysis evidence, a predetermined question set, an exact question-to-competency mapping artifact, rating anchors, job-related competency references, and a bounded interviewer panel. The question set and mapping each carry their own immutable SHA-256 evidence digest, so a count of questions cannot be mistaken for proof that every governed competency is actually assessed. It keeps candidate identity, responses, scores, demographic attributes, model output, credentials, provider data, and free-form personal/value-bearing reason text out of the packet. + +Every plan also carries a bounded positive `evidence_version` (1 through 2147483647) in canonical evidence. Version changes therefore change the SHA-256 audit correlation, and direct construction plus `dataclasses.replace(...)` revalidate the version fail closed. Version 1 is the default for the initial evidence schema; callers must increment it when the governed plan evidence is materially revised rather than treating a digest alone as semantic version identity. + +The public `tenant_record_id` follows Orgmetra's authoritative canonical non-sentinel operational UUID contract rather than imposing a second UUID-version policy at this leaf package. Packet-owned trust-bearing references remain canonical, non-sentinel UUIDv4 values and additionally require their expected namespace. That applies to the interview plan, requisition, Job, Job Analysis, question set, question-to-competency map, rating anchors, competencies, panel actors, accountable approving actor, and activation-verification evidence. UUIDv1 and other non-v4 suffixes fail closed for those packet-owned references so timestamp/node-bearing or otherwise nonconforming identifiers cannot be presented as this package's opaque trust references. Human-readable/value-bearing suffixes such as names, job labels, protected-attribute labels, compensation values, or interviewer names are also rejected before canonical evidence is produced. The plan reason vocabulary remains closed to `approved_requisition_interview`; activation evidence uses fixed `structured_interview_activation` / `human_approved_plan_activation` governance codes. + +Opaque identities and digests identify the evidence being reviewed; they do not prove tenant ownership, authoritative relationships, or actor identity. Immediately before activation, the host must re-resolve every plan reference within `tenant_record_id`, prove the requisition-to-Job-to-job-analysis binding, verify question-set, question-to-competency mapping, and rating-anchor provenance, re-resolve every panel actor, prove the resolved panel identities are distinct, and verify panel eligibility and training. + +A successfully constructed `StructuredInterviewPlan` is creation-bound before activation. Before its canonical payload is sealed, caller-owned `generated_at` is detached using one concrete UTC offset into a built-in `datetime` with `timezone.utc`; later changes to a custom mutable `tzinfo` therefore cannot change or invalidate the already-issued plan instant. Caller-provided timezone implementations are untrusted code: if `tzinfo.utcoffset()` raises, Orgmetra converts that failure into the same field-specific governed `ValueError` instead of leaking the caller exception. If the offset would place the instant outside Python's representable `datetime` range, construction likewise fails with field-specific `ValueError` rather than leaking `OverflowError`. Constructor provenance is a one-shot context-local allocator ticket: the normal class call arms it, and the exact `StructuredInterviewPlan.__new__()` invocation consumes it **before** field validation or caller-owned timezone code can execute. Reentrant code reached through `tzinfo.utcoffset()` therefore cannot retain a second allocator-created object with ambient constructor privilege. The package then computes a process-local HMAC over its exact canonical payload and stores the seal outside plan-writable dataclass slots. One live plan identity can register that issuance evidence only once: rerunning `__post_init__()` cannot renew the seal after a low-level field rewrite. `canonical_json()` and `sha256_digest()` require matching creation evidence for the exact live object, so a low-level `object.__setattr__` rewrite cannot silently redefine the plan after construction and a copied/reconstructed object cannot inherit issuance authority merely by carrying the same fields. Missing, mismatched, duplicate, allocator-bypassed, or reentrantly allocated issuance evidence fails closed. This seal and constructor-provenance mechanism are only same-process runtime-integrity evidence: they are not a hostile-interpreter capability boundary, durable signature, rehydration credential, persisted audit record, or replacement for the host's immutable audit/outbox evidence. + +`activate_structured_interview_plan(...)` makes the authoritative control flow executable without duplicating authoritative storage. The boundary accepts only the exact governed `StructuredInterviewPlan` runtime type and requires its creation-bound canonical evidence, so a duck-typed, subclassed, copied, or rewritten plan-shaped object cannot bypass construction/issuance invariants. Before authority work, activation captures the exact creation-bound canonical plan JSON and SHA-256 digest, derives tenant/interview-plan scope from those bytes, and detaches caller-owned `approved_at` into one built-in UTC snapshot. Approval-time offset evaluation failures and normalization that would leave Python's representable `datetime` range both fail as field-specific validation before any authority call, so hostile/broken timezone code cannot leak arbitrary exceptions or trigger authoritative side effects. The injected `StructuredInterviewActivationAuthority` receives **only** that built-in canonical JSON string, its exact digest, the approving actor, and the normalized approval instant—never the caller's live `StructuredInterviewPlan` object. A retained plan alias can therefore be changed and restored while authority work runs without changing the immutable plan evidence the authority actually reviews; a non-restored mutation still fails the post-authority creation-seal check. + +The authority must return an exact `StructuredInterviewActivationVerification`. This verification contract is a `NamedTuple`, so its trust-bearing tuple fields cannot be rewritten through `object.__setattr__` after return. Activation rejects subclasses before reading evidence, unpacks the exact tuple once, detaches the returned approval time into built-in UTC using the same fail-closed timezone boundary, validates the unpacked values, and requires the complete scope—including tenant, interview-plan reference, plan digest, approving actor, and approval instant—to equal the request. This removes the mixed-revision window that existed when a merely frozen dataclass could still be rewritten between field reads. + +`StructuredInterviewActivationReceipt` is a value-minimized receipt shape, not a constructor-level authorization primitive. Direct construction and `dataclasses.replace(...)` still validate fixed fields, references, digests, and timestamps, but those objects are **unissued** and cannot export `canonical_json()` or `sha256_digest()`. Only `activate_structured_interview_plan(...)`, after authoritative verification succeeds and the returned tenant/plan/digest/actor/approval-time scope exactly matches the request, registers the process-local HMAC issuance seal for that exact live receipt. A module-private sentinel is not accepted by the receipt constructor and carries no authority. The issued receipt records the exact plan digest, accountable approving actor, authority-verification reference/digest, purpose, reason, evidence version, precision-preserving approval time, mandatory human confirmation, and fixed `approved_for_use` state. It remains value-minimized and cannot contain candidate identity, responses, scores, protected-attribute values, or free-form model output. `repr(receipt)` is fully redacted as `StructuredInterviewActivationReceipt()`; canonical timestamp rendering reuses the same detached UTC validation; and mutation, missing issuance evidence, or reconstruction/replacement without factory issuance fails closed before canonical export. The seal is same-process runtime-integrity evidence only, not a durable signature, portable attestation, rehydration credential, persisted audit record, or replacement for the host immutable audit/outbox boundary. + +The plan object itself remains pending human review: `human_confirmation_required` is fixed to `True`, `review_state` is fixed to `requires_human_approval`, and its next action requires authoritative resolution before activation. Direct construction and `dataclasses.replace(...)` re-run the same fail-closed plan invariants. The activation receipt is separate evidence and does not mutate or rewrite the reviewed plan. + +`repr(plan)` is fully redacted as `StructuredInterviewPlan()`, so routine logs and assertion failures do not expose governance correlations or evidence digests. Canonical JSON remains the explicit evidence serialization boundary and is released only after creation-bound integrity verification succeeds. + +For consistency and immutable audit correlation, evidence digests are lowercase SHA-256, competency and panel tuples must be sorted and unique, and timestamps are timezone-aware RFC 3339 values with fractional precision preserved. Opaque identifiers and references are value-minimized correlation metadata, not anonymous data, and remain subject to purpose-bound authorization, least privilege, retention/export controls, and audit. + +This package does not itself persist Job Analysis, requisitions, candidates, interview responses, scores, or authoritative identity-resolution results. The process-local seals do not authorize cross-process reconstruction. The authority protocol is an execution contract, not a substitute for a concrete tenant-scoped adapter. Production hosts must implement the published detached-evidence authority contract over authoritative Orgmetra boundaries, bind the exact normalized approval instant into immutable authority evidence, and preserve immutable audit/outbox evidence for any later authoritative write. diff --git a/packages/interview-plan/pyproject.toml b/packages/interview-plan/pyproject.toml new file mode 100644 index 000000000..1bb874772 --- /dev/null +++ b/packages/interview-plan/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-interview-plan" +version = "0.1.0" +description = "Candidate-neutral structured-interview plan 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_interview_plan", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/interview-plan/src/orgmetra_interview_plan/__init__.py b/packages/interview-plan/src/orgmetra_interview_plan/__init__.py new file mode 100644 index 000000000..3aefd52c6 --- /dev/null +++ b/packages/interview-plan/src/orgmetra_interview_plan/__init__.py @@ -0,0 +1,17 @@ +"""Public structured-interview planning and activation contracts for Orgmetra.""" +from .activation import ( + StructuredInterviewActivationAuthority, + StructuredInterviewActivationReceipt, + StructuredInterviewActivationVerification, + activate_structured_interview_plan, +) +from .plan import StructuredInterviewPlan, build_structured_interview_plan + +__all__ = [ + "StructuredInterviewActivationAuthority", + "StructuredInterviewActivationReceipt", + "StructuredInterviewActivationVerification", + "StructuredInterviewPlan", + "activate_structured_interview_plan", + "build_structured_interview_plan", +] diff --git a/packages/interview-plan/src/orgmetra_interview_plan/activation.py b/packages/interview-plan/src/orgmetra_interview_plan/activation.py new file mode 100644 index 000000000..b699e857a --- /dev/null +++ b/packages/interview-plan/src/orgmetra_interview_plan/activation.py @@ -0,0 +1,328 @@ +"""Executable human-approval boundary for governed structured-interview plans. + +The authority adapter is owned by the Orgmetra host. It MUST return verification +only after re-resolving detached creation-bound plan evidence inside the exact +tenant, proving the requisition-to-Job-to-job-analysis binding, verifying +question/mapping/rating provenance, resolving distinct panel actors, confirming +panel eligibility and training, and reviewing the exact approval instant carried +into the receipt. Any failed authoritative check must raise instead of returning +verification evidence. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from hashlib import sha256 +import hmac +import json +import secrets +from threading import RLock +from typing import NamedTuple, Protocol +from weakref import finalize + +from .plan import ( + StructuredInterviewPlan, + _canonical_timestamp, + _validate_code, + _validate_digest, + _validate_operational_uuid, + _validate_reference, +) + +_PURPOSE_CODE = "structured_interview_activation" +_REASON_CODE = "human_approved_plan_activation" +_ACTIVATION_STATE = "approved_for_use" +_MAX_EVIDENCE_VERSION = 2_147_483_647 + + +def _snapshot_utc_datetime(value: datetime, field_name: str) -> datetime: + """Detach one caller-owned aware datetime into a representable built-in UTC instant.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + try: + offset = value.utcoffset() + except Exception as exc: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + if type(offset) is not timedelta: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + local_naive = value.replace(tzinfo=None) + try: + normalized = local_naive - offset + except OverflowError as exc: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + return normalized.replace(tzinfo=timezone.utc) + + +class StructuredInterviewActivationVerification(NamedTuple): + """Runtime-immutable authoritative host evidence returned after activation checks pass.""" + + tenant_record_id: str + interview_plan_reference: str + plan_digest: str + approving_actor_reference: str + authority_evidence_reference: str + authority_evidence_digest: str + approved_at: datetime + + def __repr__(self) -> str: + """Return a redacted representation suitable for routine logs and failures.""" + return "StructuredInterviewActivationVerification()" + + +class StructuredInterviewActivationAuthority(Protocol): + """Host contract that fail-closes unless every authoritative activation check passes.""" + + def verify_activation( + self, + *, + plan_canonical_json: str, + plan_digest: str, + approving_actor_reference: str, + approved_at: datetime, + ) -> StructuredInterviewActivationVerification: + """Verify detached creation-bound plan bytes and return exact-scope evidence.""" + + +def _build_activation_surface(): + """Build the public receipt type and activation factory around a lexical seal vault. + + Receipt issuance state deliberately lives in this lexical scope rather than module + attributes. Python process-local integrity is defense-in-depth, not a sandbox against + arbitrary same-process introspection; production hosts must keep untrusted code out of + the application trust domain and persist authoritative audit/outbox evidence separately. + """ + receipt_seal_key = secrets.token_bytes(32) + receipt_seals: dict[int, str] = {} + receipt_seals_lock = RLock() + + def seal_receipt(payload_json: str) -> str: + """Return a process-local HMAC over one exact canonical receipt payload.""" + return hmac.new( + receipt_seal_key, + payload_json.encode("utf-8"), + "sha256", + ).hexdigest() + + def discard_receipt_seal(receipt_id: int) -> None: + """Discard lexical issuance evidence after the issued receipt is collected.""" + with receipt_seals_lock: + receipt_seals.pop(receipt_id, None) + + def register_receipt_seal(receipt: object, payload_json: str) -> None: + """Register one fresh receipt after verified factory activation succeeds. + + This lexical helper has one call site, after constructing a fresh receipt. Repeated + ``__post_init__`` validation therefore cannot reach it or renew issuance evidence. + """ + receipt_id = id(receipt) + with receipt_seals_lock: + receipt_seals[receipt_id] = seal_receipt(payload_json) + finalize(receipt, discard_receipt_seal, receipt_id) + + def authoritative_receipt_seal(receipt: object) -> str | None: + """Read lexical issuance evidence without trusting receipt-writable state.""" + with receipt_seals_lock: + return receipt_seals.get(id(receipt)) + + @dataclass(frozen=True, slots=True, repr=False, weakref_slot=True) + class StructuredInterviewActivationReceipt: + """Value-minimized activation receipt whose trusted export requires factory issuance.""" + + tenant_record_id: str + interview_plan_reference: str + plan_digest: str + approving_actor_reference: str + authority_evidence_reference: str + authority_evidence_digest: str + approved_at: datetime + purpose_code: str = _PURPOSE_CODE + reason_code: str = _REASON_CODE + evidence_version: int = 1 + human_confirmation: bool = True + activation_state: str = _ACTIVATION_STATE + + def __post_init__(self) -> None: + """Reject forged, ambiguous, or weakened receipt values before possible issuance.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference( + self.interview_plan_reference, + "interview_plan", + "interview_plan_reference", + ) + _validate_digest(self.plan_digest, "plan_digest") + _validate_reference( + self.approving_actor_reference, + "actor", + "approving_actor_reference", + ) + _validate_reference( + self.authority_evidence_reference, + "activation_verification", + "authority_evidence_reference", + ) + _validate_digest(self.authority_evidence_digest, "authority_evidence_digest") + _canonical_timestamp(self.approved_at, "approved_at") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain structured_interview_activation") + _validate_code(self.reason_code, "reason_code") + if self.reason_code != _REASON_CODE: + raise ValueError("reason_code must remain human_approved_plan_activation") + if ( + type(self.evidence_version) is not int + or not 1 <= self.evidence_version <= _MAX_EVIDENCE_VERSION + ): + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + if self.human_confirmation is not True: + raise ValueError("human confirmation is mandatory for interview-plan activation") + if self.activation_state != _ACTIVATION_STATE: + raise ValueError("activation_state must remain approved_for_use") + + def __repr__(self) -> str: + """Return a redacted representation suitable for routine logs.""" + return "StructuredInterviewActivationReceipt()" + + def _canonical_json_unchecked(self) -> str: + """Render canonical activation bytes without process-local issuance state.""" + payload = { + "activation_state": self.activation_state, + "approved_at": _canonical_timestamp(self.approved_at, "approved_at"), + "approving_actor_reference": self.approving_actor_reference, + "authority_evidence_digest": self.authority_evidence_digest, + "authority_evidence_reference": self.authority_evidence_reference, + "evidence_version": self.evidence_version, + "human_confirmation": self.human_confirmation, + "interview_plan_reference": self.interview_plan_reference, + "plan_digest": self.plan_digest, + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def canonical_json(self) -> str: + """Return factory-issued canonical JSON for immutable audit correlation.""" + canonical = self._canonical_json_unchecked() + authoritative_seal = authoritative_receipt_seal(self) + if ( + type(authoritative_seal) is not str + or not hmac.compare_digest( + seal_receipt(canonical), + authoritative_seal, + ) + ): + raise ValueError( + "structured interview activation receipt changed after activation receipt issuance" + ) + return canonical + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact factory-issued activation receipt.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + def activate_structured_interview_plan( + *, + plan: StructuredInterviewPlan, + authority: StructuredInterviewActivationAuthority, + approving_actor_reference: str, + approved_at: datetime, + ) -> StructuredInterviewActivationReceipt: + """Activate one exact plan only after authoritative host verification succeeds. + + The host authority receives detached creation-bound plan evidence and the + normalized approval instant, never the caller's live plan object. Receipt + issuance is registered only after verification type, fields, and complete + tenant/plan/digest/actor/time scope have all matched the request. + """ + if type(plan) is not StructuredInterviewPlan: + raise TypeError("plan must be a StructuredInterviewPlan") + approved_at_snapshot = _snapshot_utc_datetime(approved_at, "approved_at") + _validate_reference(approving_actor_reference, "actor", "approving_actor_reference") + + plan_canonical_json = plan.canonical_json() + plan_payload = json.loads(plan_canonical_json) + plan_generated_at = datetime.fromisoformat( + plan_payload["generated_at"].replace("Z", "+00:00") + ) + if approved_at_snapshot < plan_generated_at: + raise ValueError("approved_at must not precede plan generated_at") + plan_digest = sha256(plan_canonical_json.encode("utf-8")).hexdigest() + plan_tenant_record_id = plan_payload["tenant_record_id"] + interview_plan_reference = plan_payload["interview_plan_reference"] + + verification = authority.verify_activation( + plan_canonical_json=plan_canonical_json, + plan_digest=plan_digest, + approving_actor_reference=approving_actor_reference, + approved_at=approved_at_snapshot, + ) + plan.canonical_json() + if type(verification) is not StructuredInterviewActivationVerification: + raise TypeError("authority must return StructuredInterviewActivationVerification") + + ( + verified_tenant_record_id, + verified_interview_plan_reference, + verified_plan_digest, + verified_approving_actor_reference, + verified_authority_evidence_reference, + verified_authority_evidence_digest, + verification_approved_at, + ) = verification + verified_approved_at = _snapshot_utc_datetime(verification_approved_at, "approved_at") + + _validate_operational_uuid(verified_tenant_record_id, "tenant_record_id") + _validate_reference( + verified_interview_plan_reference, + "interview_plan", + "interview_plan_reference", + ) + _validate_digest(verified_plan_digest, "plan_digest") + _validate_reference( + verified_approving_actor_reference, + "actor", + "approving_actor_reference", + ) + _validate_reference( + verified_authority_evidence_reference, + "activation_verification", + "authority_evidence_reference", + ) + _validate_digest(verified_authority_evidence_digest, "authority_evidence_digest") + + expected_scope = ( + plan_tenant_record_id, + interview_plan_reference, + plan_digest, + approving_actor_reference, + approved_at_snapshot, + ) + verified_scope = ( + verified_tenant_record_id, + verified_interview_plan_reference, + verified_plan_digest, + verified_approving_actor_reference, + verified_approved_at, + ) + if verified_scope != expected_scope: + raise ValueError( + "activation authority returned evidence for a different plan or actor or approval time" + ) + + receipt = StructuredInterviewActivationReceipt( + tenant_record_id=plan_tenant_record_id, + interview_plan_reference=interview_plan_reference, + plan_digest=plan_digest, + approving_actor_reference=approving_actor_reference, + authority_evidence_reference=verified_authority_evidence_reference, + authority_evidence_digest=verified_authority_evidence_digest, + approved_at=approved_at_snapshot, + ) + register_receipt_seal(receipt, receipt._canonical_json_unchecked()) + return receipt + + return StructuredInterviewActivationReceipt, activate_structured_interview_plan + + +StructuredInterviewActivationReceipt, activate_structured_interview_plan = _build_activation_surface() +del _build_activation_surface diff --git a/packages/interview-plan/src/orgmetra_interview_plan/plan.py b/packages/interview-plan/src/orgmetra_interview_plan/plan.py new file mode 100644 index 000000000..aed823a70 --- /dev/null +++ b/packages/interview-plan/src/orgmetra_interview_plan/plan.py @@ -0,0 +1,336 @@ +"""Governed, candidate-neutral structured-interview plan evidence. + +The plan binds an interview to job-analysis evidence, predetermined competencies, +question/rating artifacts, and an accountable interviewer panel. It contains no +candidate PII or candidate response/score and remains pending explicit human approval. +""" +from __future__ import annotations + +from contextvars import ContextVar +from dataclasses import dataclass +from datetime import 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 + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_PURPOSE_CODE = "structured_interview_plan" +_REVIEW_STATE = "requires_human_approval" +_ALLOWED_REASON_CODES = frozenset({"approved_requisition_interview"}) +_NEXT_ACTION = ( + "Within tenant_record_id, re-resolve every plan reference and verify the " + "requisition-to-Job-to-job-analysis binding; verify question-set, " + "question-to-competency mapping, and rating-anchor provenance; re-resolve every " + "panel_actor_reference, prove the resolved panel actor identities are distinct, " + "and verify panel eligibility and training before an accountable human activates " + "this structured interview plan." +) +_MAX_EVIDENCE_VERSION = 2_147_483_647 +_PROCESS_PLAN_SEAL_KEY = secrets.token_bytes(32) +_PLAN_SEALS: dict[int, str] = {} +_CONSTRUCTING_PLAN_IDENTITIES: WeakValueDictionary[int, object] = WeakValueDictionary() +_ISSUED_PLAN_IDENTITIES: WeakValueDictionary[int, object] = WeakValueDictionary() +_ACTIVE_PLAN_CONSTRUCTOR = ContextVar("_ACTIVE_PLAN_CONSTRUCTOR", default=None) +_PLAN_SEALS_LOCK = RLock() + + +def _discard_plan_seal(plan_id: int) -> None: + """Discard process-local issuance evidence after the plan is collected.""" + with _PLAN_SEALS_LOCK: + _PLAN_SEALS.pop(plan_id, None) + + +def _register_plan_seal(plan: object, seal: str) -> None: + """Bind one live plan identity once to evidence outside plan-writable slots.""" + plan_id = id(plan) + with _PLAN_SEALS_LOCK: + if plan_id in _PLAN_SEALS: + raise ValueError("structured interview plan issuance evidence already exists") + _PLAN_SEALS[plan_id] = seal + finalize(plan, _discard_plan_seal, plan_id) + + +def _authoritative_plan_seal(plan: object) -> str | None: + """Return process-local issuance evidence without trusting plan-owned state.""" + with _PLAN_SEALS_LOCK: + return _PLAN_SEALS.get(id(plan)) + + +def _seal_plan(payload_json: str) -> str: + """Bind one process-local plan issuance to exact canonical payload bytes.""" + return hmac.new( + _PROCESS_PLAN_SEAL_KEY, + payload_json.encode("utf-8"), + "sha256", + ).hexdigest() + + +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_code(value: str, field_name: str) -> None: + """Require exact bounded descriptive lower snake_case governance text.""" + if type(value) is not str or len(value) > 64 or not _CODE_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be bounded two-or-more-word lower snake_case") + + +def _validate_reference(value: str, prefix: str, field_name: str) -> None: + """Require the expected namespace plus a canonical non-sentinel UUIDv4 suffix.""" + namespace = f"{prefix}:" + if type(value) is not str or len(value) > 160 or not value.startswith(namespace): + raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + suffix = value[len(namespace) :] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be an opaque {prefix}: reference") from exc + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + + +def _validate_digest(value: str, field_name: str) -> None: + """Require exact built-in string lowercase SHA-256 hexadecimal evidence.""" + if type(value) is not str or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _snapshot_utc_datetime(value: datetime, field_name: str) -> datetime: + """Detach one caller-owned aware datetime into a representable built-in UTC instant.""" + if type(value) is not datetime or value.tzinfo is None: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + try: + offset = value.utcoffset() + except Exception as exc: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + if type(offset) is not timedelta: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") + local_naive = value.replace(tzinfo=None) + try: + normalized = local_naive - offset + except OverflowError as exc: + raise ValueError(f"{field_name} must be an exact timezone-aware datetime") from exc + return normalized.replace(tzinfo=timezone.utc) + + +def _canonical_timestamp(value: datetime, field_name: str = "generated_at") -> str: + """Render an aware instant as UTC RFC 3339 text after fail-closed detachment.""" + return _snapshot_utc_datetime(value, field_name).isoformat().replace("+00:00", "Z") + + +class _StructuredInterviewPlanMeta(type): + """Gate plan provenance on one allocator ticket per normal class construction.""" + + def __call__(cls, *args: object, **kwargs: object) -> object: + """Arm one allocator ticket before Python enters this class's constructor.""" + token = _ACTIVE_PLAN_CONSTRUCTOR.set(cls) + try: + return super().__call__(*args, **kwargs) + finally: + _ACTIVE_PLAN_CONSTRUCTOR.reset(token) + + +@dataclass(frozen=True, slots=True, repr=False, weakref_slot=True) +class StructuredInterviewPlan(metaclass=_StructuredInterviewPlanMeta): + """Immutable candidate-neutral interview-plan evidence awaiting human approval.""" + + tenant_record_id: str + interview_plan_reference: str + requisition_reference: str + job_profile_reference: str + job_analysis_reference: str + job_analysis_digest: str + question_set_reference: str + question_set_digest: str + question_competency_map_reference: str + question_competency_map_digest: str + rating_anchor_reference: str + rating_anchor_digest: str + competency_references: tuple[str, ...] + panel_actor_references: tuple[str, ...] + question_count: int + purpose_code: str + reason_code: str + generated_at: datetime + evidence_version: int = 1 + human_confirmation_required: bool = True + review_state: str = _REVIEW_STATE + next_action: str = _NEXT_ACTION + + def __new__(cls, *_args: object, **_kwargs: object) -> StructuredInterviewPlan: + """Consume constructor eligibility before caller-controlled validation can run.""" + instance = object.__new__(cls) + if _ACTIVE_PLAN_CONSTRUCTOR.get() is cls: + _ACTIVE_PLAN_CONSTRUCTOR.set(None) + with _PLAN_SEALS_LOCK: + _CONSTRUCTING_PLAN_IDENTITIES[id(instance)] = instance + return instance + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed contract.""" + with _PLAN_SEALS_LOCK: + if _ISSUED_PLAN_IDENTITIES.get(id(self)) is self: + raise ValueError("structured interview plan issuance evidence already exists") + if _CONSTRUCTING_PLAN_IDENTITIES.get(id(self)) is not self: + raise ValueError("structured interview plan constructor provenance is unavailable") + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference(self.interview_plan_reference, "interview_plan", "interview_plan_reference") + _validate_reference(self.requisition_reference, "requisition", "requisition_reference") + _validate_reference(self.job_profile_reference, "job_profile", "job_profile_reference") + _validate_reference(self.job_analysis_reference, "job_analysis", "job_analysis_reference") + _validate_digest(self.job_analysis_digest, "job_analysis_digest") + _validate_reference(self.question_set_reference, "question_set", "question_set_reference") + _validate_digest(self.question_set_digest, "question_set_digest") + _validate_reference( + self.question_competency_map_reference, + "question_competency_map", + "question_competency_map_reference", + ) + _validate_digest(self.question_competency_map_digest, "question_competency_map_digest") + _validate_reference(self.rating_anchor_reference, "rating_anchor", "rating_anchor_reference") + _validate_digest(self.rating_anchor_digest, "rating_anchor_digest") + if type(self.competency_references) is not tuple or not 1 <= len(self.competency_references) <= 12: + raise ValueError("competency_references must be a tuple containing 1 through 12 competencies") + for reference in self.competency_references: + _validate_reference(reference, "competency", "competency_references") + if tuple(sorted(set(self.competency_references))) != self.competency_references: + raise ValueError("competency_references must be sorted and unique") + if type(self.panel_actor_references) is not tuple or not 2 <= len(self.panel_actor_references) <= 8: + raise ValueError("panel_actor_references must be a tuple containing 2 through 8 actors") + for reference in self.panel_actor_references: + _validate_reference(reference, "actor", "panel_actor_references") + if tuple(sorted(set(self.panel_actor_references))) != self.panel_actor_references: + raise ValueError("panel_actor_references must be sorted and unique") + if type(self.question_count) is not int or not 1 <= self.question_count <= 20: + raise ValueError("question_count must be an integer from 1 through 20") + if self.question_count < len(self.competency_references): + raise ValueError("question_count must be at least the number of governed competencies") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain structured_interview_plan") + _validate_code(self.reason_code, "reason_code") + if self.reason_code not in _ALLOWED_REASON_CODES: + raise ValueError("reason_code must use a reviewed non-sensitive interview-plan reason") + generated_at_snapshot = _snapshot_utc_datetime(self.generated_at, "generated_at") + object.__setattr__(self, "generated_at", generated_at_snapshot) + if type(self.evidence_version) is not int or not 1 <= self.evidence_version <= _MAX_EVIDENCE_VERSION: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory for interview-plan approval") + if type(self.review_state) is not str or self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_approval") + if type(self.next_action) is not str or self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed interview-plan instruction") + _register_plan_seal(self, _seal_plan(self._canonical_json_unchecked())) + with _PLAN_SEALS_LOCK: + _ISSUED_PLAN_IDENTITIES[id(self)] = self + _CONSTRUCTING_PLAN_IDENTITIES.pop(id(self), None) + + def __repr__(self) -> str: + """Return a fully redacted representation safe for routine logs and assertions.""" + return "StructuredInterviewPlan()" + + def _canonical_json_unchecked(self) -> str: + """Render canonical plan bytes without process-local issuance state.""" + payload = { + "competency_references": list(self.competency_references), + "evidence_version": self.evidence_version, + "generated_at": _canonical_timestamp(self.generated_at), + "human_confirmation_required": self.human_confirmation_required, + "interview_plan_reference": self.interview_plan_reference, + "job_analysis_digest": self.job_analysis_digest, + "job_analysis_reference": self.job_analysis_reference, + "job_profile_reference": self.job_profile_reference, + "next_action": self.next_action, + "panel_actor_references": list(self.panel_actor_references), + "purpose_code": self.purpose_code, + "question_competency_map_digest": self.question_competency_map_digest, + "question_competency_map_reference": self.question_competency_map_reference, + "question_count": self.question_count, + "question_set_digest": self.question_set_digest, + "question_set_reference": self.question_set_reference, + "rating_anchor_digest": self.rating_anchor_digest, + "rating_anchor_reference": self.rating_anchor_reference, + "reason_code": self.reason_code, + "requisition_reference": self.requisition_reference, + "review_state": self.review_state, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def canonical_json(self) -> str: + """Return creation-bound canonical JSON for immutable audit correlation.""" + with _PLAN_SEALS_LOCK: + if _ISSUED_PLAN_IDENTITIES.get(id(self)) is not self: + raise ValueError("structured interview plan issuance evidence is unavailable") + canonical = self._canonical_json_unchecked() + authoritative_seal = _authoritative_plan_seal(self) + if type(authoritative_seal) is not str: + raise ValueError("structured interview plan issuance evidence is unavailable") + if not hmac.compare_digest(_seal_plan(canonical), authoritative_seal): + raise ValueError("structured interview plan changed after plan issuance") + return canonical + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact creation-bound canonical UTF-8 plan.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_structured_interview_plan( + *, + tenant_record_id: str, + interview_plan_reference: str, + requisition_reference: str, + job_profile_reference: str, + job_analysis_reference: str, + job_analysis_digest: str, + question_set_reference: str, + question_set_digest: str, + question_competency_map_reference: str, + question_competency_map_digest: str, + rating_anchor_reference: str, + rating_anchor_digest: str, + competency_references: tuple[str, ...], + panel_actor_references: tuple[str, ...], + question_count: int, + purpose_code: str, + reason_code: str, + generated_at: datetime, + evidence_version: int = 1, +) -> StructuredInterviewPlan: + """Build a governed structured-interview plan that remains pending human approval.""" + return StructuredInterviewPlan( + tenant_record_id=tenant_record_id, + interview_plan_reference=interview_plan_reference, + requisition_reference=requisition_reference, + job_profile_reference=job_profile_reference, + job_analysis_reference=job_analysis_reference, + job_analysis_digest=job_analysis_digest, + question_set_reference=question_set_reference, + question_set_digest=question_set_digest, + question_competency_map_reference=question_competency_map_reference, + question_competency_map_digest=question_competency_map_digest, + rating_anchor_reference=rating_anchor_reference, + rating_anchor_digest=rating_anchor_digest, + competency_references=competency_references, + panel_actor_references=panel_actor_references, + question_count=question_count, + purpose_code=purpose_code, + reason_code=reason_code, + generated_at=generated_at, + evidence_version=evidence_version, + ) diff --git a/packages/interview-plan/tests/test_activation.py b/packages/interview-plan/tests/test_activation.py new file mode 100644 index 000000000..8eb784d06 --- /dev/null +++ b/packages/interview-plan/tests/test_activation.py @@ -0,0 +1,360 @@ +"""Regression tests for executable, fail-closed structured-interview activation.""" + +from dataclasses import replace +from datetime import datetime, timezone +import json + +import pytest + +from orgmetra_interview_plan import ( + StructuredInterviewActivationVerification, + activate_structured_interview_plan, + build_structured_interview_plan, +) + +TENANT = "10000000-0000-7000-8000-000000000001" +INTERVIEW_PLAN = "interview_plan:11111111-1111-4111-8111-111111111111" +REQUISITION = "requisition:22222222-2222-4222-8222-222222222222" +JOB_PROFILE = "job_profile:33333333-3333-4333-8333-333333333333" +JOB_ANALYSIS = "job_analysis:44444444-4444-4444-8444-444444444444" +QUESTION_SET = "question_set:55555555-5555-4555-8555-555555555555" +QUESTION_MAP = "question_competency_map:66666666-6666-4666-8666-666666666666" +RATING_ANCHOR = "rating_anchor:77777777-7777-4777-8777-777777777777" +COMPETENCY_A = "competency:88888888-8888-4888-8888-888888888888" +COMPETENCY_B = "competency:99999999-9999-4999-8999-999999999999" +PANEL_A = "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" +PANEL_B = "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc" +APPROVER = "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd" +AUTHORITY_EVIDENCE = "activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +DIGEST_E = "e" * 64 +APPROVED_AT = datetime(2026, 8, 21, 5, 0, 0, 123456, tzinfo=timezone.utc) + + +def plan(): + """Return a valid immutable plan for activation-boundary tests.""" + return build_structured_interview_plan( + tenant_record_id=TENANT, + interview_plan_reference=INTERVIEW_PLAN, + requisition_reference=REQUISITION, + job_profile_reference=JOB_PROFILE, + job_analysis_reference=JOB_ANALYSIS, + job_analysis_digest=DIGEST_A, + question_set_reference=QUESTION_SET, + question_set_digest=DIGEST_B, + question_competency_map_reference=QUESTION_MAP, + question_competency_map_digest=DIGEST_D, + rating_anchor_reference=RATING_ANCHOR, + rating_anchor_digest=DIGEST_C, + competency_references=(COMPETENCY_A, COMPETENCY_B), + panel_actor_references=(PANEL_A, PANEL_B), + question_count=4, + purpose_code="structured_interview_plan", + reason_code="approved_requisition_interview", + generated_at=datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + ) + + +def verification_for(candidate_plan, **changes): + """Return matching authoritative host evidence, optionally mutated for failure tests.""" + values = dict( + tenant_record_id=candidate_plan.tenant_record_id, + interview_plan_reference=candidate_plan.interview_plan_reference, + plan_digest=candidate_plan.sha256_digest(), + approving_actor_reference=APPROVER, + authority_evidence_reference=AUTHORITY_EVIDENCE, + authority_evidence_digest=DIGEST_E, + approved_at=APPROVED_AT, + ) + values.update(changes) + return StructuredInterviewActivationVerification(**values) + + +class AllowingAuthority: + """Host fixture that returns evidence only after its authoritative checks succeed.""" + + def __init__(self, verification): + """Store the verification fixture and initialize the call audit list.""" + self.verification = verification + self.calls = [] + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Record detached plan/actor scope and return authoritative evidence.""" + assert approved_at == APPROVED_AT + self.calls.append((plan_canonical_json, plan_digest, approving_actor_reference)) + return self.verification + + +class RejectingAuthority: + """Host fixture representing a failed tenant/job/provenance/panel verification.""" + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Fail closed instead of producing activation evidence.""" + raise PermissionError("authoritative activation checks failed") + + +class SwitchingVerification(StructuredInterviewActivationVerification): + """Hostile result that can change valid authority evidence between attribute reads.""" + + def __getattribute__(self, name): + """Swap one format-valid authority reference after its first validation read.""" + if name == "authority_evidence_reference": + reads = object.__getattribute__(self, "_authority_reference_reads") + object.__setattr__(self, "_authority_reference_reads", reads + 1) + if reads: + return "activation_verification:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + return super().__getattribute__(name) + + +class ForgedScopeText(str): + """String subclass that makes foreign scope evidence compare equal to expected scope.""" + + def __eq__(self, other): + """Pretend to equal any string so tuple scope comparison can be forged.""" + return isinstance(other, str) + + def __ne__(self, other): + """Pretend not to differ from any string so fail-closed comparison is bypassed.""" + return not isinstance(other, str) + + __hash__ = str.__hash__ + + +def test_activation_executes_authority_and_returns_immutable_human_receipt(): + """Bind human confirmation to detached plan bytes and authoritative verification evidence.""" + candidate_plan = plan() + authority = AllowingAuthority(verification_for(candidate_plan)) + + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + assert len(authority.calls) == 1 + plan_canonical_json, plan_digest, actor_reference = authority.calls[0] + assert json.loads(plan_canonical_json)["interview_plan_reference"] == INTERVIEW_PLAN + assert plan_digest == candidate_plan.sha256_digest() + assert actor_reference == APPROVER + payload = json.loads(receipt.canonical_json()) + assert payload["tenant_record_id"] == TENANT + assert payload["interview_plan_reference"] == INTERVIEW_PLAN + assert payload["plan_digest"] == candidate_plan.sha256_digest() + assert payload["approving_actor_reference"] == APPROVER + assert payload["authority_evidence_reference"] == AUTHORITY_EVIDENCE + assert payload["authority_evidence_digest"] == DIGEST_E + assert payload["purpose_code"] == "structured_interview_activation" + assert payload["reason_code"] == "human_approved_plan_activation" + assert payload["evidence_version"] == 1 + assert payload["human_confirmation"] is True + assert payload["activation_state"] == "approved_for_use" + assert payload["approved_at"] == "2026-08-21T05:00:00.123456Z" + assert receipt.sha256_digest() + assert repr(receipt) == "StructuredInterviewActivationReceipt()" + + +def test_activation_rejects_approval_before_plan_generation(): + """Reject audit evidence claiming a plan was approved before that plan existed.""" + candidate_plan = plan() + authority = AllowingAuthority(verification_for(candidate_plan)) + + with pytest.raises(ValueError, match="approved_at must not precede plan generated_at"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=datetime(2026, 8, 21, 4, 29, 59, tzinfo=timezone.utc), + ) + + assert authority.calls == [] + + +def test_authority_rejection_blocks_activation(): + """Propagate authoritative rejection so no activation receipt can be manufactured.""" + with pytest.raises(PermissionError, match="authoritative activation checks failed"): + activate_structured_interview_plan( + plan=plan(), + authority=RejectingAuthority(), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +def test_activation_rejects_non_verification_result(): + """Reject adapters that do not return the published verification contract.""" + + class WrongAuthority: + """Fixture that violates the published authority return type.""" + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Return a non-contract object to prove type fail-closure.""" + return object() + + with pytest.raises(TypeError, match="StructuredInterviewActivationVerification"): + activate_structured_interview_plan( + plan=plan(), + authority=WrongAuthority(), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +def test_activation_rejects_verification_subclass_before_evidence_reads_can_diverge(): + """Reject subclassed host evidence that can change canonical audit fields between reads.""" + candidate_plan = plan() + base = verification_for(candidate_plan) + verification = SwitchingVerification( + tenant_record_id=base.tenant_record_id, + interview_plan_reference=base.interview_plan_reference, + plan_digest=base.plan_digest, + approving_actor_reference=base.approving_actor_reference, + authority_evidence_reference=base.authority_evidence_reference, + authority_evidence_digest=base.authority_evidence_digest, + approved_at=base.approved_at, + ) + object.__setattr__(verification, "_authority_reference_reads", 0) + + with pytest.raises(TypeError, match="StructuredInterviewActivationVerification"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +@pytest.mark.parametrize( + ("field", "forged_value"), + [ + ("tenant_record_id", ForgedScopeText("20000000-0000-7000-8000-000000000001")), + ( + "interview_plan_reference", + ForgedScopeText("interview_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + ), + ("plan_digest", ForgedScopeText("f" * 64)), + ("approving_actor_reference", ForgedScopeText(PANEL_A)), + ], +) +def test_activation_rejects_forged_scope_string_subclasses(field, forged_value): + """Validate exact verification scope types before equality can approve foreign evidence.""" + candidate_plan = plan() + authority = AllowingAuthority(verification_for(candidate_plan, **{field: forged_value})) + + with pytest.raises(ValueError, match=field): + activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +@pytest.mark.parametrize( + ("changes", "match"), + [ + ({"tenant_record_id": "20000000-0000-7000-8000-000000000001"}, "different plan or actor"), + ({"interview_plan_reference": "interview_plan:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"}, "different plan or actor"), + ({"plan_digest": "f" * 64}, "different plan or actor"), + ({"approving_actor_reference": PANEL_A}, "different plan or actor"), + ], +) +def test_activation_rejects_authority_evidence_for_other_scope(changes, match): + """Reject otherwise well-shaped verification evidence bound to a different scope.""" + candidate_plan = plan() + authority = AllowingAuthority(verification_for(candidate_plan, **changes)) + with pytest.raises(ValueError, match=match): + activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +@pytest.mark.parametrize( + ("changes", "match"), + [ + ({"authority_evidence_reference": "activation_verification:human-readable"}, "authority_evidence_reference"), + ({"authority_evidence_digest": "A" * 64}, "authority_evidence_digest"), + ], +) +def test_activation_rejects_untrusted_authority_evidence_shape(changes, match): + """Require opaque verification identity and deterministic evidence digest.""" + candidate_plan = plan() + authority = AllowingAuthority(verification_for(candidate_plan, **changes)) + with pytest.raises(ValueError, match=match): + activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +@pytest.mark.parametrize( + ("field", "bad", "match"), + [ + ("purpose_code", "other_purpose", "purpose_code"), + ("reason_code", "other_reason", "reason_code"), + ("evidence_version", True, "evidence_version"), + ("evidence_version", 0, "evidence_version"), + ("human_confirmation", False, "human confirmation"), + ("activation_state", "pending", "activation_state"), + ], +) +def test_direct_receipt_construction_fails_closed(field, bad, match): + """Preserve fixed human-authority semantics under direct dataclass construction/replacement.""" + candidate_plan = plan() + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification_for(candidate_plan)), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + with pytest.raises(ValueError, match=match): + replace(receipt, **{field: bad}) + + +def test_authority_verification_repr_redacts_correlation_evidence(): + """Keep tenant, plan, actor, evidence, and reviewed time out of routine logs.""" + candidate_plan = plan() + verification = verification_for(candidate_plan) + + text = repr(verification) + + assert text == "StructuredInterviewActivationVerification()" + for sensitive_value in ( + TENANT, + INTERVIEW_PLAN, + candidate_plan.sha256_digest(), + APPROVER, + AUTHORITY_EVIDENCE, + DIGEST_E, + APPROVED_AT.isoformat(), + ): + assert sensitive_value not in text diff --git a/packages/interview-plan/tests/test_activation_approval_time.py b/packages/interview-plan/tests/test_activation_approval_time.py new file mode 100644 index 000000000..ca6089b79 --- /dev/null +++ b/packages/interview-plan/tests/test_activation_approval_time.py @@ -0,0 +1,96 @@ +"""Regression for authoritative structured-interview approval-time binding.""" + +from datetime import datetime, timezone +import json + +from orgmetra_interview_plan import ( + StructuredInterviewActivationVerification, + activate_structured_interview_plan, + build_structured_interview_plan, +) + +TENANT = "10000000-0000-7000-8000-000000000001" +INTERVIEW_PLAN = "interview_plan:11111111-1111-4111-8111-111111111111" +APPROVER = "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd" +AUTHORITY_EVIDENCE = "activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" +APPROVED_AT = datetime(2026, 8, 21, 5, 0, 0, 123456, tzinfo=timezone.utc) + + +def _plan(): + """Return one valid immutable interview plan for the approval-time boundary.""" + return build_structured_interview_plan( + tenant_record_id=TENANT, + interview_plan_reference=INTERVIEW_PLAN, + requisition_reference="requisition:22222222-2222-4222-8222-222222222222", + job_profile_reference="job_profile:33333333-3333-4333-8333-333333333333", + job_analysis_reference="job_analysis:44444444-4444-4444-8444-444444444444", + job_analysis_digest="a" * 64, + question_set_reference="question_set:55555555-5555-4555-8555-555555555555", + question_set_digest="b" * 64, + question_competency_map_reference=( + "question_competency_map:66666666-6666-4666-8666-666666666666" + ), + question_competency_map_digest="c" * 64, + rating_anchor_reference="rating_anchor:77777777-7777-4777-8777-777777777777", + rating_anchor_digest="d" * 64, + competency_references=("competency:88888888-8888-4888-8888-888888888888",), + panel_actor_references=( + "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ), + question_count=2, + purpose_code="structured_interview_plan", + reason_code="approved_requisition_interview", + generated_at=datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + ) + + +class TimestampRecordingAuthority: + """Require the candidate approval instant to cross the authoritative boundary.""" + + def __init__(self, candidate_plan) -> None: + """Keep the expected plan only for test-side correlation and initialize calls.""" + self.candidate_plan = candidate_plan + self.calls = [] + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Record and attest detached plan evidence plus the exact approval instant.""" + payload = json.loads(plan_canonical_json) + self.calls.append((plan_canonical_json, plan_digest, approving_actor_reference, approved_at)) + return StructuredInterviewActivationVerification( + tenant_record_id=payload["tenant_record_id"], + interview_plan_reference=payload["interview_plan_reference"], + plan_digest=plan_digest, + approving_actor_reference=approving_actor_reference, + authority_evidence_reference=AUTHORITY_EVIDENCE, + authority_evidence_digest="e" * 64, + approved_at=approved_at, + ) + + +def test_activation_sends_approval_time_through_authoritative_verification(): + """Do not mint approved evidence from a timestamp the authority never reviewed.""" + candidate_plan = _plan() + authority = TimestampRecordingAuthority(candidate_plan) + + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + assert len(authority.calls) == 1 + plan_canonical_json, plan_digest, actor_reference, approved_at = authority.calls[0] + assert json.loads(plan_canonical_json)["interview_plan_reference"] == INTERVIEW_PLAN + assert plan_digest == candidate_plan.sha256_digest() + assert actor_reference == APPROVER + assert approved_at == APPROVED_AT + assert "2026-08-21T05:00:00.123456Z" in receipt.canonical_json() diff --git a/packages/interview-plan/tests/test_activation_integrity_review.py b/packages/interview-plan/tests/test_activation_integrity_review.py new file mode 100644 index 000000000..4e5b957bb --- /dev/null +++ b/packages/interview-plan/tests/test_activation_integrity_review.py @@ -0,0 +1,251 @@ +"""Regressions for current-head structured-interview activation integrity findings.""" + +from datetime import datetime, timedelta, timezone, tzinfo +from hashlib import sha256 +import json + +import pytest + +from orgmetra_interview_plan import ( + StructuredInterviewActivationVerification, + activate_structured_interview_plan, +) +from test_activation import ( + APPROVED_AT, + APPROVER, + AUTHORITY_EVIDENCE, + DIGEST_E, + AllowingAuthority, + RejectingAuthority, + plan, + verification_for, +) + +ALTERNATE_AUTHORITY_EVIDENCE = "activation_verification:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" + + +class MutableOffsetTimezone(tzinfo): + """UTC-offset provider whose offset can change after initial validation.""" + + def __init__(self, offset_hours: int) -> None: + """Store the mutable offset used by the adversarial approval-time fixture.""" + self.offset_hours = offset_hours + + def utcoffset(self, value): + """Return the currently configured offset.""" + return timedelta(hours=self.offset_hours) + + def dst(self, value): + """Return zero daylight-saving offset for deterministic test behavior.""" + return timedelta(0) + + def tzname(self, value): + """Return a stable diagnostic name for the mutable test timezone.""" + return "MutableOffsetTimezone" + + +class UnknownOffsetTimezone(tzinfo): + """Timezone fixture that cannot establish an authoritative UTC offset.""" + + def utcoffset(self, value): + """Return no offset so activation must fail before authority work.""" + return None + + def dst(self, value): + """Return no daylight-saving value for the deliberately invalid fixture.""" + return None + + def tzname(self, value): + """Return a stable diagnostic name for the invalid timezone fixture.""" + return "UnknownOffsetTimezone" + + +class ApprovalTimeMutatingAuthority: + """Mutate caller-owned timezone state only after receiving the approval snapshot.""" + + def __init__(self, source_timezone: MutableOffsetTimezone) -> None: + """Keep the caller timezone so authority work can mutate it deterministically.""" + self.source_timezone = source_timezone + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Require immutable built-in UTC evidence, then mutate the caller timezone.""" + assert approved_at.tzinfo is timezone.utc + assert approved_at == APPROVED_AT + self.source_timezone.offset_hours = 2 + payload = json.loads(plan_canonical_json) + return StructuredInterviewActivationVerification( + tenant_record_id=payload["tenant_record_id"], + interview_plan_reference=payload["interview_plan_reference"], + plan_digest=plan_digest, + approving_actor_reference=approving_actor_reference, + authority_evidence_reference=AUTHORITY_EVIDENCE, + authority_evidence_digest=DIGEST_E, + approved_at=approved_at, + ) + + +class DetachedPlanEvidenceAuthority: + """Accept only immutable creation-bound plan evidence, never the caller's live plan object.""" + + def __init__(self, verification: StructuredInterviewActivationVerification) -> None: + """Store one matching verification result and initialize the call audit list.""" + self.verification = verification + self.calls = [] + + def verify_activation( + self, + *, + plan_canonical_json: str, + plan_digest: str, + approving_actor_reference: str, + approved_at: datetime, + ) -> StructuredInterviewActivationVerification: + """Prove authority work is bound to detached canonical bytes and their exact digest.""" + assert type(plan_canonical_json) is str + payload = json.loads(plan_canonical_json) + assert payload["question_count"] == 4 + assert plan_digest == sha256(plan_canonical_json.encode("utf-8")).hexdigest() + self.calls.append((plan_canonical_json, plan_digest, approving_actor_reference, approved_at)) + return self.verification + + +def test_existing_plan_identity_cannot_renew_issuance_seal_after_mutation(): + """Repeated initialization must not legitimize changed bytes on one issued plan identity.""" + candidate_plan = plan() + object.__setattr__(candidate_plan, "question_count", 3) + + with pytest.raises(ValueError, match="issuance evidence already exists"): + candidate_plan.__post_init__() + with pytest.raises(ValueError, match="changed after plan issuance"): + candidate_plan.canonical_json() + with pytest.raises(ValueError, match="changed after plan issuance"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=RejectingAuthority(), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + +def test_existing_receipt_identity_cannot_renew_issuance_seal_after_mutation(): + """Receipt revalidation must not create new issuance evidence after factory issuance.""" + candidate_plan = plan() + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification_for(candidate_plan)), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + original_canonical = receipt.canonical_json() + object.__setattr__(receipt, "plan_digest", "f" * 64) + + receipt.__post_init__() + + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + receipt.canonical_json() + object.__setattr__(receipt, "plan_digest", json.loads(original_canonical)["plan_digest"]) + assert receipt.canonical_json() == original_canonical + + +def test_activation_rejects_naive_approval_time_before_authority_work(): + """A caller must supply an aware approval instant before authoritative review.""" + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + activate_structured_interview_plan( + plan=plan(), + authority=RejectingAuthority(), + approving_actor_reference=APPROVER, + approved_at=datetime(2026, 8, 21, 5, 0, 0), + ) + + +def test_activation_rejects_approval_time_with_unknown_offset(): + """An aware-looking timestamp without a concrete UTC offset is not auditable evidence.""" + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + activate_structured_interview_plan( + plan=plan(), + authority=RejectingAuthority(), + approving_actor_reference=APPROVER, + approved_at=datetime(2026, 8, 21, 5, 0, 0, tzinfo=UnknownOffsetTimezone()), + ) + + +def test_activation_freezes_mutable_timezone_before_authority_and_receipt(): + """Authority work cannot make one approved_at value represent two UTC instants.""" + mutable_timezone = MutableOffsetTimezone(1) + caller_time = datetime(2026, 8, 21, 6, 0, 0, 123456, tzinfo=mutable_timezone) + candidate_plan = plan() + + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=ApprovalTimeMutatingAuthority(mutable_timezone), + approving_actor_reference=APPROVER, + approved_at=caller_time, + ) + + payload = json.loads(receipt.canonical_json()) + assert payload["approved_at"] == "2026-08-21T05:00:00.123456Z" + assert receipt.approved_at.tzinfo is timezone.utc + assert receipt.approved_at == APPROVED_AT + + +def test_activation_authority_receives_detached_creation_bound_plan_evidence(): + """Do not expose a live plan that can be changed and restored while authority work runs.""" + candidate_plan = plan() + verification = verification_for(candidate_plan) + authority = DetachedPlanEvidenceAuthority(verification) + + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + assert len(authority.calls) == 1 + plan_canonical_json, plan_digest, actor_reference, approved_at = authority.calls[0] + assert json.loads(plan_canonical_json)["interview_plan_reference"] == candidate_plan.interview_plan_reference + assert plan_digest == candidate_plan.sha256_digest() + assert actor_reference == APPROVER + assert approved_at == APPROVED_AT + assert receipt.plan_digest == plan_digest + + +def test_verification_contract_cannot_be_rewritten_with_object_setattr(): + """Authority evidence must be runtime-immutable so field reads cannot mix revisions.""" + verification = verification_for(plan()) + + with pytest.raises((AttributeError, TypeError)): + object.__setattr__(verification, "authority_evidence_reference", ALTERNATE_AUTHORITY_EVIDENCE) + + assert verification.authority_evidence_reference == AUTHORITY_EVIDENCE + + +def test_verification_contract_explicitly_binds_reviewed_approval_time(): + """Authority verification must expose the exact approval instant it attests.""" + field_names = set(StructuredInterviewActivationVerification._fields) + + assert "approved_at" in field_names + + +def test_activation_rejects_verification_for_different_approval_time(): + """Do not accept authority evidence that attests a different approval instant.""" + candidate_plan = plan() + verification = verification_for( + candidate_plan, + approved_at=APPROVED_AT + timedelta(seconds=1), + ) + + with pytest.raises(ValueError, match="different plan or actor or approval time"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) diff --git a/packages/interview-plan/tests/test_activation_plan_mutation.py b/packages/interview-plan/tests/test_activation_plan_mutation.py new file mode 100644 index 000000000..d6845493d --- /dev/null +++ b/packages/interview-plan/tests/test_activation_plan_mutation.py @@ -0,0 +1,66 @@ +"""Regression tests for activation-time mutation of governed interview plans.""" + +import json + +from orgmetra_interview_plan import ( + StructuredInterviewActivationVerification, + activate_structured_interview_plan, +) +from test_activation import ( + APPROVED_AT, + APPROVER, + AUTHORITY_EVIDENCE, + DIGEST_E, + plan, +) + + +class RestoringPlanAliasAuthority: + """Authority fixture retaining a live plan alias while reviewing detached plan bytes.""" + + def __init__(self, live_plan) -> None: + """Keep an adversarial alias so change-and-restore behavior is deterministic.""" + self.live_plan = live_plan + self.reviewed_question_count = None + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Mutate and restore the live alias while attesting only detached canonical evidence.""" + original_question_count = self.live_plan.question_count + object.__setattr__(self.live_plan, "question_count", original_question_count - 1) + payload = json.loads(plan_canonical_json) + self.reviewed_question_count = payload["question_count"] + object.__setattr__(self.live_plan, "question_count", original_question_count) + return StructuredInterviewActivationVerification( + tenant_record_id=payload["tenant_record_id"], + interview_plan_reference=payload["interview_plan_reference"], + plan_digest=plan_digest, + approving_actor_reference=approving_actor_reference, + authority_evidence_reference=AUTHORITY_EVIDENCE, + authority_evidence_digest=DIGEST_E, + approved_at=approved_at, + ) + + +def test_activation_detaches_plan_evidence_from_authority_time_aba_mutation(): + """A live plan change-and-restore cycle cannot alter what the authority reviews or approves.""" + candidate_plan = plan() + original_digest = candidate_plan.sha256_digest() + authority = RestoringPlanAliasAuthority(candidate_plan) + + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=authority, + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + assert authority.reviewed_question_count == 4 + assert candidate_plan.sha256_digest() == original_digest + assert receipt.plan_digest == original_digest diff --git a/packages/interview-plan/tests/test_activation_plan_type.py b/packages/interview-plan/tests/test_activation_plan_type.py new file mode 100644 index 000000000..6dc952c71 --- /dev/null +++ b/packages/interview-plan/tests/test_activation_plan_type.py @@ -0,0 +1,59 @@ +"""Regression coverage for the structured-interview activation plan type boundary.""" + +from datetime import datetime, timezone + +import pytest + +from orgmetra_interview_plan import activate_structured_interview_plan + +TENANT = "10000000-0000-7000-8000-000000000001" +INTERVIEW_PLAN = "interview_plan:11111111-1111-4111-8111-111111111111" +APPROVER = "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd" +PLAN_DIGEST = "a" * 64 + + +class DuckTypedPlan: + """Mimic trusted plan fields without ever passing StructuredInterviewPlan validation.""" + + tenant_record_id = TENANT + interview_plan_reference = INTERVIEW_PLAN + generated_at = datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc) + + def sha256_digest(self) -> str: + """Return a plausible digest so the old duck-typed boundary can be exercised.""" + return PLAN_DIGEST + + +class RecordingAuthority: + """Record whether authority work incorrectly runs for an untrusted plan-shaped object.""" + + def __init__(self) -> None: + """Initialize the authority call counter.""" + self.calls = 0 + + def verify_activation( + self, + *, + plan_canonical_json, + plan_digest, + approving_actor_reference, + approved_at, + ): + """Fail loudly if a duck-typed plan reaches authoritative work.""" + self.calls += 1 + raise AssertionError("duck-typed plan reached authority") + + +def test_activation_rejects_duck_typed_plan_before_authority_work(): + """Never let an unvalidated plan-shaped object reach the authoritative adapter.""" + authority = RecordingAuthority() + + with pytest.raises(TypeError, match="plan must be a StructuredInterviewPlan"): + activate_structured_interview_plan( + plan=DuckTypedPlan(), + authority=authority, + approving_actor_reference=APPROVER, + approved_at=datetime(2026, 8, 21, 5, 0, tzinfo=timezone.utc), + ) + + assert authority.calls == 0 diff --git a/packages/interview-plan/tests/test_adr_numbering.py b/packages/interview-plan/tests/test_adr_numbering.py new file mode 100644 index 000000000..6d90df9fe --- /dev/null +++ b/packages/interview-plan/tests/test_adr_numbering.py @@ -0,0 +1,15 @@ +"""Regression coverage for repository-wide ADR numbering collisions.""" + +from pathlib import Path + + +def test_adr_numeric_prefixes_are_unique() -> None: + """Reject duplicate four-digit ADR identifiers after branch integration.""" + repository_root = Path(__file__).resolve().parents[3] + adr_paths = sorted((repository_root / "docs" / "adr").glob("[0-9][0-9][0-9][0-9]-*.md")) + adr_numbers = [path.name.split("-", 1)[0] for path in adr_paths] + duplicate_numbers = sorted( + number for number in set(adr_numbers) if adr_numbers.count(number) > 1 + ) + + assert duplicate_numbers == [], f"duplicate ADR numeric prefixes: {duplicate_numbers}" diff --git a/packages/interview-plan/tests/test_docstrings.py b/packages/interview-plan/tests/test_docstrings.py new file mode 100644 index 000000000..a4a38c6e5 --- /dev/null +++ b/packages/interview-plan/tests/test_docstrings.py @@ -0,0 +1,32 @@ +"""Executable documentation-quality contract for the structured-interview package.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +PYTHON_ROOTS = (PACKAGE_ROOT / "src", PACKAGE_ROOT / "tests") + + +def _undocumented_definitions(path: Path) -> list[str]: + """Return module/class/callable names that lack a beginner-readable docstring.""" + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + missing: list[str] = [] + if ast.get_docstring(tree) is None: + missing.append(f"{path}:") + for node in ast.walk(tree): + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + if ast.get_docstring(node) is None: + missing.append(f"{path}:{node.name}") + return missing + + +def test_owned_python_definitions_have_docstrings() -> None: + """Require readable docstrings across every owned production and regression definition.""" + missing: list[str] = [] + for root in PYTHON_ROOTS: + for path in sorted(root.rglob("*.py")): + missing.extend(_undocumented_definitions(path)) + assert not missing, "Missing docstrings:\n" + "\n".join(missing) diff --git a/packages/interview-plan/tests/test_evidence_version.py b/packages/interview-plan/tests/test_evidence_version.py new file mode 100644 index 000000000..7cc351574 --- /dev/null +++ b/packages/interview-plan/tests/test_evidence_version.py @@ -0,0 +1,49 @@ +"""Regression coverage for immutable structured-interview evidence versions.""" + +from dataclasses import replace +from datetime import datetime, timezone +import json + +import pytest + +from orgmetra_interview_plan import build_structured_interview_plan + + +def _plan_kwargs() -> dict[str, object]: + """Return one valid structured-interview plan input mapping.""" + return { + "tenant_record_id": "12345678-1234-4234-8234-123456789abc", + "interview_plan_reference": "interview_plan:11111111-1111-4111-8111-111111111111", + "requisition_reference": "requisition:22222222-2222-4222-8222-222222222222", + "job_profile_reference": "job_profile:33333333-3333-4333-8333-333333333333", + "job_analysis_reference": "job_analysis:44444444-4444-4444-8444-444444444444", + "job_analysis_digest": "a" * 64, + "question_set_reference": "question_set:55555555-5555-4555-8555-555555555555", + "question_set_digest": "b" * 64, + "question_competency_map_reference": "question_competency_map:66666666-6666-4666-8666-666666666666", + "question_competency_map_digest": "c" * 64, + "rating_anchor_reference": "rating_anchor:77777777-7777-4777-8777-777777777777", + "rating_anchor_digest": "d" * 64, + "competency_references": ("competency:88888888-8888-4888-8888-888888888888",), + "panel_actor_references": ( + "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ), + "question_count": 2, + "purpose_code": "structured_interview_plan", + "reason_code": "approved_requisition_interview", + "generated_at": datetime(2026, 8, 19, 12, 0, tzinfo=timezone.utc), + } + + +def test_evidence_version_is_canonical_bounded_and_revalidated() -> None: + """Bind evidence version identity and reject replacement-path drift.""" + plan = build_structured_interview_plan(**_plan_kwargs(), evidence_version=1) + assert json.loads(plan.canonical_json())["evidence_version"] == 1 + + revised = replace(plan, evidence_version=2) + assert revised.sha256_digest() != plan.sha256_digest() + + for invalid in (True, 0, -1, "1", 2_147_483_648): + with pytest.raises(ValueError, match="evidence_version"): + replace(plan, evidence_version=invalid) diff --git a/packages/interview-plan/tests/test_plan.py b/packages/interview-plan/tests/test_plan.py new file mode 100644 index 000000000..c12b9d67d --- /dev/null +++ b/packages/interview-plan/tests/test_plan.py @@ -0,0 +1,271 @@ +"""Regression tests for governed, candidate-neutral structured-interview plans.""" + +from dataclasses import replace +from datetime import datetime, timedelta, timezone, tzinfo +from hashlib import sha256 +import json +import pytest + +from orgmetra_interview_plan import StructuredInterviewPlan, build_structured_interview_plan + +TENANT = "12345678-1234-4234-8234-123456789abc" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +INTERVIEW_PLAN = "interview_plan:11111111-1111-4111-8111-111111111111" +REQUISITION = "requisition:22222222-2222-4222-8222-222222222222" +JOB_PROFILE = "job_profile:33333333-3333-4333-8333-333333333333" +JOB_ANALYSIS = "job_analysis:44444444-4444-4444-8444-444444444444" +QUESTION_SET = "question_set:55555555-5555-4555-8555-555555555555" +QUESTION_MAP = "question_competency_map:66666666-6666-4666-8666-666666666666" +RATING_ANCHOR = "rating_anchor:77777777-7777-4777-8777-777777777777" +COMPETENCY_A = "competency:88888888-8888-4888-8888-888888888888" +COMPETENCY_B = "competency:99999999-9999-4999-8999-999999999999" +COMPETENCY_C = "competency:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" +PANEL_A = "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" +PANEL_B = "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc" + + +def values(): + """Return one valid plan input mapping for focused mutation-based regressions.""" + return dict( + tenant_record_id=TENANT, + interview_plan_reference=INTERVIEW_PLAN, + requisition_reference=REQUISITION, + job_profile_reference=JOB_PROFILE, + job_analysis_reference=JOB_ANALYSIS, + job_analysis_digest=DIGEST_A, + question_set_reference=QUESTION_SET, + question_set_digest=DIGEST_B, + question_competency_map_reference=QUESTION_MAP, + question_competency_map_digest=DIGEST_D, + rating_anchor_reference=RATING_ANCHOR, + rating_anchor_digest=DIGEST_C, + competency_references=(COMPETENCY_A, COMPETENCY_B), + panel_actor_references=(PANEL_A, PANEL_B), + question_count=4, + purpose_code="structured_interview_plan", + reason_code="approved_requisition_interview", + generated_at=datetime(2026, 8, 18, 12, 34, 56, 123456, tzinfo=timezone.utc), + ) + + +def test_builds_candidate_neutral_deterministic_plan(): + """Build deterministic evidence without candidate values or autonomous authority.""" + plan = build_structured_interview_plan(**values()) + payload = json.loads(plan.canonical_json()) + assert payload["review_state"] == "requires_human_approval" + assert payload["human_confirmation_required"] is True + assert payload["generated_at"].endswith(".123456Z") + assert payload["question_competency_map_reference"] == QUESTION_MAP + assert "candidate" not in plan.canonical_json() + assert plan.sha256_digest() == sha256(plan.canonical_json().encode("utf-8")).hexdigest() + assert plan == StructuredInterviewPlan(**values()) + + +@pytest.mark.parametrize("field,bad", [ + ("tenant_record_id", "not-a-uuid"), + ("tenant_record_id", "00000000-0000-0000-0000-000000000000"), + ("tenant_record_id", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), + ("interview_plan_reference", "wrong:si-1"), + ("requisition_reference", "wrong:req-1"), + ("job_profile_reference", "wrong:job-1"), + ("job_analysis_reference", "wrong:analysis-1"), + ("question_set_reference", "wrong:q-1"), + ("question_competency_map_reference", "wrong:map-1"), + ("rating_anchor_reference", "wrong:a-1"), + ("interview_plan_reference", "interview_plan:00000000-0000-0000-0000-000000000000"), + ("job_profile_reference", "job_profile:FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), + ("rating_anchor_reference", 7), + ("job_analysis_digest", "A" * 64), + ("question_set_digest", "b" * 63), + ("question_competency_map_digest", "D" * 64), + ("rating_anchor_digest", 7), + ("purpose_code", "wrong_purpose"), + ("purpose_code", "bad"), + ("reason_code", "Bad Reason"), + ("reason_code", "a_" + "b" * 64), + ("generated_at", datetime(2026, 8, 18, 1, 2, 3)), + ("human_confirmation_required", False), + ("human_confirmation_required", 1), + ("review_state", "approved"), + ("next_action", "Skip human review"), +]) +def test_rejects_invalid_scalar_contract(field, bad): + """Reject malformed scalar identity, digest, governance, time, and state inputs.""" + data = values() + data[field] = bad + with pytest.raises((ValueError, TypeError)): + StructuredInterviewPlan(**data) + + +@pytest.mark.parametrize("refs", [(), tuple(f"competency:c{i}" for i in range(13)), [COMPETENCY_A]]) +def test_rejects_bad_competency_collection_shape(refs): + """Require competencies to use the governed bounded tuple collection shape.""" + data = values() + data["competency_references"] = refs + with pytest.raises(ValueError, match="competency_references"): + StructuredInterviewPlan(**data) + + +@pytest.mark.parametrize("refs", [ + (COMPETENCY_B, COMPETENCY_A), + (COMPETENCY_A, COMPETENCY_A), + ("wrong:analysis",), + ("competency:Jane-Doe",), +]) +def test_rejects_noncanonical_competencies(refs): + """Reject unsorted, duplicate, wrong-namespace, or value-bearing competencies.""" + data = values() + data["competency_references"] = refs + with pytest.raises(ValueError): + StructuredInterviewPlan(**data) + + +@pytest.mark.parametrize("refs", [ + (PANEL_A,), + tuple(f"actor:p{i}" for i in range(9)), + [PANEL_A, PANEL_B], + (PANEL_B, PANEL_A), + (PANEL_A, PANEL_A), + ("wrong:a", PANEL_B), + (PANEL_A, "actor:seonghobae"), +]) +def test_rejects_bad_panel_contract(refs): + """Require a sorted unique bounded panel of opaque accountable actor references.""" + data = values() + data["panel_actor_references"] = refs + with pytest.raises(ValueError, match="panel_actor_references|actor"): + StructuredInterviewPlan(**data) + + +@pytest.mark.parametrize("count", [True, 0, 21, 1]) +def test_rejects_bad_question_count(count): + """Reject boolean, out-of-range, or competency-underflow question counts.""" + data = values() + data["question_count"] = count + with pytest.raises(ValueError, match="question_count"): + StructuredInterviewPlan(**data) + + +def test_question_count_error_describes_only_the_cardinality_constraint(): + """Keep the count failure message limited to cardinality rather than coverage claims.""" + data = values() + data["competency_references"] = (COMPETENCY_A, COMPETENCY_B, COMPETENCY_C) + data["question_count"] = 2 + with pytest.raises( + ValueError, + match="question_count must be at least the number of governed competencies", + ): + StructuredInterviewPlan(**data) + + +def test_accepts_question_count_equal_to_competency_count(): + """Accept the smallest count consistent with the governed competency cardinality.""" + data = values() + data["question_count"] = 2 + assert StructuredInterviewPlan(**data).question_count == 2 + + +class UnknownOffset(tzinfo): + """Timezone fixture whose UTC offset is intentionally unknowable.""" + + def utcoffset(self, dt): + """Return no UTC offset so timestamp validation must fail closed.""" + return None + + def dst(self, dt): + """Return no daylight-saving offset for this deliberately invalid fixture.""" + return None + + +def test_rejects_timezone_with_unknown_offset(): + """Reject tzinfo objects that cannot resolve an actual UTC offset.""" + data = values() + data["generated_at"] = datetime(2026, 8, 18, tzinfo=UnknownOffset()) + with pytest.raises(ValueError, match="timezone-aware"): + StructuredInterviewPlan(**data) + + +def test_canonicalizes_non_utc_offset_and_preserves_fractional_precision(): + """Normalize valid offsets to UTC without collapsing fractional-second evidence.""" + data = values() + data["generated_at"] = datetime( + 2026, 8, 18, 21, 34, 56, 123456, tzinfo=timezone(timedelta(hours=9)) + ) + payload = json.loads(StructuredInterviewPlan(**data).canonical_json()) + assert payload["generated_at"] == "2026-08-18T12:34:56.123456Z" + + +def test_direct_replace_is_revalidated(): + """Re-run all fail-closed invariants when immutable plans are copied with changes.""" + plan = StructuredInterviewPlan(**values()) + with pytest.raises(ValueError, match="question_set_digest"): + replace(plan, question_set_digest="not-a-digest") + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("interview_plan_reference", "interview_plan:Jane-Doe"), + ("requisition_reference", "requisition:customer-42"), + ("job_profile_reference", "job_profile:RN-ICU"), + ("job_analysis_reference", "job_analysis:salary-120000"), + ("question_set_reference", "question_set:executive-candidates"), + ("question_competency_map_reference", "question_competency_map:race-gender"), + ("rating_anchor_reference", "rating_anchor:top-secret"), + ], +) +def test_scalar_trust_references_reject_value_bearing_non_uuid_suffixes(field, value): + """Reject semantic or value-bearing scalar trust references before serialization.""" + data = values() + data[field] = value + with pytest.raises(ValueError): + StructuredInterviewPlan(**data) + + +def test_collection_trust_references_reject_value_bearing_non_uuid_suffixes(): + """Apply opaque-reference requirements to competency and panel collections.""" + for field, refs in ( + ("competency_references", (COMPETENCY_A, "competency:Jane-Doe")), + ("panel_actor_references", (PANEL_A, "actor:seonghobae")), + ): + data = values() + data[field] = refs + with pytest.raises(ValueError): + StructuredInterviewPlan(**data) + + +@pytest.mark.parametrize("reason", ["jane_doe", "salary_120000", "race_gender_review"]) +def test_reason_code_rejects_personal_or_value_bearing_free_form_codes(reason): + """Keep interview-plan reason metadata on a reviewed value-free vocabulary.""" + data = values() + data["reason_code"] = reason + with pytest.raises(ValueError): + StructuredInterviewPlan(**data) + + +def test_repr_redacts_interview_plan_correlations(): + """Prevent routine logging from exposing governance references or evidence digests.""" + plan = StructuredInterviewPlan(**values()) + rendered = repr(plan) + assert rendered == "StructuredInterviewPlan()" + for sensitive in ( + plan.interview_plan_reference, + plan.job_profile_reference, + plan.panel_actor_references[0], + plan.job_analysis_digest, + ): + assert sensitive not in rendered + + +def test_replace_cannot_reintroduce_value_bearing_metadata(): + """Preserve the privacy boundary under dataclass replacement.""" + plan = StructuredInterviewPlan(**values()) + for field, value in ( + ("job_profile_reference", "job_profile:RN-ICU"), + ("reason_code", "salary_120000"), + ): + with pytest.raises(ValueError): + replace(plan, **{field: value}) diff --git a/packages/interview-plan/tests/test_plan_issuance_integrity.py b/packages/interview-plan/tests/test_plan_issuance_integrity.py new file mode 100644 index 000000000..4f12a8386 --- /dev/null +++ b/packages/interview-plan/tests/test_plan_issuance_integrity.py @@ -0,0 +1,132 @@ +"""Regression tests for post-construction structured-interview plan integrity.""" + +from copy import copy +from dataclasses import fields, replace +from datetime import datetime, timedelta, tzinfo + +import pytest + +import orgmetra_interview_plan.plan as plan_module +from test_activation import plan + + +class ReentrantPlanAllocatorTimezone(tzinfo): + """Retain a plan allocated reentrantly from a caller-owned timezone callback.""" + + def __init__(self) -> None: + """Start without a retained forged plan.""" + self.forged_plan: object | None = None + + def utcoffset(self, _dt: datetime | None) -> timedelta: + """Allocate once while the legitimate constructor normalizes generated_at.""" + if self.forged_plan is None: + self.forged_plan = plan_module.StructuredInterviewPlan.__new__( + plan_module.StructuredInterviewPlan + ) + return timedelta(hours=9) + + def dst(self, _dt: datetime | None) -> timedelta: + """Use a stable zero daylight-saving offset.""" + return timedelta(0) + + def tzname(self, _dt: datetime | None) -> str: + """Return a descriptive test-only timezone name.""" + return "REENTRANT" + + +def test_plan_canonical_evidence_fails_closed_after_low_level_mutation(): + """A built plan must not export different canonical evidence after issuance.""" + candidate_plan = plan() + original_json = candidate_plan.canonical_json() + original_digest = candidate_plan.sha256_digest() + + object.__setattr__( + candidate_plan, + "question_count", + candidate_plan.question_count - 1, + ) + + with pytest.raises(ValueError, match="changed after plan issuance"): + candidate_plan.canonical_json() + with pytest.raises(ValueError, match="changed after plan issuance"): + candidate_plan.sha256_digest() + + assert original_json + assert len(original_digest) == 64 + + +def test_missing_process_local_plan_issuance_evidence_fails_closed(): + """Canonical export requires the creation-bound process-local plan seal.""" + candidate_plan = plan() + plan_module._discard_plan_seal(id(candidate_plan)) + + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + candidate_plan.canonical_json() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + candidate_plan.sha256_digest() + + +def test_copied_plan_has_no_transferable_process_local_issuance_evidence(): + """Copying fields must not manufacture a second issued plan identity.""" + copied_plan = copy(plan()) + + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + copied_plan.canonical_json() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + copied_plan.sha256_digest() + + +def test_object_new_clone_cannot_acquire_plan_issuance_evidence(): + """An object.__new__ clone must not mint fresh creation evidence.""" + issued_plan = plan() + forged_plan = object.__new__(type(issued_plan)) + for field in fields(issued_plan): + object.__setattr__(forged_plan, field.name, getattr(issued_plan, field.name)) + + with pytest.raises(ValueError, match="constructor provenance is unavailable"): + forged_plan.__post_init__() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + forged_plan.canonical_json() + + +def test_direct_class_new_clone_cannot_acquire_plan_issuance_evidence(): + """Calling the class allocator directly must not grant constructor provenance.""" + issued_plan = plan() + forged_plan = type(issued_plan).__new__(type(issued_plan)) + for field in fields(issued_plan): + object.__setattr__(forged_plan, field.name, getattr(issued_plan, field.name)) + + with pytest.raises(ValueError, match="constructor provenance is unavailable"): + forged_plan.__post_init__() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + forged_plan.canonical_json() + + +def test_timezone_callback_cannot_mint_plan_constructor_provenance(): + """Caller timezone code must not retain constructor privilege for another plan.""" + callback_timezone = ReentrantPlanAllocatorTimezone() + issued_plan = replace( + plan(), + generated_at=datetime(2026, 8, 30, 12, 0, tzinfo=callback_timezone), + ) + forged_plan = callback_timezone.forged_plan + assert forged_plan is not None + + for field in fields(issued_plan): + object.__setattr__(forged_plan, field.name, getattr(issued_plan, field.name)) + + with pytest.raises(ValueError, match="constructor provenance is unavailable"): + forged_plan.__post_init__() + with pytest.raises(ValueError, match="issuance evidence is unavailable"): + forged_plan.canonical_json() + + +def test_existing_plan_seal_cannot_be_replaced_by_secondary_registration(): + """A second seal registration must not overwrite an already issued plan.""" + issued_plan = plan() + original_json = issued_plan.canonical_json() + + with pytest.raises(ValueError, match="issuance evidence already exists"): + plan_module._register_plan_seal(issued_plan, "0" * 64) + + assert issued_plan.canonical_json() == original_json diff --git a/packages/interview-plan/tests/test_receipt_issuance.py b/packages/interview-plan/tests/test_receipt_issuance.py new file mode 100644 index 000000000..db58c1ffb --- /dev/null +++ b/packages/interview-plan/tests/test_receipt_issuance.py @@ -0,0 +1,106 @@ +"""Regression tests for authoritative structured-interview receipt issuance.""" + +from dataclasses import replace +from datetime import datetime, timezone + +import pytest + +import orgmetra_interview_plan.activation as activation_module +from orgmetra_interview_plan import ( + StructuredInterviewActivationReceipt, + activate_structured_interview_plan, +) +from test_activation import APPROVED_AT, APPROVER, AllowingAuthority, plan, verification_for + + +def direct_receipt() -> StructuredInterviewActivationReceipt: + """Return one syntactically valid receipt that never crossed the authority factory.""" + return StructuredInterviewActivationReceipt( + tenant_record_id="10000000-0000-7000-8000-000000000001", + interview_plan_reference="interview_plan:11111111-1111-4111-8111-111111111111", + plan_digest="a" * 64, + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + authority_evidence_reference=( + "activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + ), + authority_evidence_digest="e" * 64, + approved_at=datetime(2026, 8, 21, 5, 0, tzinfo=timezone.utc), + ) + + +def test_activation_receipt_cannot_be_minted_without_verified_factory_path(): + """Directly constructed values cannot export authoritative activation evidence.""" + receipt = direct_receipt() + + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + receipt.canonical_json() + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + receipt.sha256_digest() + + +def test_receipt_issuance_capabilities_are_not_module_attributes(): + """Do not publish callables or secrets that can mint verified receipt evidence.""" + forbidden_names = ( + "_ACTIVATION_RECEIPT_ISSUANCE_TOKEN", + "_PROCESS_ACTIVATION_RECEIPT_SEAL_KEY", + "_ACTIVATION_RECEIPT_SEALS", + "_register_activation_receipt_seal", + "_seal_activation_receipt", + "_discard_activation_receipt_seal", + "_authoritative_activation_receipt_seal", + ) + + assert all(not hasattr(activation_module, name) for name in forbidden_names) + + +def test_private_constructor_argument_cannot_mint_verified_receipt_directly(): + """Constructor-private-looking keywords must never act as issuance authority.""" + with pytest.raises(TypeError, match="_issuance_token"): + StructuredInterviewActivationReceipt( + tenant_record_id="10000000-0000-7000-8000-000000000001", + interview_plan_reference="interview_plan:11111111-1111-4111-8111-111111111111", + plan_digest="a" * 64, + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + authority_evidence_reference=( + "activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" + ), + authority_evidence_digest="e" * 64, + approved_at=datetime(2026, 8, 21, 5, 0, tzinfo=timezone.utc), + _issuance_token=object(), + ) + + +def test_issued_activation_receipt_cannot_be_replaced_with_unverified_scope(): + """Dataclass replacement creates unissued values rather than reusable trust evidence.""" + candidate_plan = plan() + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification_for(candidate_plan)), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + + replacement = replace(receipt, plan_digest="b" * 64) + + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + replacement.canonical_json() + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + replacement.sha256_digest() + assert receipt.canonical_json() + + +def test_issued_activation_receipt_rejects_post_issuance_rewrite(): + """Reject low-level rewriting of already-issued canonical activation evidence.""" + candidate_plan = plan() + receipt = activate_structured_interview_plan( + plan=candidate_plan, + authority=AllowingAuthority(verification_for(candidate_plan)), + approving_actor_reference=APPROVER, + approved_at=APPROVED_AT, + ) + object.__setattr__(receipt, "plan_digest", "b" * 64) + + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + receipt.canonical_json() + with pytest.raises(ValueError, match="changed after activation receipt issuance"): + receipt.sha256_digest() diff --git a/packages/interview-plan/tests/test_string_runtime_evidence_integrity.py b/packages/interview-plan/tests/test_string_runtime_evidence_integrity.py new file mode 100644 index 000000000..5ce61746e --- /dev/null +++ b/packages/interview-plan/tests/test_string_runtime_evidence_integrity.py @@ -0,0 +1,194 @@ +"""Regression coverage for string-subclass evidence-boundary integrity.""" + +from __future__ import annotations + +from dataclasses import replace +from datetime import datetime, timezone + +import pytest + +from orgmetra_interview_plan import build_structured_interview_plan + + +class ForgedReference(str): + """String subclass that forges namespace/suffix validation while keeping hostile text.""" + + def startswith(self, prefix, *args): # type: ignore[no-untyped-def] + """Pretend the hostile value carries every requested namespace.""" + return True + + def __getitem__(self, key): # type: ignore[no-untyped-def] + """Return a valid UUIDv4 only when validation slices the reference suffix.""" + if isinstance(key, slice): + return "11111111-1111-4111-8111-111111111111" + return super().__getitem__(key) + + +class ForgedTenantUUIDText(str): + """String subclass that forges UUID parsing and canonical-equality checks.""" + + def replace(self, old, new, *args): # type: ignore[no-untyped-def] + """Feed UUID() canonical text instead of the stored hostile tenant text.""" + canonical = "12345678-1234-4234-8234-123456789abc" + return canonical.replace(old, new, *args) + + def __eq__(self, other): # type: ignore[no-untyped-def] + """Pretend hostile tenant text equals every non-null comparison target.""" + if other is None: + return False + return True + + def __ne__(self, other): # type: ignore[no-untyped-def] + """Pretend hostile tenant text differs only from a null comparison target.""" + if other is None: + return True + return False + + +class ForgedGovernanceCode(str): + """String subclass that forges fixed-code equality and allow-list membership.""" + + def __eq__(self, other): # type: ignore[no-untyped-def] + """Pretend hostile governance text equals every comparison target.""" + return True + + def __ne__(self, other): # type: ignore[no-untyped-def] + """Pretend hostile governance text never differs from a comparison target.""" + return False + + def __hash__(self) -> int: + """Return the hash of an allowed reason code to probe set membership defenses.""" + return hash("approved_requisition_interview") + + +class SwitchingReferenceTuple(tuple): + """Tuple subclass that changes references after validation has already completed.""" + + def __new__( + cls, + values: tuple[str, ...], + forged_values: tuple[str, ...], + ) -> "SwitchingReferenceTuple": + """Store valid tuple payload plus later forged references for canonicalization.""" + instance = super().__new__(cls, values) + instance._forged_values = forged_values + instance._iteration_count = 0 + return instance + + def __iter__(self): # type: ignore[no-untyped-def] + """Yield valid references twice, then substitute forged references on later reads.""" + self._iteration_count += 1 + if self._iteration_count >= 3: + return iter(self._forged_values) + return tuple.__iter__(self) + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid structured-interview plan input.""" + return { + "tenant_record_id": "12345678-1234-4234-8234-123456789abc", + "interview_plan_reference": "interview_plan:11111111-1111-4111-8111-111111111111", + "requisition_reference": "requisition:22222222-2222-4222-8222-222222222222", + "job_profile_reference": "job_profile:33333333-3333-4333-8333-333333333333", + "job_analysis_reference": "job_analysis:44444444-4444-4444-8444-444444444444", + "job_analysis_digest": "a" * 64, + "question_set_reference": "question_set:55555555-5555-4555-8555-555555555555", + "question_set_digest": "b" * 64, + "question_competency_map_reference": "question_competency_map:66666666-6666-4666-8666-666666666666", + "question_competency_map_digest": "d" * 64, + "rating_anchor_reference": "rating_anchor:77777777-7777-4777-8777-777777777777", + "rating_anchor_digest": "c" * 64, + "competency_references": ( + "competency:88888888-8888-4888-8888-888888888888", + "competency:99999999-9999-4999-8999-999999999999", + ), + "panel_actor_references": ( + "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ), + "question_count": 4, + "purpose_code": "structured_interview_plan", + "reason_code": "approved_requisition_interview", + "generated_at": datetime(2026, 8, 21, 5, 0, tzinfo=timezone.utc), + } + + +def test_rejects_reference_string_subclass_that_can_forge_namespace_validation() -> None: + """Reject reference subclasses before forged namespace behavior can affect evidence.""" + kwargs = valid_kwargs() + kwargs["interview_plan_reference"] = ForgedReference("attacker-controlled-reference-data") + with pytest.raises(ValueError, match="interview_plan_reference"): + build_structured_interview_plan(**kwargs) + + +def test_rejects_tenant_string_subclass_that_can_forge_uuid_validation() -> None: + """Reject tenant-text subclasses before forged UUID behavior can affect identity evidence.""" + kwargs = valid_kwargs() + kwargs["tenant_record_id"] = ForgedTenantUUIDText("not-a-tenant-uuid") + with pytest.raises(ValueError, match="tenant_record_id"): + build_structured_interview_plan(**kwargs) + + +def test_rejects_purpose_code_string_subclass_that_can_forge_fixed_code_check() -> None: + """Reject purpose-code subclasses before forged equality can bypass the closed code.""" + kwargs = valid_kwargs() + kwargs["purpose_code"] = ForgedGovernanceCode("attacker_controlled_purpose") + with pytest.raises(ValueError, match="purpose_code"): + build_structured_interview_plan(**kwargs) + + +def test_rejects_reason_code_string_subclass_that_can_forge_allow_list_check() -> None: + """Reject reason-code subclasses before forged equality or hashing can bypass policy.""" + kwargs = valid_kwargs() + kwargs["reason_code"] = ForgedGovernanceCode("attacker_controlled_reason") + with pytest.raises(ValueError, match="reason_code"): + build_structured_interview_plan(**kwargs) + + +@pytest.mark.parametrize( + ("field", "forged_value"), + [ + ("review_state", ForgedGovernanceCode("attacker_controlled_review_state")), + ("next_action", ForgedGovernanceCode("attacker_controlled_next_action")), + ], +) +def test_rejects_fixed_governance_text_subclasses_after_plan_construction( + field: str, + forged_value: ForgedGovernanceCode, +) -> None: + """Reject replacement-time string subclasses before immutable governance text can be forged.""" + candidate_plan = build_structured_interview_plan(**valid_kwargs()) + with pytest.raises(ValueError, match=field): + replace(candidate_plan, **{field: forged_value}) + + +@pytest.mark.parametrize( + ("field", "forged_values"), + [ + ( + "competency_references", + ( + "competency:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "competency:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + ), + ), + ( + "panel_actor_references", + ( + "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + ), + ), + ], +) +def test_rejects_reference_tuple_subclasses_before_iteration_can_switch_evidence( + field: str, + forged_values: tuple[str, ...], +) -> None: + """Reject tuple subclasses that can change canonical references after validation.""" + kwargs = valid_kwargs() + original_values = kwargs[field] + assert type(original_values) is tuple + kwargs[field] = SwitchingReferenceTuple(original_values, forged_values) + with pytest.raises(ValueError, match=field): + build_structured_interview_plan(**kwargs) diff --git a/packages/interview-plan/tests/test_supported_python_versions.py b/packages/interview-plan/tests/test_supported_python_versions.py new file mode 100644 index 000000000..14a5c0ecc --- /dev/null +++ b/packages/interview-plan/tests/test_supported_python_versions.py @@ -0,0 +1,19 @@ +"""Regression coverage for the package's declared Python compatibility contract.""" + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[3] + + +def test_declared_python_floor_has_executable_compatibility_lanes() -> None: + """Require every declared minor from 3.12 through the primary 3.14 lane in CI.""" + package_config = (REPOSITORY_ROOT / "packages/interview-plan/pyproject.toml").read_text() + workflow = (REPOSITORY_ROOT / ".github/workflows/interview-plan-quality.yml").read_text() + + assert 'requires-python = ">=3.12"' in package_config + assert "compatibility:" in workflow + assert '"3.12"' in workflow + assert '"3.13"' in workflow + assert 'python-version: "3.14"' in workflow + assert "python-version: ${{ matrix.python-version }}" in workflow diff --git a/packages/interview-plan/tests/test_temporal_evidence_integrity.py b/packages/interview-plan/tests/test_temporal_evidence_integrity.py new file mode 100644 index 000000000..6f10da7d5 --- /dev/null +++ b/packages/interview-plan/tests/test_temporal_evidence_integrity.py @@ -0,0 +1,195 @@ +"""Regression coverage for interview-plan recorded-time evidence integrity.""" + +from __future__ import annotations + +from datetime import datetime, timedelta, timezone, tzinfo +import json + +import pytest + +from orgmetra_interview_plan import ( + StructuredInterviewActivationReceipt, + activate_structured_interview_plan, + build_structured_interview_plan, +) + + +class ForgedDateTime(datetime): + """Datetime subclass able to forge canonical recorded-time evidence.""" + + def astimezone(self, tz=None): # type: ignore[no-untyped-def] + """Keep the hostile subclass alive across UTC normalization.""" + return self + + def isoformat(self, *args, **kwargs) -> str: # type: ignore[no-untyped-def] + """Return an instant different from the underlying plan evidence.""" + return "2099-12-31T23:59:59+00:00" + + +class MutableOffsetTimezone(tzinfo): + """Timezone fixture whose offset can change after plan construction.""" + + def __init__(self, offset_hours: int) -> None: + """Store the mutable offset used by the temporal-integrity regression.""" + self.offset_hours = offset_hours + + def utcoffset(self, value): # type: ignore[no-untyped-def] + """Return the currently configured offset.""" + return timedelta(hours=self.offset_hours) + + def dst(self, value): # type: ignore[no-untyped-def] + """Return zero daylight-saving offset for deterministic behavior.""" + return timedelta(0) + + def tzname(self, value): # type: ignore[no-untyped-def] + """Return a stable diagnostic name for the mutable test timezone.""" + return "MutableOffsetTimezone" + + +class ExplodingOffsetTimezone(tzinfo): + """Timezone fixture that raises while caller-controlled UTC offset is evaluated.""" + + def utcoffset(self, value): # type: ignore[no-untyped-def] + """Simulate hostile or broken caller timezone code at the trust boundary.""" + raise RuntimeError("hostile utcoffset evaluation") + + def dst(self, value): # type: ignore[no-untyped-def] + """Return zero daylight-saving offset when queried independently.""" + return timedelta(0) + + def tzname(self, value): # type: ignore[no-untyped-def] + """Return a stable diagnostic name without evaluating the hostile offset.""" + return "ExplodingOffsetTimezone" + + +class RejectUnexpectedAuthorityCall: + """Fail if activation reaches authority work after invalid time evidence.""" + + def verify_activation(self, **kwargs): # type: ignore[no-untyped-def] + """Prove invalid approval-time normalization fails before authority work.""" + raise AssertionError("authority must not run for out-of-range approval time") + + +def valid_kwargs() -> dict[str, object]: + """Return one otherwise valid structured-interview plan input.""" + return { + "tenant_record_id": "12345678-1234-4234-8234-123456789abc", + "interview_plan_reference": "interview_plan:11111111-1111-4111-8111-111111111111", + "requisition_reference": "requisition:22222222-2222-4222-8222-222222222222", + "job_profile_reference": "job_profile:33333333-3333-4333-8333-333333333333", + "job_analysis_reference": "job_analysis:44444444-4444-4444-8444-444444444444", + "job_analysis_digest": "a" * 64, + "question_set_reference": "question_set:55555555-5555-4555-8555-555555555555", + "question_set_digest": "b" * 64, + "question_competency_map_reference": "question_competency_map:66666666-6666-4666-8666-666666666666", + "question_competency_map_digest": "d" * 64, + "rating_anchor_reference": "rating_anchor:77777777-7777-4777-8777-777777777777", + "rating_anchor_digest": "c" * 64, + "competency_references": ( + "competency:88888888-8888-4888-8888-888888888888", + "competency:99999999-9999-4999-8999-999999999999", + ), + "panel_actor_references": ( + "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ), + "question_count": 4, + "purpose_code": "structured_interview_plan", + "reason_code": "approved_requisition_interview", + "generated_at": datetime(2026, 8, 21, 4, 30, tzinfo=timezone.utc), + } + + +def test_rejects_datetime_subclasses_that_can_forge_recorded_time_evidence() -> None: + """Canonical audit evidence must not call caller-overridable datetime methods.""" + kwargs = valid_kwargs() + kwargs["generated_at"] = ForgedDateTime(2026, 8, 21, 4, 30, tzinfo=timezone.utc) + + with pytest.raises(ValueError, match="generated_at"): + build_structured_interview_plan(**kwargs) + + +def test_plan_detaches_mutable_generated_at_timezone_before_sealing() -> None: + """Caller timezone mutation must not change or invalidate already-issued plan evidence.""" + mutable_timezone = MutableOffsetTimezone(1) + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime(2026, 8, 21, 5, 30, 0, 123456, tzinfo=mutable_timezone) + + candidate_plan = build_structured_interview_plan(**kwargs) + mutable_timezone.offset_hours = 2 + + assert candidate_plan.generated_at.tzinfo is timezone.utc + assert candidate_plan.generated_at == datetime(2026, 8, 21, 4, 30, 0, 123456, tzinfo=timezone.utc) + assert json.loads(candidate_plan.canonical_json())["generated_at"] == "2026-08-21T04:30:00.123456Z" + + +def test_plan_normalizes_hostile_timezone_failure_to_validation_error() -> None: + """Caller timezone code must not leak arbitrary exceptions through plan validation.""" + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime(2026, 8, 21, 4, 30, tzinfo=ExplodingOffsetTimezone()) + + with pytest.raises(ValueError, match="generated_at must be an exact timezone-aware datetime"): + build_structured_interview_plan(**kwargs) + + +def test_plan_rejects_utc_normalization_beyond_datetime_min_as_validation_error() -> None: + """Out-of-range UTC conversion must fail as governed plan validation, not OverflowError.""" + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime.min.replace(tzinfo=timezone(timedelta(hours=1))) + + with pytest.raises(ValueError, match="generated_at must be an exact timezone-aware datetime"): + build_structured_interview_plan(**kwargs) + + +def test_activation_normalizes_hostile_timezone_failure_before_authority() -> None: + """Approval-time timezone failures must remain validation errors before side effects.""" + candidate_plan = build_structured_interview_plan(**valid_kwargs()) + + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=RejectUnexpectedAuthorityCall(), + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + approved_at=datetime(2026, 8, 21, 5, 0, tzinfo=ExplodingOffsetTimezone()), + ) + + +def test_activation_rejects_utc_normalization_beyond_datetime_max_before_authority() -> None: + """Out-of-range approval UTC conversion must fail before authoritative side effects.""" + candidate_plan = build_structured_interview_plan(**valid_kwargs()) + + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + activate_structured_interview_plan( + plan=candidate_plan, + authority=RejectUnexpectedAuthorityCall(), + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + approved_at=datetime.max.replace(tzinfo=timezone(-timedelta(hours=1))), + ) + + +def test_activation_receipt_normalizes_hostile_timezone_failure() -> None: + """Receipt construction must not leak arbitrary caller timezone exceptions.""" + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + StructuredInterviewActivationReceipt( + tenant_record_id="12345678-1234-4234-8234-123456789abc", + interview_plan_reference="interview_plan:11111111-1111-4111-8111-111111111111", + plan_digest="a" * 64, + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + authority_evidence_reference="activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", + authority_evidence_digest="e" * 64, + approved_at=datetime(2026, 8, 21, 5, 0, tzinfo=ExplodingOffsetTimezone()), + ) + + +def test_activation_receipt_names_approved_at_when_recorded_time_is_invalid() -> None: + """Tell callers which approval timestamp must be repaired before activation can proceed.""" + with pytest.raises(ValueError, match="approved_at must be an exact timezone-aware datetime"): + StructuredInterviewActivationReceipt( + tenant_record_id="12345678-1234-4234-8234-123456789abc", + interview_plan_reference="interview_plan:11111111-1111-4111-8111-111111111111", + plan_digest="a" * 64, + approving_actor_reference="actor:dddddddd-dddd-4ddd-8ddd-dddddddddddd", + authority_evidence_reference="activation_verification:eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", + authority_evidence_digest="e" * 64, + approved_at=datetime(2026, 8, 21, 5, 0), + ) diff --git a/packages/interview-plan/tests/test_tenant_scope.py b/packages/interview-plan/tests/test_tenant_scope.py new file mode 100644 index 000000000..faf457793 --- /dev/null +++ b/packages/interview-plan/tests/test_tenant_scope.py @@ -0,0 +1,22 @@ +"""Tenant-scope regressions for governed structured-interview activation.""" + +from orgmetra_interview_plan import StructuredInterviewPlan +from test_plan import values + + +def test_activation_requires_authoritative_tenant_and_job_scope_resolution() -> None: + """Do not infer tenant ownership or Job linkage from opaque references and digests.""" + action = StructuredInterviewPlan(**values()).next_action + + assert "Within tenant_record_id, re-resolve every plan reference" in action + assert "verify the requisition-to-Job-to-job-analysis binding" in action + assert "verify question-set, question-to-competency mapping, and rating-anchor provenance" in action + + +def test_activation_requires_authoritative_panel_actor_separation() -> None: + """Do not treat distinct actor-reference strings as distinct authoritative people.""" + action = StructuredInterviewPlan(**values()).next_action + + assert "re-resolve every panel_actor_reference" in action + assert "prove the resolved panel actor identities are distinct" in action + assert "verify panel eligibility and training" in action diff --git a/packages/interview-plan/tests/test_traceability_scope.py b/packages/interview-plan/tests/test_traceability_scope.py new file mode 100644 index 000000000..fb4db18cb --- /dev/null +++ b/packages/interview-plan/tests/test_traceability_scope.py @@ -0,0 +1,26 @@ +"""Regression contracts for honest structured-interview activation traceability.""" + +from __future__ import annotations + +from pathlib import Path + + +TRACEABILITY = Path(__file__).resolve().parents[3] / "docs" / "traceability" / "structured-interview-plan.md" + + +def test_traceability_matches_executable_activation_boundary() -> None: + """Keep traceability aligned with the executable host-orchestration boundary and its limits.""" + text = TRACEABILITY.read_text(encoding="utf-8") + + assert "implements an executable activation orchestration boundary" in text + assert "`StructuredInterviewActivationAuthority`" in text + assert "built-in UTC approval snapshot" in text + assert "`test_activation_executes_authority_and_returns_immutable_human_receipt`" in text + assert "`test_authority_rejection_blocks_activation`" in text + assert "`test_activation_rejects_authority_evidence_for_other_scope`" in text + assert "`test_activation_detaches_plan_evidence_from_authority_time_aba_mutation`" in text + assert "pre-call request" in text + assert "A concrete production adapter remains responsible" in text + assert "do **not** prove that a particular deployed adapter already performs database/API resolution correctly" in text + assert "No host activation path is implemented in this slice." not in text + assert "(next_action contract regression only)" not in text diff --git a/packages/interview-plan/tests/test_uuid_version.py b/packages/interview-plan/tests/test_uuid_version.py new file mode 100644 index 000000000..c7696929b --- /dev/null +++ b/packages/interview-plan/tests/test_uuid_version.py @@ -0,0 +1,67 @@ +"""Regression coverage for tenant interoperability and UUIDv4 trust references.""" + +from datetime import datetime, timezone + +import pytest + +from orgmetra_interview_plan import build_structured_interview_plan + +_UUID1 = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +_UUID7_TENANT = "10000000-0000-7000-8000-000000000001" + + +def _plan_kwargs() -> dict[str, object]: + """Return one valid structured-interview plan input mapping.""" + return { + "tenant_record_id": "12345678-1234-4234-8234-123456789abc", + "interview_plan_reference": "interview_plan:11111111-1111-4111-8111-111111111111", + "requisition_reference": "requisition:22222222-2222-4222-8222-222222222222", + "job_profile_reference": "job_profile:33333333-3333-4333-8333-333333333333", + "job_analysis_reference": "job_analysis:44444444-4444-4444-8444-444444444444", + "job_analysis_digest": "a" * 64, + "question_set_reference": "question_set:55555555-5555-4555-8555-555555555555", + "question_set_digest": "b" * 64, + "question_competency_map_reference": "question_competency_map:66666666-6666-4666-8666-666666666666", + "question_competency_map_digest": "c" * 64, + "rating_anchor_reference": "rating_anchor:77777777-7777-4777-8777-777777777777", + "rating_anchor_digest": "d" * 64, + "competency_references": ("competency:88888888-8888-4888-8888-888888888888",), + "panel_actor_references": ( + "actor:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", + "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ), + "question_count": 2, + "purpose_code": "structured_interview_plan", + "reason_code": "approved_requisition_interview", + "generated_at": datetime(2026, 8, 19, 12, 0, tzinfo=timezone.utc), + "evidence_version": 1, + } + + +def test_authoritative_uuid7_tenant_identity_is_accepted() -> None: + """Accept the canonical UUIDv7 tenant identity already valid in Orgmetra core.""" + data = _plan_kwargs() + data["tenant_record_id"] = _UUID7_TENANT + + plan = build_structured_interview_plan(**data) + + assert plan.tenant_record_id == _UUID7_TENANT + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("job_profile_reference", f"job_profile:{_UUID1}"), + ("competency_references", (f"competency:{_UUID1}",)), + ( + "panel_actor_references", + (f"actor:{_UUID1}", "actor:cccccccc-cccc-4ccc-8ccc-cccccccccccc"), + ), + ], +) +def test_uuid1_trust_references_fail_closed(field: str, value: object) -> None: + """Reject time/node-bearing UUIDv1 suffixes across scalar and collection references.""" + data = _plan_kwargs() + data[field] = value + with pytest.raises(ValueError, match="canonical-uuid"): + build_structured_interview_plan(**data) diff --git a/tests/test_foundation_ci_dependency_hygiene.sh b/tests/test_foundation_ci_dependency_hygiene.sh index 6a6cb51a8..5c68789ec 100644 --- a/tests/test_foundation_ci_dependency_hygiene.sh +++ b/tests/test_foundation_ci_dependency_hygiene.sh @@ -58,21 +58,74 @@ if [[ ! -f "${requirements_path}" ]]; then exit 1 fi -mapfile -t package_lines < <(grep -Ev '^[[:space:]]*(#|$)' "${requirements_path}") +# pip requirements may carry multiple platform-specific hashes on backslash- +# continued physical lines. Validate and count logical requirements so adding +# reviewed wheel hashes cannot be misclassified as extra packages. +mapfile -t package_lines < <( + awk ' + /^[[:space:]]*(#|$)/ { next } + { + line=$0 + sub(/^[[:space:]]+/, "", line) + continues=(line ~ /\\[[:space:]]*$/) + sub(/[[:space:]]*\\[[:space:]]*$/, "", line) + if (logical == "") { + logical=line + } else { + logical=logical " " line + } + if (!continues) { + print logical + logical="" + } + } + END { + if (logical != "") { + print "__UNTERMINATED__ " logical + } + } + ' "${requirements_path}" +) + if [[ "${#package_lines[@]}" -ne 7 ]]; then printf 'Foundation CI requirements must contain the seven reviewed direct/runtime test packages.\n' >&2 exit 1 fi for package_line in "${package_lines[@]}"; do - if [[ ! "${package_line}" =~ ^[A-Za-z0-9._-]+==[0-9][A-Za-z0-9._-]*[[:space:]]--hash=sha256:[0-9a-f]{64}$ ]]; then + if [[ "${package_line}" == __UNTERMINATED__* ]]; then + printf 'Foundation CI requirement has an unterminated continuation: %s\n' "${package_line#__UNTERMINATED__ }" >&2 + exit 1 + fi + if [[ ! "${package_line}" =~ ^[A-Za-z0-9._-]+==[0-9][A-Za-z0-9._-]*([[:space:]]--hash=sha256:[0-9a-f]{64})+$ ]]; then printf 'Unpinned or unhashed Foundation CI requirement: %s\n' "${package_line}" >&2 exit 1 fi done +package_line_is_present() { + local package_name="$1" + shift + local package_line + for package_line in "$@"; do + if [[ "${package_line}" == "${package_name}=="* ]]; then + return 0 + fi + done + return 1 +} + +stress_package_lines=() +for _ in {1..128}; do + stress_package_lines+=("${package_lines[@]}") +done +if ! package_line_is_present coverage "${stress_package_lines[@]}"; then + printf 'Foundation CI required-package lookup must remain reliable under pipefail after an early match.\n' >&2 + exit 1 +fi + for package_name in coverage iniconfig packaging pluggy Pygments pytest pytest-cov; do - if ! printf '%s\n' "${package_lines[@]}" | grep -Eq "^${package_name}=="; then + if ! package_line_is_present "${package_name}" "${package_lines[@]}"; then printf 'Foundation CI requirement is missing: %s\n' "${package_name}" >&2 exit 1 fi