From e03381379c06a1820305f1e53fe0488a5c14f59d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:38:04 -0700 Subject: [PATCH 01/48] test: define candidate evidence intake contract --- .../workflows/candidate-evidence-quality.yml | 57 ++++++++ packages/candidate-evidence/pyproject.toml | 24 +++ .../candidate-evidence/tests/test_packet.py | 138 ++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 .github/workflows/candidate-evidence-quality.yml create mode 100644 packages/candidate-evidence/pyproject.toml create mode 100644 packages/candidate-evidence/tests/test_packet.py diff --git a/.github/workflows/candidate-evidence-quality.yml b/.github/workflows/candidate-evidence-quality.yml new file mode 100644 index 000000000..0e573e611 --- /dev/null +++ b/.github/workflows/candidate-evidence-quality.yml @@ -0,0 +1,57 @@ +name: Candidate Evidence Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/candidate-evidence/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/candidate-evidence-quality.yml" + - "docs/adr/0015-governed-candidate-evidence-intake.md" + - "docs/doctoring/candidate-evidence-intake-references.md" + - "docs/traceability/candidate-evidence-intake.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: candidate-evidence-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Candidate evidence 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 candidate evidence package + run: python -m compileall -q packages/candidate-evidence/src packages/candidate-evidence/tests + - name: Test candidate evidence with exact statement and branch coverage + env: + PYTHONPATH: packages/candidate-evidence/src + COVERAGE_FILE: /tmp/orgmetra-candidate-evidence.coverage + run: python -m pytest -c packages/candidate-evidence/pyproject.toml packages/candidate-evidence/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/packages/candidate-evidence/pyproject.toml b/packages/candidate-evidence/pyproject.toml new file mode 100644 index 000000000..7d9f42023 --- /dev/null +++ b/packages/candidate-evidence/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-candidate-evidence" +version = "0.1.0" +description = "PII-minimized candidate-evidence intake correlation 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_candidate_evidence", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py new file mode 100644 index 000000000..45c4cc41e --- /dev/null +++ b/packages/candidate-evidence/tests/test_packet.py @@ -0,0 +1,138 @@ +from dataclasses import replace +from datetime import datetime, timedelta, timezone, tzinfo +from hashlib import sha256 +import json +import pytest + +from orgmetra_candidate_evidence import ( + CandidateEvidenceIntakePacket, + build_candidate_evidence_intake_packet, +) + +TENANT = "12345678-1234-4234-8234-123456789abc" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +DIGEST_E = "e" * 64 + + +def values(): + return dict( + tenant_record_id=TENANT, + intake_reference="candidate_evidence_intake:intake-2026-001", + candidate_profile_reference="candidate_profile:candidate-001", + requisition_reference="requisition:req-2026-001", + job_profile_reference="job_profile:job-001", + job_requirements_reference="job_requirements:requirements-v3", + job_requirements_digest=DIGEST_A, + evidence_set_reference="evidence_set:evidence-v2", + evidence_set_digest=DIGEST_B, + source_provenance_reference="source_provenance:manifest-v4", + source_provenance_digest=DIGEST_C, + handling_policy_reference="handling_policy:recruiting-candidate-v2", + handling_policy_digest=DIGEST_D, + retention_policy_reference="retention_policy:recruiting-2026-v1", + retention_policy_digest=DIGEST_E, + actor_reference="actor:recruiter-007", + evidence_item_count=5, + purpose_code="candidate_evidence_intake", + reason_code="requisition_candidate_review", + collected_at=datetime(2026, 8, 19, 1, 2, 3, 456789, tzinfo=timezone.utc), + ) + + +def test_builds_reference_only_deterministic_packet(): + packet = build_candidate_evidence_intake_packet(**values()) + payload = json.loads(packet.canonical_json()) + assert payload["review_state"] == "requires_human_review" + assert payload["human_confirmation_required"] is True + assert payload["collected_at"].endswith(".456789Z") + assert payload["candidate_profile_reference"] == "candidate_profile:candidate-001" + forbidden = ("candidate_name", "email", "demographic", "assessment_value", "raw_evidence", "model_output") + assert all(name not in payload for name in forbidden) + assert packet.sha256_digest() == sha256(packet.canonical_json().encode("utf-8")).hexdigest() + assert packet == CandidateEvidenceIntakePacket(**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"), + ("intake_reference", "wrong:intake"), + ("candidate_profile_reference", "wrong:candidate"), + ("requisition_reference", "wrong:req"), + ("job_profile_reference", "wrong:job"), + ("job_requirements_reference", "wrong:reqs"), + ("evidence_set_reference", "wrong:evidence"), + ("source_provenance_reference", "wrong:source"), + ("handling_policy_reference", "wrong:handling"), + ("retention_policy_reference", "wrong:retention"), + ("actor_reference", "wrong:actor"), + ("job_requirements_digest", "A" * 64), + ("evidence_set_digest", "b" * 63), + ("source_provenance_digest", "C" * 64), + ("handling_policy_digest", "d" * 63), + ("retention_policy_digest", 7), + ("purpose_code", "wrong_purpose"), + ("purpose_code", "bad"), + ("reason_code", "Bad Reason"), + ("reason_code", "a_" + "b" * 64), + ("collected_at", datetime(2026, 8, 19, 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): + data = values() + data[field] = bad + with pytest.raises((ValueError, TypeError)): + CandidateEvidenceIntakePacket(**data) + + +@pytest.mark.parametrize("count", [True, 0, 101, 1.0]) +def test_rejects_invalid_evidence_item_count(count): + data = values() + data["evidence_item_count"] = count + with pytest.raises(ValueError, match="evidence_item_count"): + CandidateEvidenceIntakePacket(**data) + + +class UnknownOffset(tzinfo): + def utcoffset(self, dt): + return None + + def dst(self, dt): + return None + + +def test_rejects_timezone_with_unknown_offset(): + data = values() + data["collected_at"] = datetime(2026, 8, 19, tzinfo=UnknownOffset()) + with pytest.raises(ValueError, match="timezone-aware"): + CandidateEvidenceIntakePacket(**data) + + +def test_canonicalizes_non_utc_offset_and_preserves_fractional_precision(): + data = values() + data["collected_at"] = datetime( + 2026, 8, 19, 10, 2, 3, 456789, tzinfo=timezone(timedelta(hours=9)) + ) + payload = json.loads(CandidateEvidenceIntakePacket(**data).canonical_json()) + assert payload["collected_at"] == "2026-08-19T01:02:03.456789Z" + + +def test_distinct_subsecond_instants_produce_distinct_evidence(): + first = CandidateEvidenceIntakePacket(**values()) + data = values() + data["collected_at"] = data["collected_at"].replace(microsecond=456790) + second = CandidateEvidenceIntakePacket(**data) + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +def test_direct_replace_is_revalidated(): + packet = CandidateEvidenceIntakePacket(**values()) + with pytest.raises(ValueError, match="retention_policy_digest"): + replace(packet, retention_policy_digest="not-a-digest") From 1dc2f1395d333c269e684222e9bff458de185805 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:40:00 -0700 Subject: [PATCH 02/48] feat: add governed candidate evidence intake --- ...0015-governed-candidate-evidence-intake.md | 44 ++++ .../candidate-evidence-intake-references.md | 17 ++ .../traceability/candidate-evidence-intake.md | 19 ++ packages/candidate-evidence/CHANGELOG.md | 8 + packages/candidate-evidence/README.md | 11 + .../orgmetra_candidate_evidence/__init__.py | 5 + .../src/orgmetra_candidate_evidence/packet.py | 206 ++++++++++++++++++ 7 files changed, 310 insertions(+) create mode 100644 docs/adr/0015-governed-candidate-evidence-intake.md create mode 100644 docs/doctoring/candidate-evidence-intake-references.md create mode 100644 docs/traceability/candidate-evidence-intake.md create mode 100644 packages/candidate-evidence/CHANGELOG.md create mode 100644 packages/candidate-evidence/README.md create mode 100644 packages/candidate-evidence/src/orgmetra_candidate_evidence/__init__.py create mode 100644 packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md new file mode 100644 index 000000000..7815e5453 --- /dev/null +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -0,0 +1,44 @@ +# ADR 0015: Govern candidate evidence intake as reference-only evidence + +- **Status:** Proposed — active PR only +- **Date:** 2026-08-19 + +## Context + +Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. + +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. At the same time, a bare file reference is insufficient: an accountable reviewer needs evidence that the intake was tied to the right candidate, requisition, authoritative Job and job requirements, source-provenance manifest, handling policy, retention policy, actor, purpose, and exact evidence-set version. + +## Decision + +Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: + +- canonical tenant identity and one opaque intake reference; +- one candidate profile, requisition, and authoritative Job reference; +- exact job-requirements reference plus SHA-256 digest; +- exact evidence-set and source-provenance references plus independent SHA-256 digests; +- exact handling-policy and retention-policy references plus independent SHA-256 digests; +- one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; +- mandatory human confirmation, immutable `requires_human_review` state, and actionable next-step copy. + +The governance packet is reference-only. It contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. Direct construction and builder construction share the same fail-closed validation. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, or final approval. + +## Consequences + +### Positive + +- Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. +- Source provenance, purpose-bound handling, and retention become explicit versioned evidence rather than prose-only assumptions. +- Downstream evidence sealing and human selection review can reject drift by reference/digest. +- The package remains standalone and MSA-friendly and performs no cross-service application-table SQL. + +### Costs and constraints + +- The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. +- Opaque candidate references are still sensitive correlating metadata and require least-privilege handling. +- Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. +- This ADR remains proposed until its exact PR head merges into protected `develop`. + +## References + +See `docs/doctoring/candidate-evidence-intake-references.md`. diff --git a/docs/doctoring/candidate-evidence-intake-references.md b/docs/doctoring/candidate-evidence-intake-references.md new file mode 100644 index 000000000..dd62ecf7a --- /dev/null +++ b/docs/doctoring/candidate-evidence-intake-references.md @@ -0,0 +1,17 @@ +# Candidate evidence intake references + +These sources inform the active-PR intake boundary. They do not establish certification, jurisdiction-specific legal compliance, or permission to collect/use any particular candidate evidence item. + +## 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 + +International Organization for Standardization, & International Electrotechnical Commission. (2025). *ISO/IEC 27701:2025 information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance*. https://www.iso.org/standard/27701 + +U.S. Equal Employment Opportunity Commission. (n.d.). *Regulations and guidelines*. Retrieved August 19, 2026, from https://www.eeoc.gov/regulations-and-guidelines + +U.S. Equal Employment Opportunity Commission. (2007, May 16). *Employment tests and selection procedures*. https://www.eeoc.gov/laws/guidance/employment-tests-and-selection-procedures + +## Applied boundary + +ISO 30405:2023 provides current recruitment-process guidance spanning preparation, sourcing, assessment, employment and review. ISO/IEC 27701:2025 provides current requirements and guidance for privacy-information management. EEOC identifies 29 CFR Part 1607 as the Uniform Guidelines on Employee Selection Procedures and notes that selection procedures should be evaluated for job relatedness and disparate impact. Orgmetra therefore keeps candidate values outside this governance packet while binding the exact Job requirements, evidence/provenance identities, handling and retention policy versions, accountable actor, purpose and human-review state needed for downstream audit. diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md new file mode 100644 index 000000000..841b5b6f5 --- /dev/null +++ b/docs/traceability/candidate-evidence-intake.md @@ -0,0 +1,19 @@ +# Candidate evidence intake traceability + +- **Maturity:** `implemented_on_active_pr` +- **Buyer capability:** Candidate Evidence workspace governance boundary +- **Owned contract:** `CandidateEvidenceIntakePacket` + +| Requirement | Evidence | +|---|---| +| Correlate candidate evidence to the correct recruiting context | Canonical tenant, candidate-profile, requisition and Job references | +| Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | +| Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | +| Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | +| Preserve accountable collection context | Actor reference, fixed purpose, bounded reason, exact evidence-item count and precision-preserving UTC time | +| Prevent packet-as-decision misuse | Exact boolean human confirmation, immutable `requires_human_review` state and fixed next action | +| Preserve deterministic audit correlation | Canonical JSON + independently testable SHA-256 digest | +| Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | +| Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | + +The packet is correlation evidence, not proof of evidence truth, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. Those claims require their own owner-bound evidence. diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md new file mode 100644 index 000000000..7ca054814 --- /dev/null +++ b/packages/candidate-evidence/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## 0.1.0 — 2026-08-19 + +- Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. +- Bind evidence, source provenance, handling and retention artifacts by opaque reference and SHA-256 digest. +- Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. +- Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md new file mode 100644 index 000000000..024a35b3e --- /dev/null +++ b/packages/candidate-evidence/README.md @@ -0,0 +1,11 @@ +# Orgmetra candidate evidence intake + +This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. + +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, evidence-item count, and collection time. Every trust-bearing artifact is represented by an opaque reference and, where content drift matters, an independent lowercase SHA-256 digest. + +The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. An opaque candidate reference remains sensitive correlating metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. + +The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and the next action tells the operator to verify job relevance, source provenance, retention handling, and completeness before sealing the evidence set for accountable human review. + +Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/__init__.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/__init__.py new file mode 100644 index 000000000..643fe42b1 --- /dev/null +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/__init__.py @@ -0,0 +1,5 @@ +"""Public candidate-evidence intake contract.""" + +from .packet import CandidateEvidenceIntakePacket, build_candidate_evidence_intake_packet + +__all__ = ["CandidateEvidenceIntakePacket", "build_candidate_evidence_intake_packet"] diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py new file mode 100644 index 000000000..bd41062f7 --- /dev/null +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -0,0 +1,206 @@ +"""Governed, PII-minimized candidate-evidence intake correlation. + +The contract binds candidate evidence intake to an authoritative candidate, requisition, +Job, job requirements, evidence-set identity, retention policy, and accountable actor. +It intentionally carries no raw candidate evidence or candidate PII. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$") +_PURPOSE_CODE = "candidate_evidence_intake" +_REVIEW_STATE = "requires_human_review" +_NEXT_ACTION = ( + "Verify job relevance, source provenance, retention handling, and evidence completeness " + "before sealing this candidate evidence set for accountable human review." +) + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require canonical non-sentinel UUID text for a governance identity.""" + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_code(value: str, field_name: str) -> None: + """Require a bounded descriptive lower snake_case governance code.""" + if not isinstance(value, 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 a bounded namespaced opaque reference with the expected prefix.""" + if ( + not isinstance(value, str) + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + + +def _validate_digest(value: str, field_name: str) -> None: + """Require lowercase SHA-256 hexadecimal evidence.""" + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _canonical_timestamp(value: datetime) -> str: + """Render an aware instant as precision-preserving UTC RFC 3339 text.""" + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise ValueError("collected_at must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@dataclass(frozen=True, slots=True) +class CandidateEvidenceIntakePacket: + """Immutable reference-only candidate-evidence intake awaiting human review.""" + + tenant_record_id: str + intake_reference: str + candidate_profile_reference: str + requisition_reference: str + job_profile_reference: str + job_requirements_reference: str + job_requirements_digest: str + evidence_set_reference: str + evidence_set_digest: str + source_provenance_reference: str + source_provenance_digest: str + handling_policy_reference: str + handling_policy_digest: str + retention_policy_reference: str + retention_policy_digest: str + actor_reference: str + evidence_item_count: int + purpose_code: str + reason_code: str + collected_at: datetime + human_confirmation_required: bool = True + review_state: str = _REVIEW_STATE + next_action: str = _NEXT_ACTION + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed contract.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference(self.intake_reference, "candidate_evidence_intake", "intake_reference") + _validate_reference(self.candidate_profile_reference, "candidate_profile", "candidate_profile_reference") + _validate_reference(self.requisition_reference, "requisition", "requisition_reference") + _validate_reference(self.job_profile_reference, "job_profile", "job_profile_reference") + _validate_reference(self.job_requirements_reference, "job_requirements", "job_requirements_reference") + _validate_digest(self.job_requirements_digest, "job_requirements_digest") + _validate_reference(self.evidence_set_reference, "evidence_set", "evidence_set_reference") + _validate_digest(self.evidence_set_digest, "evidence_set_digest") + _validate_reference(self.source_provenance_reference, "source_provenance", "source_provenance_reference") + _validate_digest(self.source_provenance_digest, "source_provenance_digest") + _validate_reference(self.handling_policy_reference, "handling_policy", "handling_policy_reference") + _validate_digest(self.handling_policy_digest, "handling_policy_digest") + _validate_reference(self.retention_policy_reference, "retention_policy", "retention_policy_reference") + _validate_digest(self.retention_policy_digest, "retention_policy_digest") + _validate_reference(self.actor_reference, "actor", "actor_reference") + if type(self.evidence_item_count) is not int or not 1 <= self.evidence_item_count <= 100: + raise ValueError("evidence_item_count must be an integer from 1 through 100") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain candidate_evidence_intake") + _validate_code(self.reason_code, "reason_code") + _canonical_timestamp(self.collected_at) + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before sealing candidate evidence") + if self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_review") + if self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed candidate-evidence instruction") + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + payload = { + "actor_reference": self.actor_reference, + "candidate_profile_reference": self.candidate_profile_reference, + "collected_at": _canonical_timestamp(self.collected_at), + "evidence_item_count": self.evidence_item_count, + "evidence_set_digest": self.evidence_set_digest, + "evidence_set_reference": self.evidence_set_reference, + "handling_policy_digest": self.handling_policy_digest, + "handling_policy_reference": self.handling_policy_reference, + "human_confirmation_required": self.human_confirmation_required, + "intake_reference": self.intake_reference, + "job_profile_reference": self.job_profile_reference, + "job_requirements_digest": self.job_requirements_digest, + "job_requirements_reference": self.job_requirements_reference, + "next_action": self.next_action, + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "requisition_reference": self.requisition_reference, + "retention_policy_digest": self.retention_policy_digest, + "retention_policy_reference": self.retention_policy_reference, + "review_state": self.review_state, + "source_provenance_digest": self.source_provenance_digest, + "source_provenance_reference": self.source_provenance_reference, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 intake packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_candidate_evidence_intake_packet( + *, + tenant_record_id: str, + intake_reference: str, + candidate_profile_reference: str, + requisition_reference: str, + job_profile_reference: str, + job_requirements_reference: str, + job_requirements_digest: str, + evidence_set_reference: str, + evidence_set_digest: str, + source_provenance_reference: str, + source_provenance_digest: str, + handling_policy_reference: str, + handling_policy_digest: str, + retention_policy_reference: str, + retention_policy_digest: str, + actor_reference: str, + evidence_item_count: int, + purpose_code: str, + reason_code: str, + collected_at: datetime, +) -> CandidateEvidenceIntakePacket: + """Build a reference-only candidate-evidence packet pending accountable review.""" + return CandidateEvidenceIntakePacket( + tenant_record_id=tenant_record_id, + intake_reference=intake_reference, + candidate_profile_reference=candidate_profile_reference, + requisition_reference=requisition_reference, + job_profile_reference=job_profile_reference, + job_requirements_reference=job_requirements_reference, + job_requirements_digest=job_requirements_digest, + evidence_set_reference=evidence_set_reference, + evidence_set_digest=evidence_set_digest, + source_provenance_reference=source_provenance_reference, + source_provenance_digest=source_provenance_digest, + handling_policy_reference=handling_policy_reference, + handling_policy_digest=handling_policy_digest, + retention_policy_reference=retention_policy_reference, + retention_policy_digest=retention_policy_digest, + actor_reference=actor_reference, + evidence_item_count=evidence_item_count, + purpose_code=purpose_code, + reason_code=reason_code, + collected_at=collected_at, + ) From 84b55544d4a80d57a8fd9ae95a35a9746b45f085 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:41:40 -0700 Subject: [PATCH 03/48] test: require actionable evidence sealing handoff --- packages/candidate-evidence/tests/test_packet.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index 45c4cc41e..15ac8939e 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -15,6 +15,10 @@ DIGEST_C = "c" * 64 DIGEST_D = "d" * 64 DIGEST_E = "e" * 64 +EXPECTED_NEXT_ACTION = ( + "Verify job relevance, source provenance, permitted handling, retention, and evidence " + "completeness; then request authoritative evidence sealing and accountable human review." +) def values(): @@ -47,6 +51,7 @@ def test_builds_reference_only_deterministic_packet(): payload = json.loads(packet.canonical_json()) assert payload["review_state"] == "requires_human_review" assert payload["human_confirmation_required"] is True + assert payload["next_action"] == EXPECTED_NEXT_ACTION assert payload["collected_at"].endswith(".456789Z") assert payload["candidate_profile_reference"] == "candidate_profile:candidate-001" forbidden = ("candidate_name", "email", "demographic", "assessment_value", "raw_evidence", "model_output") From e8563f794a109e16fbe79602ab35d2ae840d252f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:42:30 -0700 Subject: [PATCH 04/48] fix: make candidate evidence next action explicit --- .../src/orgmetra_candidate_evidence/packet.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index bd41062f7..528ba0c0b 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -1,8 +1,9 @@ """Governed, PII-minimized candidate-evidence intake correlation. The contract binds candidate evidence intake to an authoritative candidate, requisition, -Job, job requirements, evidence-set identity, retention policy, and accountable actor. -It intentionally carries no raw candidate evidence or candidate PII. +Job, job requirements, evidence-set identity, handling/retention policy, and accountable +actor. It carries no raw candidate evidence values; the opaque candidate reference remains +sensitive correlating metadata. """ from __future__ import annotations @@ -19,8 +20,8 @@ _PURPOSE_CODE = "candidate_evidence_intake" _REVIEW_STATE = "requires_human_review" _NEXT_ACTION = ( - "Verify job relevance, source provenance, retention handling, and evidence completeness " - "before sealing this candidate evidence set for accountable human review." + "Verify job relevance, source provenance, permitted handling, retention, and evidence " + "completeness; then request authoritative evidence sealing and accountable human review." ) From e6a640e69d99d614053c24cffbc3cd0a03bf55fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:44:33 -0700 Subject: [PATCH 05/48] docs: correct candidate evidence source metadata --- docs/doctoring/candidate-evidence-intake-references.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/candidate-evidence-intake-references.md b/docs/doctoring/candidate-evidence-intake-references.md index dd62ecf7a..47edcb351 100644 --- a/docs/doctoring/candidate-evidence-intake-references.md +++ b/docs/doctoring/candidate-evidence-intake-references.md @@ -6,11 +6,11 @@ These sources inform the active-PR intake boundary. They do not establish certif International Organization for Standardization. (2023). *ISO 30405:2023 human resource management — Guidelines on recruitment* (2nd ed.). https://www.iso.org/standard/79488.html -International Organization for Standardization, & International Electrotechnical Commission. (2025). *ISO/IEC 27701:2025 information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance*. https://www.iso.org/standard/27701 +International Organization for Standardization, & International Electrotechnical Commission. (2025). *ISO/IEC 27701:2025 information security, cybersecurity and privacy protection — Privacy information management systems — Requirements and guidance* (2nd ed.). https://www.iso.org/standard/27701 U.S. Equal Employment Opportunity Commission. (n.d.). *Regulations and guidelines*. Retrieved August 19, 2026, from https://www.eeoc.gov/regulations-and-guidelines -U.S. Equal Employment Opportunity Commission. (2007, May 16). *Employment tests and selection procedures*. https://www.eeoc.gov/laws/guidance/employment-tests-and-selection-procedures +U.S. Equal Employment Opportunity Commission. (2007, December 1). *Employment tests and selection procedures*. https://www.eeoc.gov/laws/guidance/employment-tests-and-selection-procedures ## Applied boundary From ece860c3beb98e553fad55592f06edec2c8e2ed8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 12:04:03 -0700 Subject: [PATCH 06/48] fix: keep candidate evidence changelog unreleased --- packages/candidate-evidence/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index 7ca054814..98e8f747c 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.1.0 — 2026-08-19 +## Unreleased - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. - Bind evidence, source provenance, handling and retention artifacts by opaque reference and SHA-256 digest. From 6822ca9aeabc383dd6c1afefe1cb77d5a28c2a47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:14:49 -0700 Subject: [PATCH 07/48] test: harden candidate evidence references --- .../candidate-evidence/tests/test_packet.py | 141 +++++++++++++----- 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index 15ac8939e..f468def7a 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta, timezone, tzinfo from hashlib import sha256 import json + import pytest from orgmetra_candidate_evidence import ( @@ -10,6 +11,18 @@ ) TENANT = "12345678-1234-4234-8234-123456789abc" +REF = { + "intake": "11111111-1111-4111-8111-111111111111", + "candidate": "22222222-2222-4222-8222-222222222222", + "requisition": "33333333-3333-4333-8333-333333333333", + "job": "44444444-4444-4444-8444-444444444444", + "requirements": "55555555-5555-4555-8555-555555555555", + "evidence": "66666666-6666-4666-8666-666666666666", + "source": "77777777-7777-4777-8777-777777777777", + "handling": "88888888-8888-4888-8888-888888888888", + "retention": "99999999-9999-4999-8999-999999999999", + "actor": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", +} DIGEST_A = "a" * 64 DIGEST_B = "b" * 64 DIGEST_C = "c" * 64 @@ -24,21 +37,21 @@ def values(): return dict( tenant_record_id=TENANT, - intake_reference="candidate_evidence_intake:intake-2026-001", - candidate_profile_reference="candidate_profile:candidate-001", - requisition_reference="requisition:req-2026-001", - job_profile_reference="job_profile:job-001", - job_requirements_reference="job_requirements:requirements-v3", + intake_reference=f"candidate_evidence_intake:{REF['intake']}", + candidate_profile_reference=f"candidate_profile:{REF['candidate']}", + requisition_reference=f"requisition:{REF['requisition']}", + job_profile_reference=f"job_profile:{REF['job']}", + job_requirements_reference=f"job_requirements:{REF['requirements']}", job_requirements_digest=DIGEST_A, - evidence_set_reference="evidence_set:evidence-v2", + evidence_set_reference=f"evidence_set:{REF['evidence']}", evidence_set_digest=DIGEST_B, - source_provenance_reference="source_provenance:manifest-v4", + source_provenance_reference=f"source_provenance:{REF['source']}", source_provenance_digest=DIGEST_C, - handling_policy_reference="handling_policy:recruiting-candidate-v2", + handling_policy_reference=f"handling_policy:{REF['handling']}", handling_policy_digest=DIGEST_D, - retention_policy_reference="retention_policy:recruiting-2026-v1", + retention_policy_reference=f"retention_policy:{REF['retention']}", retention_policy_digest=DIGEST_E, - actor_reference="actor:recruiter-007", + actor_reference=f"actor:{REF['actor']}", evidence_item_count=5, purpose_code="candidate_evidence_intake", reason_code="requisition_candidate_review", @@ -53,42 +66,52 @@ def test_builds_reference_only_deterministic_packet(): assert payload["human_confirmation_required"] is True assert payload["next_action"] == EXPECTED_NEXT_ACTION assert payload["collected_at"].endswith(".456789Z") - assert payload["candidate_profile_reference"] == "candidate_profile:candidate-001" - forbidden = ("candidate_name", "email", "demographic", "assessment_value", "raw_evidence", "model_output") + assert payload["candidate_profile_reference"] == f"candidate_profile:{REF['candidate']}" + forbidden = ( + "candidate_name", + "email", + "demographic", + "assessment_value", + "raw_evidence", + "model_output", + ) assert all(name not in payload for name in forbidden) assert packet.sha256_digest() == sha256(packet.canonical_json().encode("utf-8")).hexdigest() assert packet == CandidateEvidenceIntakePacket(**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"), - ("intake_reference", "wrong:intake"), - ("candidate_profile_reference", "wrong:candidate"), - ("requisition_reference", "wrong:req"), - ("job_profile_reference", "wrong:job"), - ("job_requirements_reference", "wrong:reqs"), - ("evidence_set_reference", "wrong:evidence"), - ("source_provenance_reference", "wrong:source"), - ("handling_policy_reference", "wrong:handling"), - ("retention_policy_reference", "wrong:retention"), - ("actor_reference", "wrong:actor"), - ("job_requirements_digest", "A" * 64), - ("evidence_set_digest", "b" * 63), - ("source_provenance_digest", "C" * 64), - ("handling_policy_digest", "d" * 63), - ("retention_policy_digest", 7), - ("purpose_code", "wrong_purpose"), - ("purpose_code", "bad"), - ("reason_code", "Bad Reason"), - ("reason_code", "a_" + "b" * 64), - ("collected_at", datetime(2026, 8, 19, 1, 2, 3)), - ("human_confirmation_required", False), - ("human_confirmation_required", 1), - ("review_state", "approved"), - ("next_action", "Skip human review"), -]) +@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"), + ("intake_reference", "wrong:intake"), + ("candidate_profile_reference", "wrong:candidate"), + ("requisition_reference", "wrong:req"), + ("job_profile_reference", "wrong:job"), + ("job_requirements_reference", "wrong:reqs"), + ("evidence_set_reference", "wrong:evidence"), + ("source_provenance_reference", "wrong:source"), + ("handling_policy_reference", "wrong:handling"), + ("retention_policy_reference", "wrong:retention"), + ("actor_reference", "wrong:actor"), + ("job_requirements_digest", "A" * 64), + ("evidence_set_digest", "b" * 63), + ("source_provenance_digest", "C" * 64), + ("handling_policy_digest", "d" * 63), + ("retention_policy_digest", 7), + ("purpose_code", "wrong_purpose"), + ("purpose_code", "bad"), + ("reason_code", "Bad Reason"), + ("reason_code", "a_" + "b" * 64), + ("collected_at", datetime(2026, 8, 19, 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): data = values() data[field] = bad @@ -96,6 +119,42 @@ def test_rejects_invalid_scalar_contract(field, bad): CandidateEvidenceIntakePacket(**data) +@pytest.mark.parametrize( + ("field", "prefix"), + [ + ("intake_reference", "candidate_evidence_intake"), + ("candidate_profile_reference", "candidate_profile"), + ("requisition_reference", "requisition"), + ("job_profile_reference", "job_profile"), + ("job_requirements_reference", "job_requirements"), + ("evidence_set_reference", "evidence_set"), + ("source_provenance_reference", "source_provenance"), + ("handling_policy_reference", "handling_policy"), + ("retention_policy_reference", "retention_policy"), + ("actor_reference", "actor"), + ], +) +def test_reference_suffixes_are_opaque_canonical_operational_uuids(field, prefix): + packet = CandidateEvidenceIntakePacket(**values()) + for suffix in ( + "Jane-Doe", + "00000000-0000-0000-0000-000000000000", + "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + ): + with pytest.raises(ValueError): + replace(packet, **{field: f"{prefix}:{suffix}"}) + + +def test_repr_redacts_candidate_correlation_and_evidence(): + packet = CandidateEvidenceIntakePacket(**values()) + rendered = repr(packet) + assert rendered == "CandidateEvidenceIntakePacket()" + assert packet.tenant_record_id not in rendered + assert packet.candidate_profile_reference not in rendered + assert packet.actor_reference not in rendered + assert packet.evidence_set_digest not in rendered + + @pytest.mark.parametrize("count", [True, 0, 101, 1.0]) def test_rejects_invalid_evidence_item_count(count): data = values() From 71370f4d00d052e9d640ee04de4c9c08639766d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:15:19 -0700 Subject: [PATCH 08/48] fix: enforce opaque candidate evidence references --- .../src/orgmetra_candidate_evidence/packet.py | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index 528ba0c0b..f15e16e7c 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -2,8 +2,8 @@ The contract binds candidate evidence intake to an authoritative candidate, requisition, Job, job requirements, evidence-set identity, handling/retention policy, and accountable -actor. It carries no raw candidate evidence values; the opaque candidate reference remains -sensitive correlating metadata. +actor. It carries no raw candidate evidence values; UUID-backed opaque references remain +sensitive correlating metadata and are redacted from the ordinary object representation. """ from __future__ import annotations @@ -16,7 +16,9 @@ _CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") _DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") -_REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) _PURPOSE_CODE = "candidate_evidence_intake" _REVIEW_STATE = "requires_human_review" _NEXT_ACTION = ( @@ -42,14 +44,22 @@ def _validate_code(value: str, field_name: str) -> None: def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require a bounded namespaced opaque reference with the expected prefix.""" + """Require the expected namespace plus a canonical operational UUID suffix.""" + message = f"{field_name} must be an opaque {prefix}: reference" if ( not isinstance(value, str) or len(value) > 160 or not _REFERENCE_PATTERN.fullmatch(value) or not value.startswith(f"{prefix}:") ): - raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + raise ValueError(message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(message) from exc + if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + raise ValueError(message) def _validate_digest(value: str, field_name: str) -> None: @@ -65,7 +75,7 @@ def _canonical_timestamp(value: datetime) -> str: return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True, slots=True, repr=False) class CandidateEvidenceIntakePacket: """Immutable reference-only candidate-evidence intake awaiting human review.""" @@ -93,22 +103,46 @@ class CandidateEvidenceIntakePacket: review_state: str = _REVIEW_STATE next_action: str = _NEXT_ACTION + def __repr__(self) -> str: + """Return a representation that never emits candidate correlation evidence.""" + return "CandidateEvidenceIntakePacket()" + def __post_init__(self) -> None: """Fail closed when direct construction drifts from the governed contract.""" _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") _validate_reference(self.intake_reference, "candidate_evidence_intake", "intake_reference") - _validate_reference(self.candidate_profile_reference, "candidate_profile", "candidate_profile_reference") + _validate_reference( + self.candidate_profile_reference, + "candidate_profile", + "candidate_profile_reference", + ) _validate_reference(self.requisition_reference, "requisition", "requisition_reference") _validate_reference(self.job_profile_reference, "job_profile", "job_profile_reference") - _validate_reference(self.job_requirements_reference, "job_requirements", "job_requirements_reference") + _validate_reference( + self.job_requirements_reference, + "job_requirements", + "job_requirements_reference", + ) _validate_digest(self.job_requirements_digest, "job_requirements_digest") _validate_reference(self.evidence_set_reference, "evidence_set", "evidence_set_reference") _validate_digest(self.evidence_set_digest, "evidence_set_digest") - _validate_reference(self.source_provenance_reference, "source_provenance", "source_provenance_reference") + _validate_reference( + self.source_provenance_reference, + "source_provenance", + "source_provenance_reference", + ) _validate_digest(self.source_provenance_digest, "source_provenance_digest") - _validate_reference(self.handling_policy_reference, "handling_policy", "handling_policy_reference") + _validate_reference( + self.handling_policy_reference, + "handling_policy", + "handling_policy_reference", + ) _validate_digest(self.handling_policy_digest, "handling_policy_digest") - _validate_reference(self.retention_policy_reference, "retention_policy", "retention_policy_reference") + _validate_reference( + self.retention_policy_reference, + "retention_policy", + "retention_policy_reference", + ) _validate_digest(self.retention_policy_digest, "retention_policy_digest") _validate_reference(self.actor_reference, "actor", "actor_reference") if type(self.evidence_item_count) is not int or not 1 <= self.evidence_item_count <= 100: From 2a61de8459d5c8f9544bbe001474fec1b9599967 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:16:03 -0700 Subject: [PATCH 09/48] docs: harden candidate evidence references --- packages/candidate-evidence/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index 024a35b3e..df82439fb 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -2,9 +2,9 @@ This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. -`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, evidence-item count, and collection time. Every trust-bearing artifact is represented by an opaque reference and, where content drift matters, an independent lowercase SHA-256 digest. +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUID suffix; content-bearing evidence also carries an independent lowercase SHA-256 digest. -The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. An opaque candidate reference remains sensitive correlating metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. +The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable or value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and the next action tells the operator to verify job relevance, source provenance, retention handling, and completeness before sealing the evidence set for accountable human review. From 6fb4cb5ba61ff65bc80d360b15d48b4046022b6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:16:17 -0700 Subject: [PATCH 10/48] docs: record opaque candidate evidence identity --- ...0015-governed-candidate-evidence-intake.md | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md index 7815e5453..4737a19a6 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -7,35 +7,37 @@ Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. -Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. At the same time, a bare file reference is insufficient: an accountable reviewer needs evidence that the intake was tied to the right candidate, requisition, authoritative Job and job requirements, source-provenance manifest, handling policy, retention policy, actor, purpose, and exact evidence-set version. +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. ## Decision Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: -- canonical tenant identity and one opaque intake reference; -- one candidate profile, requisition, and authoritative Job reference; -- exact job-requirements reference plus SHA-256 digest; -- exact evidence-set and source-provenance references plus independent SHA-256 digests; -- exact handling-policy and retention-policy references plus independent SHA-256 digests; +- canonical tenant identity and one UUID-backed opaque intake reference; +- UUID-backed candidate-profile, requisition, authoritative Job, job-requirements, evidence-set, provenance, handling-policy, retention-policy and actor references; +- independent SHA-256 digests where content drift matters; - one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; -- mandatory human confirmation, immutable `requires_human_review` state, and actionable next-step copy. +- mandatory human confirmation, immutable `requires_human_review` state, actionable next-step copy, and a fully redacted ordinary object representation. -The governance packet is reference-only. It contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. Direct construction and builder construction share the same fail-closed validation. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, or final approval. +Every reference must use the expected namespace plus a canonical non-sentinel UUID suffix. Human-readable/value-bearing suffixes are rejected fail-closed. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. + +Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, policy enforcement, or final approval. ## Consequences ### Positive - Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. -- Source provenance, purpose-bound handling, and retention become explicit versioned evidence rather than prose-only assumptions. -- Downstream evidence sealing and human selection review can reject drift by reference/digest. +- Reference fields cannot quietly become a human-readable PII/value channel. +- Source provenance, purpose-bound handling, and retention are explicit versioned evidence rather than prose-only assumptions. +- Ordinary logging/assertion formatting does not emit candidate correlation or evidence digests. - The package remains standalone and MSA-friendly and performs no cross-service application-table SQL. ### Costs and constraints +- Hosts must use durable UUID-backed public identifiers rather than convenient semantic slugs inside governance references. - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. -- Opaque candidate references are still sensitive correlating metadata and require least-privilege handling. +- UUID-backed candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. - This ADR remains proposed until its exact PR head merges into protected `develop`. From 16c1680bb0bd05bb954c212ab5dc5e26207decd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:16:25 -0700 Subject: [PATCH 11/48] docs: trace candidate reference hardening --- docs/traceability/candidate-evidence-intake.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index 841b5b6f5..cb908b52d 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -6,12 +6,14 @@ | Requirement | Evidence | |---|---| -| Correlate candidate evidence to the correct recruiting context | Canonical tenant, candidate-profile, requisition and Job references | +| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUID-backed candidate-profile, requisition and Job references | +| Prevent reference fields from becoming a covert PII/value channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUID suffix; human-readable/value-bearing suffixes are rejected | reference-suffix regressions | | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | | Preserve accountable collection context | Actor reference, fixed purpose, bounded reason, exact evidence-item count and precision-preserving UTC time | | Prevent packet-as-decision misuse | Exact boolean human confirmation, immutable `requires_human_review` state and fixed next action | +| Prevent ordinary logs/assertions from leaking candidate correlation | generated dataclass repr disabled; `repr(packet)` is `CandidateEvidenceIntakePacket()` | repr-redaction regression | | Preserve deterministic audit correlation | Canonical JSON + independently testable SHA-256 digest | | Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | | Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | From 6336c29a3586dced07316adeeffaba97fbb33867 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:16:37 -0700 Subject: [PATCH 12/48] docs: note candidate reference hardening --- packages/candidate-evidence/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index 98e8f747c..694751087 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. -- Bind evidence, source provenance, handling and retention artifacts by opaque reference and SHA-256 digest. +- Bind evidence, source provenance, handling and retention artifacts by UUID-backed opaque reference and SHA-256 digest. +- Reject human-readable/value-bearing reference suffixes and sentinel/noncanonical UUIDs so governance references cannot become a covert candidate-data channel. +- Redact the ordinary packet representation so candidate correlation and evidence digests are not emitted through routine logs/assertion failures. - Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. - Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. From 57f2452b74f6cb39f43d4befc3a2e823bb345a47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 21:25:36 -0700 Subject: [PATCH 13/48] test: require complete candidate evidence docstrings --- .../tests/test_docstrings.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 packages/candidate-evidence/tests/test_docstrings.py diff --git a/packages/candidate-evidence/tests/test_docstrings.py b/packages/candidate-evidence/tests/test_docstrings.py new file mode 100644 index 000000000..64d78c0ec --- /dev/null +++ b/packages/candidate-evidence/tests/test_docstrings.py @@ -0,0 +1,31 @@ +"""Executable documentation-completeness contract for candidate evidence intake.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = PACKAGE_ROOT / "src" / "orgmetra_candidate_evidence" +TEST_ROOT = PACKAGE_ROOT / "tests" + + +def _python_files() -> tuple[Path, ...]: + """Return every owned Python source and test file in deterministic order.""" + return tuple(sorted((*SOURCE_ROOT.glob("*.py"), *TEST_ROOT.glob("*.py")))) + + +def test_owned_python_modules_and_callables_are_documented() -> None: + """Require beginner-readable docstrings on all owned modules, classes, and callables.""" + missing: list[str] = [] + for path in _python_files(): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + if ast.get_docstring(tree, clean=False) is None: + missing.append(f"{path.relative_to(PACKAGE_ROOT)}:") + for node in ast.walk(tree): + if not isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if ast.get_docstring(node, clean=False) is None: + missing.append(f"{path.relative_to(PACKAGE_ROOT)}:{node.lineno}:{node.name}") + assert not missing, "Missing owned Python docstrings: " + ", ".join(missing) From bd443ca0bb8e4a99af6e9d3fb3bb93affb12260c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 21:26:05 -0700 Subject: [PATCH 14/48] fix: make candidate evidence tests beginner-readable --- .../candidate-evidence/tests/test_packet.py | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index f468def7a..c1367ca90 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -1,3 +1,5 @@ +"""Regression coverage for governed candidate-evidence intake packets.""" + from dataclasses import replace from datetime import datetime, timedelta, timezone, tzinfo from hashlib import sha256 @@ -34,7 +36,8 @@ ) -def values(): +def values() -> dict[str, object]: + """Return one valid candidate-evidence packet input mapping for focused overrides.""" return dict( tenant_record_id=TENANT, intake_reference=f"candidate_evidence_intake:{REF['intake']}", @@ -59,7 +62,8 @@ def values(): ) -def test_builds_reference_only_deterministic_packet(): +def test_builds_reference_only_deterministic_packet() -> None: + """Build deterministic reference-only evidence without candidate value duplication.""" packet = build_candidate_evidence_intake_packet(**values()) payload = json.loads(packet.canonical_json()) assert payload["review_state"] == "requires_human_review" @@ -112,7 +116,8 @@ def test_builds_reference_only_deterministic_packet(): ("next_action", "Skip human review"), ], ) -def test_rejects_invalid_scalar_contract(field, bad): +def test_rejects_invalid_scalar_contract(field: str, bad: object) -> None: + """Reject malformed scalar governance metadata and attempts to bypass review state.""" data = values() data[field] = bad with pytest.raises((ValueError, TypeError)): @@ -134,7 +139,10 @@ def test_rejects_invalid_scalar_contract(field, bad): ("actor_reference", "actor"), ], ) -def test_reference_suffixes_are_opaque_canonical_operational_uuids(field, prefix): +def test_reference_suffixes_are_opaque_canonical_operational_uuids( + field: str, prefix: str +) -> None: + """Reject semantic, sentinel, and noncanonical values in opaque reference suffixes.""" packet = CandidateEvidenceIntakePacket(**values()) for suffix in ( "Jane-Doe", @@ -145,7 +153,8 @@ def test_reference_suffixes_are_opaque_canonical_operational_uuids(field, prefix replace(packet, **{field: f"{prefix}:{suffix}"}) -def test_repr_redacts_candidate_correlation_and_evidence(): +def test_repr_redacts_candidate_correlation_and_evidence() -> None: + """Keep candidate correlation, actor identity, and evidence digests out of repr output.""" packet = CandidateEvidenceIntakePacket(**values()) rendered = repr(packet) assert rendered == "CandidateEvidenceIntakePacket()" @@ -156,7 +165,8 @@ def test_repr_redacts_candidate_correlation_and_evidence(): @pytest.mark.parametrize("count", [True, 0, 101, 1.0]) -def test_rejects_invalid_evidence_item_count(count): +def test_rejects_invalid_evidence_item_count(count: object) -> None: + """Require a bounded true integer count rather than booleans or numeric lookalikes.""" data = values() data["evidence_item_count"] = count with pytest.raises(ValueError, match="evidence_item_count"): @@ -164,21 +174,27 @@ def test_rejects_invalid_evidence_item_count(count): class UnknownOffset(tzinfo): - def utcoffset(self, dt): + """Timezone fixture whose UTC offset cannot be resolved.""" + + def utcoffset(self, dt: datetime | None) -> None: + """Return an unknown offset to exercise fail-closed timestamp validation.""" return None - def dst(self, dt): + def dst(self, dt: datetime | None) -> None: + """Return no daylight-saving offset for the unknown-offset fixture.""" return None -def test_rejects_timezone_with_unknown_offset(): +def test_rejects_timezone_with_unknown_offset() -> None: + """Reject datetime values whose timezone object cannot resolve an absolute instant.""" data = values() data["collected_at"] = datetime(2026, 8, 19, tzinfo=UnknownOffset()) with pytest.raises(ValueError, match="timezone-aware"): CandidateEvidenceIntakePacket(**data) -def test_canonicalizes_non_utc_offset_and_preserves_fractional_precision(): +def test_canonicalizes_non_utc_offset_and_preserves_fractional_precision() -> None: + """Normalize an aware local instant to UTC without dropping microsecond evidence.""" data = values() data["collected_at"] = datetime( 2026, 8, 19, 10, 2, 3, 456789, tzinfo=timezone(timedelta(hours=9)) @@ -187,16 +203,20 @@ def test_canonicalizes_non_utc_offset_and_preserves_fractional_precision(): assert payload["collected_at"] == "2026-08-19T01:02:03.456789Z" -def test_distinct_subsecond_instants_produce_distinct_evidence(): +def test_distinct_subsecond_instants_produce_distinct_evidence() -> None: + """Preserve digest separation for valid evidence timestamps one microsecond apart.""" first = CandidateEvidenceIntakePacket(**values()) data = values() - data["collected_at"] = data["collected_at"].replace(microsecond=456790) + collected_at = data["collected_at"] + assert isinstance(collected_at, datetime) + data["collected_at"] = collected_at.replace(microsecond=456790) second = CandidateEvidenceIntakePacket(**data) assert first.canonical_json() != second.canonical_json() assert first.sha256_digest() != second.sha256_digest() -def test_direct_replace_is_revalidated(): +def test_direct_replace_is_revalidated() -> None: + """Re-run invariant validation when an immutable packet is copied with new fields.""" packet = CandidateEvidenceIntakePacket(**values()) with pytest.raises(ValueError, match="retention_policy_digest"): replace(packet, retention_policy_digest="not-a-digest") From 2a22cb4e456ae6a67fa23f94868b2066e10a0752 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:22:29 -0700 Subject: [PATCH 15/48] test: close candidate evidence reason vocabulary --- .../candidate-evidence/tests/test_packet.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index c1367ca90..f4a363c8e 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -220,3 +220,22 @@ def test_direct_replace_is_revalidated() -> None: packet = CandidateEvidenceIntakePacket(**values()) with pytest.raises(ValueError, match="retention_policy_digest"): replace(packet, retention_policy_digest="not-a-digest") + + +@pytest.mark.parametrize( + "reason_code", + ["jane_doe", "salary_120000", "race_gender_review", "candidate_alice_smith"], +) +def test_reason_code_rejects_personal_or_value_bearing_free_form_codes(reason_code: str) -> None: + """Prevent nominal governance reason metadata from becoming a candidate-data channel.""" + data = values() + data["reason_code"] = reason_code + with pytest.raises(ValueError): + CandidateEvidenceIntakePacket(**data) + + +def test_reason_code_replace_path_is_revalidated() -> None: + """Reject value-bearing reason metadata when copying an immutable intake packet.""" + packet = CandidateEvidenceIntakePacket(**values()) + with pytest.raises(ValueError): + replace(packet, reason_code="salary_120000") From d166535a00dad4e14ed416623dee6ea9b50e08ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:23:02 -0700 Subject: [PATCH 16/48] fix: close candidate evidence reason vocabulary --- .../src/orgmetra_candidate_evidence/packet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index f15e16e7c..8923d7880 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -21,6 +21,7 @@ ) _PURPOSE_CODE = "candidate_evidence_intake" _REVIEW_STATE = "requires_human_review" +_ALLOWED_REASON_CODES = frozenset({"requisition_candidate_review"}) _NEXT_ACTION = ( "Verify job relevance, source provenance, permitted handling, retention, and evidence " "completeness; then request authoritative evidence sealing and accountable human review." @@ -151,6 +152,8 @@ def __post_init__(self) -> None: if self.purpose_code != _PURPOSE_CODE: raise ValueError("purpose_code must remain candidate_evidence_intake") _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 candidate-evidence reason") _canonical_timestamp(self.collected_at) if self.human_confirmation_required is not True: raise ValueError("human confirmation is mandatory before sealing candidate evidence") From 81800afed330ad967f13ec3a67c74b5e883451d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:15:36 -0700 Subject: [PATCH 17/48] test: require candidate evidence versioning --- .../tests/test_evidence_version.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/candidate-evidence/tests/test_evidence_version.py diff --git a/packages/candidate-evidence/tests/test_evidence_version.py b/packages/candidate-evidence/tests/test_evidence_version.py new file mode 100644 index 000000000..a95e82ce5 --- /dev/null +++ b/packages/candidate-evidence/tests/test_evidence_version.py @@ -0,0 +1,39 @@ +"""Regression coverage for explicit high-impact candidate-evidence versioning.""" + +from dataclasses import replace +import json + +import pytest + +from orgmetra_candidate_evidence import CandidateEvidenceIntakePacket + +from test_packet import values + + +def _packet(evidence_version: int = 1) -> CandidateEvidenceIntakePacket: + """Build one valid packet while varying only its immutable evidence version.""" + data = values() + data["evidence_version"] = evidence_version + return CandidateEvidenceIntakePacket(**data) + + +def test_evidence_version_is_canonical_evidence() -> None: + """Bind the explicit evidence version into canonical JSON and its SHA-256 digest.""" + first = _packet(1) + second = _packet(2) + assert json.loads(first.canonical_json())["evidence_version"] == 1 + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +@pytest.mark.parametrize("evidence_version", [True, False, 0, -1, 2_147_483_648, "1", 1.0]) +def test_rejects_noncanonical_evidence_versions(evidence_version: object) -> None: + """Reject booleans, non-integers, non-positive values, and signed-int32 overflow.""" + with pytest.raises(ValueError, match="evidence_version"): + _packet(evidence_version) # type: ignore[arg-type] + + +def test_replace_cannot_bypass_evidence_version_validation() -> None: + """Revalidate explicit evidence-version bounds when immutable packets are copied.""" + with pytest.raises(ValueError, match="evidence_version"): + replace(_packet(), evidence_version=0) From 2c971015dbdc6152ba1335131b9df2dec874418b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:16:12 -0700 Subject: [PATCH 18/48] fix: version candidate evidence packets --- .../src/orgmetra_candidate_evidence/packet.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index 8923d7880..6f01bb86f 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -100,6 +100,7 @@ class CandidateEvidenceIntakePacket: purpose_code: str reason_code: str collected_at: datetime + evidence_version: int = 1 human_confirmation_required: bool = True review_state: str = _REVIEW_STATE next_action: str = _NEXT_ACTION @@ -155,6 +156,8 @@ def __post_init__(self) -> None: if self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must use a reviewed non-sensitive candidate-evidence reason") _canonical_timestamp(self.collected_at) + if type(self.evidence_version) is not int or not 1 <= self.evidence_version <= 2_147_483_647: + 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 before sealing candidate evidence") if self.review_state != _REVIEW_STATE: @@ -171,6 +174,7 @@ def canonical_json(self) -> str: "evidence_item_count": self.evidence_item_count, "evidence_set_digest": self.evidence_set_digest, "evidence_set_reference": self.evidence_set_reference, + "evidence_version": self.evidence_version, "handling_policy_digest": self.handling_policy_digest, "handling_policy_reference": self.handling_policy_reference, "human_confirmation_required": self.human_confirmation_required, @@ -218,6 +222,7 @@ def build_candidate_evidence_intake_packet( purpose_code: str, reason_code: str, collected_at: datetime, + evidence_version: int = 1, ) -> CandidateEvidenceIntakePacket: """Build a reference-only candidate-evidence packet pending accountable review.""" return CandidateEvidenceIntakePacket( @@ -241,4 +246,5 @@ def build_candidate_evidence_intake_packet( purpose_code=purpose_code, reason_code=reason_code, collected_at=collected_at, + evidence_version=evidence_version, ) From 790ab9f2721c16412b749f78c0d39e002befb06f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:16:30 -0700 Subject: [PATCH 19/48] docs: bind candidate evidence version --- packages/candidate-evidence/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index df82439fb..f13d776a9 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -2,7 +2,7 @@ This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. -`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUID suffix; content-bearing evidence also carries an independent lowercase SHA-256 digest. +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUID suffix; content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable or value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. From dabcebfcc6ff18c095f271628a4a3860932d7d61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:16:40 -0700 Subject: [PATCH 20/48] docs: trace candidate evidence versioning --- docs/traceability/candidate-evidence-intake.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index cb908b52d..883ede5a8 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -11,6 +11,7 @@ | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | +| Version actor/purpose/reason evidence explicitly | `evidence_version` is a true positive integer through signed-int32 max and participates in canonical JSON/SHA-256 | `test_evidence_version.py` proves presence, digest separation, bounds, and `dataclasses.replace(...)` revalidation | | Preserve accountable collection context | Actor reference, fixed purpose, bounded reason, exact evidence-item count and precision-preserving UTC time | | Prevent packet-as-decision misuse | Exact boolean human confirmation, immutable `requires_human_review` state and fixed next action | | Prevent ordinary logs/assertions from leaking candidate correlation | generated dataclass repr disabled; `repr(packet)` is `CandidateEvidenceIntakePacket()` | repr-redaction regression | From a74ab4768c4710461bd0fefc85d62b78a162418b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:24:05 -0700 Subject: [PATCH 21/48] test(candidate-evidence): require tenant-scoped reference resolution --- .../tests/test_tenant_scope.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/candidate-evidence/tests/test_tenant_scope.py diff --git a/packages/candidate-evidence/tests/test_tenant_scope.py b/packages/candidate-evidence/tests/test_tenant_scope.py new file mode 100644 index 000000000..62f30bddb --- /dev/null +++ b/packages/candidate-evidence/tests/test_tenant_scope.py @@ -0,0 +1,45 @@ +"""Tenant-scope regressions for governed candidate-evidence intake.""" + +from datetime import datetime, timezone + +from orgmetra_candidate_evidence import build_candidate_evidence_intake_packet + + +def _packet(): + """Build one valid candidate-evidence packet for boundary assertions.""" + return build_candidate_evidence_intake_packet( + tenant_record_id="12345678-1234-4234-8234-123456789abc", + intake_reference="candidate_evidence_intake:11111111-1111-4111-8111-111111111111", + candidate_profile_reference="candidate_profile:22222222-2222-4222-8222-222222222222", + requisition_reference="requisition:33333333-3333-4333-8333-333333333333", + job_profile_reference="job_profile:44444444-4444-4444-8444-444444444444", + job_requirements_reference="job_requirements:55555555-5555-4555-8555-555555555555", + job_requirements_digest="a" * 64, + evidence_set_reference="evidence_set:66666666-6666-4666-8666-666666666666", + evidence_set_digest="b" * 64, + source_provenance_reference="source_provenance:77777777-7777-4777-8777-777777777777", + source_provenance_digest="c" * 64, + handling_policy_reference="handling_policy:88888888-8888-4888-8888-888888888888", + handling_policy_digest="d" * 64, + retention_policy_reference="retention_policy:99999999-9999-4999-8999-999999999999", + retention_policy_digest="e" * 64, + actor_reference="actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + evidence_item_count=5, + purpose_code="candidate_evidence_intake", + reason_code="requisition_candidate_review", + collected_at=datetime(2026, 8, 19, 1, 2, 3, 456789, tzinfo=timezone.utc), + ) + + +def test_sealing_requires_every_reference_to_resolve_in_exact_tenant() -> None: + """Prevent cross-tenant evidence mixing behind syntactically valid opaque references.""" + action = _packet().next_action + tenant_clause = "re-resolve every packet reference within tenant_record_id" + correlation_clause = "verify candidate, requisition, and Job correlation" + provenance_clause = "verify job relevance, source provenance" + sealing_clause = "authoritative evidence sealing" + + assert tenant_clause in action + assert action.index(tenant_clause) < action.index(correlation_clause) + assert action.index(correlation_clause) < action.index(provenance_clause) + assert action.index(provenance_clause) < action.index(sealing_clause) From 14340873bd7eb465b7dcf3ad0f20bf2daff0a141 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:24:40 -0700 Subject: [PATCH 22/48] fix(candidate-evidence): bind intake references to tenant scope --- .../src/orgmetra_candidate_evidence/packet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index 6f01bb86f..71b6e4710 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -23,8 +23,10 @@ _REVIEW_STATE = "requires_human_review" _ALLOWED_REASON_CODES = frozenset({"requisition_candidate_review"}) _NEXT_ACTION = ( - "Verify job relevance, source provenance, permitted handling, retention, and evidence " - "completeness; then request authoritative evidence sealing and accountable human review." + "Re-resolve every packet reference within tenant_record_id through its authoritative " + "boundary; verify candidate, requisition, and Job correlation; then verify job relevance, " + "source provenance, permitted handling, retention, and evidence completeness before " + "requesting authoritative evidence sealing and accountable human review." ) From d8d81caaaa403c8e5dea61082277a4f0b76e76c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:25:13 -0700 Subject: [PATCH 23/48] test(candidate-evidence): align governed next action --- packages/candidate-evidence/tests/test_packet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index f4a363c8e..a20bdfda6 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -31,8 +31,10 @@ DIGEST_D = "d" * 64 DIGEST_E = "e" * 64 EXPECTED_NEXT_ACTION = ( - "Verify job relevance, source provenance, permitted handling, retention, and evidence " - "completeness; then request authoritative evidence sealing and accountable human review." + "Re-resolve every packet reference within tenant_record_id through its authoritative " + "boundary; verify candidate, requisition, and Job correlation; then verify job relevance, " + "source provenance, permitted handling, retention, and evidence completeness before " + "requesting authoritative evidence sealing and accountable human review." ) From 18c7f0cf5bfb89b7a7bb6cd4a05b84a0af2e9fb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:25:32 -0700 Subject: [PATCH 24/48] docs(candidate-evidence): align tenant evidence boundary --- packages/candidate-evidence/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index f13d776a9..a1a399f5d 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -6,6 +6,6 @@ This package defines a transport-neutral, reference-only packet for admitting ca The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable or value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. -The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and the next action tells the operator to verify job relevance, source provenance, retention handling, and completeness before sealing the evidence set for accountable human review. +The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. This prevents a syntactically valid reference from another tenant from being mixed into an otherwise valid intake envelope. -Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. +Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. From 657806349f3e6004c8300a25f4578eb49a0611ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:25:52 -0700 Subject: [PATCH 25/48] docs(candidate-evidence): require exact-tenant resolution --- docs/adr/0015-governed-candidate-evidence-intake.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md index 4737a19a6..8420671e1 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -7,7 +7,7 @@ Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. -Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. ## Decision @@ -19,7 +19,7 @@ Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: - one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; - mandatory human confirmation, immutable `requires_human_review` state, actionable next-step copy, and a fully redacted ordinary object representation. -Every reference must use the expected namespace plus a canonical non-sentinel UUID suffix. Human-readable/value-bearing suffixes are rejected fail-closed. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. +Every reference must use the expected namespace plus a canonical non-sentinel UUID suffix. Human-readable/value-bearing suffixes are rejected fail-closed. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUID syntax is only an early syntactic guard; it is not tenant authority. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, policy enforcement, or final approval. @@ -29,13 +29,14 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab - Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. - Reference fields cannot quietly become a human-readable PII/value channel. +- Cross-tenant evidence mixing is fail-closed at the host sealing/review boundary because every packet reference must resolve within the packet tenant. - Source provenance, purpose-bound handling, and retention are explicit versioned evidence rather than prose-only assumptions. - Ordinary logging/assertion formatting does not emit candidate correlation or evidence digests. - The package remains standalone and MSA-friendly and performs no cross-service application-table SQL. ### Costs and constraints -- Hosts must use durable UUID-backed public identifiers rather than convenient semantic slugs inside governance references. +- Hosts must use durable UUID-backed public identifiers rather than convenient semantic slugs inside governance references and must authoritatively resolve them in the exact tenant before use. - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. - UUID-backed candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. From 61179021f94dcd1a134fa5c9b9c285e74020bce5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:26:10 -0700 Subject: [PATCH 26/48] docs(candidate-evidence): trace tenant-resolution regression --- docs/traceability/candidate-evidence-intake.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index 883ede5a8..d4c9814e4 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -6,17 +6,18 @@ | Requirement | Evidence | |---|---| -| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUID-backed candidate-profile, requisition and Job references | -| Prevent reference fields from becoming a covert PII/value channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUID suffix; human-readable/value-bearing suffixes are rejected | reference-suffix regressions | +| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUID-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | +| Prevent cross-tenant evidence mixing | `test_tenant_scope.py` requires tenant-scoped resolution of every packet reference before correlation/provenance checks and authoritative sealing; UUID syntax alone is not tenant authority | +| Prevent reference fields from becoming a covert PII/value channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUID suffix; human-readable/value-bearing suffixes are rejected | | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | -| Version actor/purpose/reason evidence explicitly | `evidence_version` is a true positive integer through signed-int32 max and participates in canonical JSON/SHA-256 | `test_evidence_version.py` proves presence, digest separation, bounds, and `dataclasses.replace(...)` revalidation | +| Version actor/purpose/reason evidence explicitly | `evidence_version` is a true positive integer through signed-int32 max and participates in canonical JSON/SHA-256; `test_evidence_version.py` proves presence, digest separation, bounds, and `dataclasses.replace(...)` revalidation | | Preserve accountable collection context | Actor reference, fixed purpose, bounded reason, exact evidence-item count and precision-preserving UTC time | | Prevent packet-as-decision misuse | Exact boolean human confirmation, immutable `requires_human_review` state and fixed next action | -| Prevent ordinary logs/assertions from leaking candidate correlation | generated dataclass repr disabled; `repr(packet)` is `CandidateEvidenceIntakePacket()` | repr-redaction regression | +| Prevent ordinary logs/assertions from leaking candidate correlation | generated dataclass repr disabled; `repr(packet)` is `CandidateEvidenceIntakePacket()`; repr-redaction regression | | Preserve deterministic audit correlation | Canonical JSON + independently testable SHA-256 digest | | Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | | Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | -The packet is correlation evidence, not proof of evidence truth, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. Those claims require their own owner-bound evidence. +The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. Those claims require their own owner-bound evidence. From 4096d24d0724ddfb84e731788af148a30a57619a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:26:26 -0700 Subject: [PATCH 27/48] docs(candidate-evidence): record tenant binding repair --- packages/candidate-evidence/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index 694751087..16f70edf2 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -5,6 +5,7 @@ - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. - Bind evidence, source provenance, handling and retention artifacts by UUID-backed opaque reference and SHA-256 digest. - Reject human-readable/value-bearing reference suffixes and sentinel/noncanonical UUIDs so governance references cannot become a covert candidate-data channel. +- Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUIDs. - Redact the ordinary packet representation so candidate correlation and evidence digests are not emitted through routine logs/assertion failures. - Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. - Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. From 61b416a656aaff540e4907e7be4a05fb2412e17f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:42:18 -0700 Subject: [PATCH 28/48] test(candidate-evidence): align tenant next-action assertion --- packages/candidate-evidence/tests/test_tenant_scope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/candidate-evidence/tests/test_tenant_scope.py b/packages/candidate-evidence/tests/test_tenant_scope.py index 62f30bddb..496dd338a 100644 --- a/packages/candidate-evidence/tests/test_tenant_scope.py +++ b/packages/candidate-evidence/tests/test_tenant_scope.py @@ -34,7 +34,7 @@ def _packet(): def test_sealing_requires_every_reference_to_resolve_in_exact_tenant() -> None: """Prevent cross-tenant evidence mixing behind syntactically valid opaque references.""" action = _packet().next_action - tenant_clause = "re-resolve every packet reference within tenant_record_id" + tenant_clause = "Re-resolve every packet reference within tenant_record_id" correlation_clause = "verify candidate, requisition, and Job correlation" provenance_clause = "verify job relevance, source provenance" sealing_clause = "authoritative evidence sealing" From 0bc62b25363343cd99381ba5f087cc4e4d1df3c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:50:33 -0700 Subject: [PATCH 29/48] test: reject UUIDv1 candidate evidence trust references --- .../tests/test_reference_privacy.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/candidate-evidence/tests/test_reference_privacy.py diff --git a/packages/candidate-evidence/tests/test_reference_privacy.py b/packages/candidate-evidence/tests/test_reference_privacy.py new file mode 100644 index 000000000..6a9fb9f4c --- /dev/null +++ b/packages/candidate-evidence/tests/test_reference_privacy.py @@ -0,0 +1,66 @@ +"""Privacy regressions for opaque candidate-evidence trust references.""" +from dataclasses import replace +from datetime import datetime, timezone + +import pytest + +from orgmetra_candidate_evidence import build_candidate_evidence_intake_packet + +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + + +def valid_kwargs() -> dict[str, object]: + """Return one complete packet whose trust references use opaque UUIDv4 suffixes.""" + return { + "tenant_record_id": "12345678-1234-4234-8234-123456789abc", + "intake_reference": "candidate_evidence_intake:11111111-1111-4111-8111-111111111111", + "candidate_profile_reference": "candidate_profile:22222222-2222-4222-8222-222222222222", + "requisition_reference": "requisition:33333333-3333-4333-8333-333333333333", + "job_profile_reference": "job_profile:44444444-4444-4444-8444-444444444444", + "job_requirements_reference": "job_requirements:55555555-5555-4555-8555-555555555555", + "job_requirements_digest": "a" * 64, + "evidence_set_reference": "evidence_set:66666666-6666-4666-8666-666666666666", + "evidence_set_digest": "b" * 64, + "source_provenance_reference": "source_provenance:77777777-7777-4777-8777-777777777777", + "source_provenance_digest": "c" * 64, + "handling_policy_reference": "handling_policy:88888888-8888-4888-8888-888888888888", + "handling_policy_digest": "d" * 64, + "retention_policy_reference": "retention_policy:99999999-9999-4999-8999-999999999999", + "retention_policy_digest": "e" * 64, + "actor_reference": "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "evidence_item_count": 5, + "purpose_code": "candidate_evidence_intake", + "reason_code": "requisition_candidate_review", + "collected_at": datetime(2026, 8, 19, 1, 2, 3, 456789, tzinfo=timezone.utc), + } + + +@pytest.mark.parametrize( + ("field_name", "prefix"), + [ + ("intake_reference", "candidate_evidence_intake"), + ("candidate_profile_reference", "candidate_profile"), + ("requisition_reference", "requisition"), + ("job_profile_reference", "job_profile"), + ("job_requirements_reference", "job_requirements"), + ("evidence_set_reference", "evidence_set"), + ("source_provenance_reference", "source_provenance"), + ("handling_policy_reference", "handling_policy"), + ("retention_policy_reference", "retention_policy"), + ("actor_reference", "actor"), + ], +) +def test_uuid1_trust_reference_is_rejected_by_builder_and_replace( + field_name: str, + prefix: str, +) -> None: + """UUIDv1 timestamp/node metadata must never enter an opaque trust-reference field.""" + value = f"{prefix}:{UUID1_ID}" + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=field_name): + build_candidate_evidence_intake_packet(**kwargs) + + packet = build_candidate_evidence_intake_packet(**valid_kwargs()) + with pytest.raises(ValueError, match=field_name): + replace(packet, **{field_name: value}) From c483f913e8e799d7a09707b3a72ca8ea32f821f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:51:02 -0700 Subject: [PATCH 30/48] fix: require UUIDv4 candidate evidence trust references --- .../src/orgmetra_candidate_evidence/packet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index 71b6e4710..a8a5ea123 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -47,7 +47,7 @@ def _validate_code(value: str, field_name: str) -> None: def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require the expected namespace plus a canonical operational UUID suffix.""" + """Require the expected namespace plus a canonical opaque UUIDv4 suffix.""" message = f"{field_name} must be an opaque {prefix}: reference" if ( not isinstance(value, str) @@ -61,7 +61,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: parsed = UUID(suffix) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(message) from exc - if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): raise ValueError(message) From 0c60c780b60b1447003adc623bc8215c663e7912 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:51:22 -0700 Subject: [PATCH 31/48] docs: define UUIDv4 candidate evidence references --- packages/candidate-evidence/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index a1a399f5d..00fb874d0 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -2,10 +2,10 @@ This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. -`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUID suffix; content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUIDv4 suffix; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter an otherwise opaque field. Content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. -The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable or value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. +The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable/value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. -The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. This prevents a syntactically valid reference from another tenant from being mixed into an otherwise valid intake envelope. +The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. UUIDv4 syntax constrains reference opacity only; it does not prove tenant ownership, record relationship, or authorization. Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. From 5c959c9e0065de15fde7101588cafe4ab4ffcc61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:51:34 -0700 Subject: [PATCH 32/48] docs: record UUIDv4 candidate evidence decision --- docs/adr/0015-governed-candidate-evidence-intake.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md index 8420671e1..156904e58 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -7,19 +7,19 @@ Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. -Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. UUIDv1 adds a subtler correlation channel because timestamp/node-derived metadata can be embedded in an otherwise canonical UUID. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. ## Decision Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: -- canonical tenant identity and one UUID-backed opaque intake reference; -- UUID-backed candidate-profile, requisition, authoritative Job, job-requirements, evidence-set, provenance, handling-policy, retention-policy and actor references; +- canonical tenant identity and one UUIDv4-backed opaque intake reference; +- UUIDv4-backed candidate-profile, requisition, authoritative Job, job-requirements, evidence-set, provenance, handling-policy, retention-policy and actor references; - independent SHA-256 digests where content drift matters; - one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; - mandatory human confirmation, immutable `requires_human_review` state, actionable next-step copy, and a fully redacted ordinary object representation. -Every reference must use the expected namespace plus a canonical non-sentinel UUID suffix. Human-readable/value-bearing suffixes are rejected fail-closed. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUID syntax is only an early syntactic guard; it is not tenant authority. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. +Every namespaced trust reference must use the expected namespace plus a canonical non-sentinel UUIDv4 suffix. Human-readable/value-bearing suffixes, UUIDv1, and every other UUID version are rejected fail-closed. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUIDv4 syntax is only an early opacity guard; it is not tenant authority or relationship evidence. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, policy enforcement, or final approval. @@ -28,7 +28,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ### Positive - Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. -- Reference fields cannot quietly become a human-readable PII/value channel. +- Reference fields cannot quietly become a human-readable PII/value channel or UUIDv1 timestamp/node correlation channel. - Cross-tenant evidence mixing is fail-closed at the host sealing/review boundary because every packet reference must resolve within the packet tenant. - Source provenance, purpose-bound handling, and retention are explicit versioned evidence rather than prose-only assumptions. - Ordinary logging/assertion formatting does not emit candidate correlation or evidence digests. @@ -36,7 +36,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ### Costs and constraints -- Hosts must use durable UUID-backed public identifiers rather than convenient semantic slugs inside governance references and must authoritatively resolve them in the exact tenant before use. +- Hosts must use durable UUIDv4-backed public identifiers rather than convenient semantic slugs or UUID versions with embedded correlation metadata, and must authoritatively resolve them in the exact tenant before use. - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. - UUID-backed candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. From 2f6e52660fe2dad309fd17709604e88eb69657fe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:51:48 -0700 Subject: [PATCH 33/48] docs: record candidate UUIDv4 privacy hardening --- packages/candidate-evidence/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index 16f70edf2..964fff142 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -3,9 +3,9 @@ ## Unreleased - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. -- Bind evidence, source provenance, handling and retention artifacts by UUID-backed opaque reference and SHA-256 digest. -- Reject human-readable/value-bearing reference suffixes and sentinel/noncanonical UUIDs so governance references cannot become a covert candidate-data channel. -- Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUIDs. +- Bind evidence, source provenance, handling and retention artifacts by UUIDv4-backed opaque reference and SHA-256 digest. +- Reject human-readable/value-bearing reference suffixes, sentinel/noncanonical UUIDs, UUIDv1 timestamp/node correlation metadata, and every other non-v4 UUID version so governance references cannot become a covert candidate-data channel. +- Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUIDv4 syntax. - Redact the ordinary packet representation so candidate correlation and evidence digests are not emitted through routine logs/assertion failures. - Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. - Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. From 66af0d8ef43174827f6feaafb108e5b2241f0732 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:51:57 -0700 Subject: [PATCH 34/48] docs: trace UUIDv4 candidate evidence regression --- docs/traceability/candidate-evidence-intake.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index d4c9814e4..48ccaf06a 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -6,9 +6,9 @@ | Requirement | Evidence | |---|---| -| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUID-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | +| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUIDv4-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | | Prevent cross-tenant evidence mixing | `test_tenant_scope.py` requires tenant-scoped resolution of every packet reference before correlation/provenance checks and authoritative sealing; UUID syntax alone is not tenant authority | -| Prevent reference fields from becoming a covert PII/value channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUID suffix; human-readable/value-bearing suffixes are rejected | +| Prevent reference fields from becoming a covert PII/value/correlation channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUIDv4 suffix; human-readable/value-bearing suffixes, UUIDv1 timestamp/node metadata, and other UUID versions are rejected; `test_reference_privacy.py` covers every trust-reference field through builder and replacement paths | | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | @@ -20,4 +20,4 @@ | Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | | Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | -The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. Those claims require their own owner-bound evidence. +The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. UUIDv4 is an opacity/privacy constraint only; those claims require their own owner-bound evidence. From a806204849ef66ba428c59abc2468e3becda849d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:38:58 -0700 Subject: [PATCH 35/48] test: reject correlating tenant UUIDv1 in candidate intake --- .../tests/test_reference_privacy.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/candidate-evidence/tests/test_reference_privacy.py b/packages/candidate-evidence/tests/test_reference_privacy.py index 6a9fb9f4c..e6ed5da93 100644 --- a/packages/candidate-evidence/tests/test_reference_privacy.py +++ b/packages/candidate-evidence/tests/test_reference_privacy.py @@ -64,3 +64,15 @@ def test_uuid1_trust_reference_is_rejected_by_builder_and_replace( packet = build_candidate_evidence_intake_packet(**valid_kwargs()) with pytest.raises(ValueError, match=field_name): replace(packet, **{field_name: value}) + + +def test_uuid1_tenant_identity_is_rejected_by_builder_and_replace() -> None: + """A public tenant identity must not admit UUIDv1 timestamp/node correlation metadata.""" + kwargs = valid_kwargs() + kwargs["tenant_record_id"] = UUID1_ID + with pytest.raises(ValueError, match="tenant_record_id"): + build_candidate_evidence_intake_packet(**kwargs) + + packet = build_candidate_evidence_intake_packet(**valid_kwargs()) + with pytest.raises(ValueError, match="tenant_record_id"): + replace(packet, tenant_record_id=UUID1_ID) From 379eeeb46f1b3cab58577596d24b74317cfd76c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:39:37 -0700 Subject: [PATCH 36/48] fix: require opaque UUIDv4 tenant identity in candidate intake --- .../src/orgmetra_candidate_evidence/packet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index a8a5ea123..2b52652f5 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -31,13 +31,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical non-sentinel UUID text for a governance identity.""" + """Require canonical UUIDv4 text so a public governance identity stays opaque.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUID") + if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUIDv4") def _validate_code(value: str, field_name: str) -> None: @@ -249,4 +249,4 @@ def build_candidate_evidence_intake_packet( reason_code=reason_code, collected_at=collected_at, evidence_version=evidence_version, - ) + ) \ No newline at end of file From 71f7fa8495b7cdc4fa3bfa54229492451dc45266 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:39:59 -0700 Subject: [PATCH 37/48] docs: bind candidate intake tenant identity to UUIDv4 opacity --- packages/candidate-evidence/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index 00fb874d0..d9d828018 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -2,10 +2,10 @@ This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. -`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. Every trust-bearing reference uses its expected namespace plus a canonical non-sentinel UUIDv4 suffix; UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter an otherwise opaque field. Content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. The public `tenant_record_id` and every trust-bearing reference use canonical non-sentinel UUIDv4 identity; namespaced references additionally require their expected namespace. UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter otherwise opaque public identity fields. Content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. -The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable/value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed candidate correlation remains sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. +The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable/value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed tenant and candidate correlation remain sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. -The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. UUIDv4 syntax constrains reference opacity only; it does not prove tenant ownership, record relationship, or authorization. +The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. UUIDv4 syntax constrains public-identity opacity only; it does not prove tenant ownership, record relationship, or authorization. -Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. +Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. \ No newline at end of file From dc162ce6aed5ef41a8e09de89d24de58e2b1fb8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:40:15 -0700 Subject: [PATCH 38/48] docs: require opaque UUIDv4 tenant identity for candidate intake --- docs/adr/0015-governed-candidate-evidence-intake.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md index 156904e58..759160628 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -7,19 +7,19 @@ Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. -Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. UUIDv1 adds a subtler correlation channel because timestamp/node-derived metadata can be embedded in an otherwise canonical UUID. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. UUIDv1 adds a subtler correlation channel because timestamp/node-derived metadata can be embedded in an otherwise canonical UUID. That applies to the public tenant identity as well as namespaced trust references. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. ## Decision Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: -- canonical tenant identity and one UUIDv4-backed opaque intake reference; +- canonical UUIDv4 tenant identity and one UUIDv4-backed opaque intake reference; - UUIDv4-backed candidate-profile, requisition, authoritative Job, job-requirements, evidence-set, provenance, handling-policy, retention-policy and actor references; - independent SHA-256 digests where content drift matters; - one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; - mandatory human confirmation, immutable `requires_human_review` state, actionable next-step copy, and a fully redacted ordinary object representation. -Every namespaced trust reference must use the expected namespace plus a canonical non-sentinel UUIDv4 suffix. Human-readable/value-bearing suffixes, UUIDv1, and every other UUID version are rejected fail-closed. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUIDv4 syntax is only an early opacity guard; it is not tenant authority or relationship evidence. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed candidate correlation remains sensitive metadata rather than anonymous data. +The public tenant identity and every namespaced trust reference must use canonical non-sentinel UUIDv4. Namespaced references must also use the expected prefix. Human-readable/value-bearing suffixes, UUIDv1, and every other UUID version are rejected fail-closed so public identities cannot carry timestamp/node-derived correlation metadata. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUIDv4 syntax is only an early opacity guard; it is not tenant authority or relationship evidence. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed tenant and candidate correlation remain sensitive metadata rather than anonymous data. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, policy enforcement, or final approval. @@ -28,7 +28,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ### Positive - Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. -- Reference fields cannot quietly become a human-readable PII/value channel or UUIDv1 timestamp/node correlation channel. +- Public tenant and reference fields cannot quietly become a human-readable PII/value channel or UUIDv1 timestamp/node correlation channel. - Cross-tenant evidence mixing is fail-closed at the host sealing/review boundary because every packet reference must resolve within the packet tenant. - Source provenance, purpose-bound handling, and retention are explicit versioned evidence rather than prose-only assumptions. - Ordinary logging/assertion formatting does not emit candidate correlation or evidence digests. @@ -38,10 +38,10 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab - Hosts must use durable UUIDv4-backed public identifiers rather than convenient semantic slugs or UUID versions with embedded correlation metadata, and must authoritatively resolve them in the exact tenant before use. - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. -- UUID-backed candidate references are still sensitive correlating metadata and require least-privilege handling. +- UUID-backed tenant and candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. - This ADR remains proposed until its exact PR head merges into protected `develop`. ## References -See `docs/doctoring/candidate-evidence-intake-references.md`. +See `docs/doctoring/candidate-evidence-intake-references.md`. \ No newline at end of file From 57edb18af87151f7b0c721ada2cafdbaf8e7da4c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:40:29 -0700 Subject: [PATCH 39/48] docs: trace opaque UUIDv4 tenant identity in candidate intake --- docs/traceability/candidate-evidence-intake.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index 48ccaf06a..b17eaefa2 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -6,9 +6,9 @@ | Requirement | Evidence | |---|---| -| Correlate candidate evidence to the correct recruiting context | Canonical tenant plus UUIDv4-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | +| Correlate candidate evidence to the correct recruiting context | Canonical UUIDv4 tenant plus UUIDv4-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | | Prevent cross-tenant evidence mixing | `test_tenant_scope.py` requires tenant-scoped resolution of every packet reference before correlation/provenance checks and authoritative sealing; UUID syntax alone is not tenant authority | -| Prevent reference fields from becoming a covert PII/value/correlation channel | Every governed reference requires its expected namespace plus a canonical non-sentinel UUIDv4 suffix; human-readable/value-bearing suffixes, UUIDv1 timestamp/node metadata, and other UUID versions are rejected; `test_reference_privacy.py` covers every trust-reference field through builder and replacement paths | +| Prevent public identity/reference fields from becoming a covert PII/value/correlation channel | `tenant_record_id` and every governed reference require canonical non-sentinel UUIDv4 identity; namespaced references also require their expected prefix. Human-readable/value-bearing suffixes, UUIDv1 timestamp/node metadata, and other UUID versions are rejected; `test_reference_privacy.py` covers the tenant identity plus every trust-reference field through builder and replacement paths | | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | @@ -20,4 +20,4 @@ | Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | | Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | -The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. UUIDv4 is an opacity/privacy constraint only; those claims require their own owner-bound evidence. +The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. UUIDv4 is an opacity/privacy constraint only; those claims require their own owner-bound evidence. \ No newline at end of file From b64c4118905861ca8bf8b220764a411e5e92e5f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:40:51 -0700 Subject: [PATCH 40/48] docs: record UUIDv4 tenant privacy hardening --- packages/candidate-evidence/CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index 964fff142..fed409931 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -3,9 +3,9 @@ ## Unreleased - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. -- Bind evidence, source provenance, handling and retention artifacts by UUIDv4-backed opaque reference and SHA-256 digest. -- Reject human-readable/value-bearing reference suffixes, sentinel/noncanonical UUIDs, UUIDv1 timestamp/node correlation metadata, and every other non-v4 UUID version so governance references cannot become a covert candidate-data channel. +- Bind the public tenant identity plus evidence, source provenance, handling and retention artifacts to canonical UUIDv4-backed opaque identity; content-bearing evidence also carries SHA-256 digests. +- Reject human-readable/value-bearing reference suffixes, sentinel/noncanonical UUIDs, UUIDv1 timestamp/node correlation metadata, and every other non-v4 UUID version so tenant/reference identities cannot become a covert candidate-data or correlation channel. - Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUIDv4 syntax. - Redact the ordinary packet representation so candidate correlation and evidence digests are not emitted through routine logs/assertion failures. - Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. -- Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. +- Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. \ No newline at end of file From b0889f5bf4f4d1b67d2ffcb05151cad86e6bdf2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 20:46:18 +0900 Subject: [PATCH 41/48] fix: preserve candidate packet positional compatibility --- .../src/orgmetra_candidate_evidence/packet.py | 4 ++-- packages/candidate-evidence/tests/test_packet.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index 2b52652f5..d5f3e9c61 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -102,10 +102,10 @@ class CandidateEvidenceIntakePacket: purpose_code: str reason_code: str collected_at: datetime - evidence_version: int = 1 human_confirmation_required: bool = True review_state: str = _REVIEW_STATE next_action: str = _NEXT_ACTION + evidence_version: int = 1 def __repr__(self) -> str: """Return a representation that never emits candidate correlation evidence.""" @@ -249,4 +249,4 @@ def build_candidate_evidence_intake_packet( reason_code=reason_code, collected_at=collected_at, evidence_version=evidence_version, - ) \ No newline at end of file + ) diff --git a/packages/candidate-evidence/tests/test_packet.py b/packages/candidate-evidence/tests/test_packet.py index a20bdfda6..ed9b7151c 100644 --- a/packages/candidate-evidence/tests/test_packet.py +++ b/packages/candidate-evidence/tests/test_packet.py @@ -86,6 +86,19 @@ def test_builds_reference_only_deterministic_packet() -> None: assert packet == CandidateEvidenceIntakePacket(**values()) +def test_existing_positional_optional_arguments_retain_meaning() -> None: + """Keep existing positional callers bound to review controls after versioning.""" + packet = CandidateEvidenceIntakePacket( + *values().values(), + True, + "requires_human_review", + EXPECTED_NEXT_ACTION, + ) + assert packet.human_confirmation_required is True + assert packet.review_state == "requires_human_review" + assert packet.evidence_version == 1 + + @pytest.mark.parametrize( "field,bad", [ From c4154ebb0938a57c1e434274e2185b1c12af3b80 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:15:43 -0700 Subject: [PATCH 42/48] test: require candidate evidence to accept core tenant UUIDv7 --- .../tests/test_reference_privacy.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/candidate-evidence/tests/test_reference_privacy.py b/packages/candidate-evidence/tests/test_reference_privacy.py index e6ed5da93..60ffdd031 100644 --- a/packages/candidate-evidence/tests/test_reference_privacy.py +++ b/packages/candidate-evidence/tests/test_reference_privacy.py @@ -7,6 +7,7 @@ from orgmetra_candidate_evidence import build_candidate_evidence_intake_packet UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +UUID7_TENANT = "10000000-0000-7000-8000-000000000001" def valid_kwargs() -> dict[str, object]: @@ -66,13 +67,13 @@ def test_uuid1_trust_reference_is_rejected_by_builder_and_replace( replace(packet, **{field_name: value}) -def test_uuid1_tenant_identity_is_rejected_by_builder_and_replace() -> None: - """A public tenant identity must not admit UUIDv1 timestamp/node correlation metadata.""" +def test_authoritative_uuid7_tenant_identity_is_accepted_by_builder_and_replace() -> None: + """The leaf packet must accept tenant UUIDs already valid in authoritative Orgmetra core.""" kwargs = valid_kwargs() - kwargs["tenant_record_id"] = UUID1_ID - with pytest.raises(ValueError, match="tenant_record_id"): - build_candidate_evidence_intake_packet(**kwargs) + kwargs["tenant_record_id"] = UUID7_TENANT - packet = build_candidate_evidence_intake_packet(**valid_kwargs()) - with pytest.raises(ValueError, match="tenant_record_id"): - replace(packet, tenant_record_id=UUID1_ID) + packet = build_candidate_evidence_intake_packet(**kwargs) + replaced = replace(build_candidate_evidence_intake_packet(**valid_kwargs()), tenant_record_id=UUID7_TENANT) + + assert packet.tenant_record_id == UUID7_TENANT + assert replaced.tenant_record_id == UUID7_TENANT From d746da2bec02b859bb01ac9c302b4fa783509e9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:16:11 -0700 Subject: [PATCH 43/48] fix: honor authoritative tenant UUID contract in candidate evidence --- .../src/orgmetra_candidate_evidence/packet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py index d5f3e9c61..628488eb6 100644 --- a/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py +++ b/packages/candidate-evidence/src/orgmetra_candidate_evidence/packet.py @@ -31,13 +31,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical UUIDv4 text so a public governance identity stays opaque.""" + """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUIDv4") + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") def _validate_code(value: str, field_name: str) -> None: From a0c223b0d19b6bc2282c2eb7b9356f4dc707772d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:16:33 -0700 Subject: [PATCH 44/48] docs: align candidate evidence tenant identity with core --- packages/candidate-evidence/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/candidate-evidence/README.md b/packages/candidate-evidence/README.md index d9d828018..4d29f2674 100644 --- a/packages/candidate-evidence/README.md +++ b/packages/candidate-evidence/README.md @@ -2,10 +2,10 @@ This package defines a transport-neutral, reference-only packet for admitting candidate evidence into an accountable recruiting workflow without copying the candidate's evidence values into the governance envelope. -`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. The public `tenant_record_id` and every trust-bearing reference use canonical non-sentinel UUIDv4 identity; namespaced references additionally require their expected namespace. UUIDv1 and every other UUID version are rejected so timestamp/node-derived correlation metadata cannot enter otherwise opaque public identity fields. Content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. +`CandidateEvidenceIntakePacket` binds one tenant, candidate profile, requisition, authoritative Job, versioned job requirements, evidence set, source-provenance manifest, handling policy, retention policy, accountable actor, purpose/reason, bounded positive `evidence_version`, evidence-item count, and collection time. `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 namespaces. UUIDv1 and other non-v4 suffixes are rejected for those references so timestamp/node-derived or otherwise nonconforming identifiers cannot masquerade as the package's opaque trust-reference format. Content-bearing evidence also carries an independent lowercase SHA-256 digest. `evidence_version` is part of canonical JSON and SHA-256 evidence so actor/purpose/reason evidence from different contract revisions cannot silently collide; it must be a true integer from 1 through 2147483647. The packet deliberately contains no candidate name, email, demographic attribute, assessment value, raw resume/document content, credential, or free-form model output. Human-readable/value-bearing reference suffixes are rejected so those fields cannot become a covert PII channel. UUID-backed tenant and candidate correlation remain sensitive metadata and must still be handled under the bound purpose, handling policy, retention policy, least-privilege authorization, export controls, and audit boundary. The ordinary object representation is fully redacted to avoid leaking correlation/evidence through logs or assertion failures. -The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. UUIDv4 syntax constrains public-identity opacity only; it does not prove tenant ownership, record relationship, or authorization. +The packet cannot represent approval. `human_confirmation_required` must be the boolean singleton `True`, `review_state` is fixed to `requires_human_review`, and its immutable next action requires the host to re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary before evidence sealing. The host must then verify candidate↔requisition↔Job correlation, job relevance, source provenance, permitted handling, retention, and evidence completeness before requesting authoritative evidence sealing and accountable human review. Reference UUIDv4 syntax constrains packet-owned trust-reference opacity only; it does not prove tenant ownership, record relationship, or authorization, and tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. -Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. \ No newline at end of file +Canonical JSON and SHA-256 support immutable correlation only. They prove neither the truth of referenced evidence nor selection validity, fairness, legal compliance, authorization to use a particular evidence item, or final human approval. Authoritative persistence, tenant-scoped reference resolution, evidence sealing, selection decisions, audit/outbox, and candidate-to-worker conversion remain separate Orgmetra boundaries. From 0ca1c1a35267e2a51d0ac9e5dbadd1bdfa97034e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:16:54 -0700 Subject: [PATCH 45/48] docs: separate candidate packet and tenant UUID ownership --- docs/adr/0015-governed-candidate-evidence-intake.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0015-governed-candidate-evidence-intake.md index 759160628..9a17e2a9e 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0015-governed-candidate-evidence-intake.md @@ -7,19 +7,19 @@ Orgmetra already owns candidate profiles, sealed selection-decision evidence, candidate-to-worker conversion, governed requisition review, and candidate-neutral interview planning. A buyer still needs a defensible intake boundary between receiving candidate-related material and later sealing or using it in a high-impact selection decision. -Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. UUIDv1 adds a subtler correlation channel because timestamp/node-derived metadata can be embedded in an otherwise canonical UUID. That applies to the public tenant identity as well as namespaced trust references. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. +Copying resumes, assessment values, demographic attributes, or other candidate content into every workflow envelope increases privacy exposure and makes retention, provenance, and purpose control harder to audit. A nominally opaque packet-owned reference is also unsafe if arbitrary human-readable/value-bearing suffixes are accepted: candidate names or other sensitive values can then be smuggled through reference fields. UUIDv1 adds a subtler correlation channel for packet-owned references because timestamp/node-derived metadata can be embedded in an otherwise canonical UUID. The authoritative tenant identifier is different: it is issued by Orgmetra core, so this leaf package must accept the canonical non-sentinel operational UUID contract owned by that boundary rather than silently imposing a second version policy. Ordinary object representations can create the same leak if they print sensitive correlation metadata in logs or assertion failures. Canonical UUID syntax also does not prove tenant ownership, so a valid reference from another tenant must not be accepted as authoritative evidence merely because its shape is correct. ## Decision Add a transport-neutral `CandidateEvidenceIntakePacket` that binds: -- canonical UUIDv4 tenant identity and one UUIDv4-backed opaque intake reference; +- canonical non-sentinel Orgmetra tenant identity and one UUIDv4-backed opaque intake reference; - UUIDv4-backed candidate-profile, requisition, authoritative Job, job-requirements, evidence-set, provenance, handling-policy, retention-policy and actor references; - independent SHA-256 digests where content drift matters; - one accountable actor, bounded evidence-item count, fixed `candidate_evidence_intake` purpose, bounded reason metadata, and precision-preserving UTC collection time; - mandatory human confirmation, immutable `requires_human_review` state, actionable next-step copy, and a fully redacted ordinary object representation. -The public tenant identity and every namespaced trust reference must use canonical non-sentinel UUIDv4. Namespaced references must also use the expected prefix. Human-readable/value-bearing suffixes, UUIDv1, and every other UUID version are rejected fail-closed so public identities cannot carry timestamp/node-derived correlation metadata. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUIDv4 syntax is only an early opacity guard; it is not tenant authority or relationship evidence. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed tenant and candidate correlation remain sensitive metadata rather than anonymous data. +`tenant_record_id` must be canonical and non-sentinel under Orgmetra's authoritative operational UUID contract. The package does not reinterpret its UUID version because tenant identity generation and migration policy belong to the authoritative HRIS boundary. Packet-owned namespaced trust references separately require canonical non-sentinel UUIDv4 and the expected prefix. Human-readable/value-bearing suffixes, UUIDv1, and other non-v4 suffixes are rejected fail-closed for those references. Before sealing or accountable review, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary, then prove candidate↔requisition↔Job correlation and verify provenance, handling, retention, and completeness. UUID syntax is only an early shape/opacity guard; it is not tenant authority or relationship evidence. The governance packet contains no candidate name, email, demographic attribute, assessment value, raw document content, credential, or free-form model output. UUID-backed tenant and candidate correlation remain sensitive metadata rather than anonymous data. Canonical JSON plus SHA-256 provide immutable audit correlation but do not establish evidence truth, scientific validity, fairness, legal compliance, authorization, policy enforcement, or final approval. @@ -28,7 +28,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ### Positive - Recruiting workflows can correlate candidate evidence to the exact Job/requisition and policy versions without duplicating candidate content. -- Public tenant and reference fields cannot quietly become a human-readable PII/value channel or UUIDv1 timestamp/node correlation channel. +- Packet-owned reference fields cannot quietly become a human-readable PII/value channel or UUIDv1 timestamp/node correlation channel, while the leaf package remains interoperable with authoritative Orgmetra tenant UUIDs. - Cross-tenant evidence mixing is fail-closed at the host sealing/review boundary because every packet reference must resolve within the packet tenant. - Source provenance, purpose-bound handling, and retention are explicit versioned evidence rather than prose-only assumptions. - Ordinary logging/assertion formatting does not emit candidate correlation or evidence digests. @@ -36,7 +36,7 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ### Costs and constraints -- Hosts must use durable UUIDv4-backed public identifiers rather than convenient semantic slugs or UUID versions with embedded correlation metadata, and must authoritatively resolve them in the exact tenant before use. +- Packet-owned trust references must use durable UUIDv4-backed public identifiers and must be authoritatively resolved in the exact tenant before use. Tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. - The packet does not store raw candidate evidence, decide whether an item is lawfully usable, or prove the referenced policy was followed. - UUID-backed tenant and candidate references are still sensitive correlating metadata and require least-privilege handling. - Evidence sealing, authoritative selection decisions, immutable audit/outbox, deletion/retention execution, export controls, accommodations, adverse-impact monitoring, and jurisdiction-specific legal review remain separate obligations. @@ -44,4 +44,4 @@ Canonical JSON plus SHA-256 provide immutable audit correlation but do not estab ## References -See `docs/doctoring/candidate-evidence-intake-references.md`. \ No newline at end of file +See `docs/doctoring/candidate-evidence-intake-references.md`. From 4465590b160ff016a5964111d15b3030562c7a32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:17:10 -0700 Subject: [PATCH 46/48] docs: trace candidate tenant UUID interoperability --- docs/traceability/candidate-evidence-intake.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/traceability/candidate-evidence-intake.md b/docs/traceability/candidate-evidence-intake.md index b17eaefa2..62955813c 100644 --- a/docs/traceability/candidate-evidence-intake.md +++ b/docs/traceability/candidate-evidence-intake.md @@ -6,9 +6,9 @@ | Requirement | Evidence | |---|---| -| Correlate candidate evidence to the correct recruiting context | Canonical UUIDv4 tenant plus UUIDv4-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | +| Correlate candidate evidence to the correct recruiting context | Canonical non-sentinel tenant identity under the Orgmetra core operational-UUID contract plus UUIDv4-backed candidate-profile, requisition and Job references; immutable next action requires every packet reference to be re-resolved within the exact tenant and candidate↔requisition↔Job correlation verified before sealing/review | | Prevent cross-tenant evidence mixing | `test_tenant_scope.py` requires tenant-scoped resolution of every packet reference before correlation/provenance checks and authoritative sealing; UUID syntax alone is not tenant authority | -| Prevent public identity/reference fields from becoming a covert PII/value/correlation channel | `tenant_record_id` and every governed reference require canonical non-sentinel UUIDv4 identity; namespaced references also require their expected prefix. Human-readable/value-bearing suffixes, UUIDv1 timestamp/node metadata, and other UUID versions are rejected; `test_reference_privacy.py` covers the tenant identity plus every trust-reference field through builder and replacement paths | +| Prevent packet-owned reference fields from becoming a covert PII/value/correlation channel without duplicating tenant identity policy | `tenant_record_id` is canonical/non-sentinel under the authoritative core contract; every packet-owned governed reference requires canonical non-sentinel UUIDv4 plus its expected prefix. Human-readable/value-bearing suffixes and UUIDv1/non-v4 reference suffixes are rejected; `test_reference_privacy.py` covers authoritative UUIDv7 tenant interoperability plus every trust-reference field through builder and replacement paths | | Preserve job-related evidence context | Versioned job-requirements reference + SHA-256 digest | | Preserve evidence identity and source provenance without copying values | Evidence-set and source-provenance references + independent SHA-256 digests | | Bind privacy/operational handling | Handling-policy and retention-policy references + independent SHA-256 digests | @@ -20,4 +20,4 @@ | Minimize candidate content exposure | No candidate name/email/demographic attribute/assessment value/raw evidence/credential/free-form model output in the packet | | Keep service ownership boundaries intact | No database migration, no provider execution, no cross-service application-table SQL | -The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. UUIDv4 is an opacity/privacy constraint only; those claims require their own owner-bound evidence. \ No newline at end of file +The packet is correlation evidence, not proof of evidence truth, tenant ownership until authoritative resolution, lawful use, selection validity, fairness, policy execution, evidence sealing, or a final employment decision. UUIDv4 is an opacity/privacy constraint for packet-owned trust references only; tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. Those claims require their own owner-bound evidence. From 8b131d752ccded6b079413a325d17459e79b90e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:17:24 -0700 Subject: [PATCH 47/48] docs: record candidate tenant identity interoperability repair --- packages/candidate-evidence/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/candidate-evidence/CHANGELOG.md b/packages/candidate-evidence/CHANGELOG.md index fed409931..162d4cb86 100644 --- a/packages/candidate-evidence/CHANGELOG.md +++ b/packages/candidate-evidence/CHANGELOG.md @@ -3,9 +3,9 @@ ## Unreleased - Add a PII-minimized candidate-evidence intake packet with exact candidate/requisition/Job/job-requirements correlation. -- Bind the public tenant identity plus evidence, source provenance, handling and retention artifacts to canonical UUIDv4-backed opaque identity; content-bearing evidence also carries SHA-256 digests. -- Reject human-readable/value-bearing reference suffixes, sentinel/noncanonical UUIDs, UUIDv1 timestamp/node correlation metadata, and every other non-v4 UUID version so tenant/reference identities cannot become a covert candidate-data or correlation channel. -- Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUIDv4 syntax. +- Follow Orgmetra's authoritative canonical non-sentinel operational UUID contract for `tenant_record_id`, while packet-owned evidence, source-provenance, handling and retention references remain canonical UUIDv4-backed opaque identity; content-bearing evidence also carries SHA-256 digests. +- Reject human-readable/value-bearing reference suffixes, sentinel/noncanonical reference UUIDs, and UUIDv1/non-v4 packet-reference suffixes so trust-reference fields cannot become a covert candidate-data or correlation channel without duplicating tenant identity policy. +- Require every packet reference to be re-resolved within the exact tenant through its authoritative boundary before candidate↔requisition↔Job correlation, evidence sealing, or accountable review, preventing cross-tenant evidence mixing behind valid UUID syntax. - Redact the ordinary packet representation so candidate correlation and evidence digests are not emitted through routine logs/assertion failures. - Require bounded evidence counts, fixed purpose, explicit human-review state, precision-preserving UTC timestamps, deterministic canonical JSON, and exact packet SHA-256. -- Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. \ No newline at end of file +- Keep raw candidate evidence, demographic attributes, assessment values, credentials, and free-form model output outside the governance packet. From 03405df9e8bbf4b4e206b7ccd3e413e626618018 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 13:13:26 -0700 Subject: [PATCH 48/48] fix(candidate-evidence): reserve unique ADR number --- .github/workflows/candidate-evidence-quality.yml | 2 +- ...nce-intake.md => 0025-governed-candidate-evidence-intake.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/adr/{0015-governed-candidate-evidence-intake.md => 0025-governed-candidate-evidence-intake.md} (98%) diff --git a/.github/workflows/candidate-evidence-quality.yml b/.github/workflows/candidate-evidence-quality.yml index 0e573e611..45d02a7d1 100644 --- a/.github/workflows/candidate-evidence-quality.yml +++ b/.github/workflows/candidate-evidence-quality.yml @@ -8,7 +8,7 @@ on: - "packages/candidate-evidence/**" - ".github/requirements/foundation-test.txt" - ".github/workflows/candidate-evidence-quality.yml" - - "docs/adr/0015-governed-candidate-evidence-intake.md" + - "docs/adr/0025-governed-candidate-evidence-intake.md" - "docs/doctoring/candidate-evidence-intake-references.md" - "docs/traceability/candidate-evidence-intake.md" workflow_dispatch: diff --git a/docs/adr/0015-governed-candidate-evidence-intake.md b/docs/adr/0025-governed-candidate-evidence-intake.md similarity index 98% rename from docs/adr/0015-governed-candidate-evidence-intake.md rename to docs/adr/0025-governed-candidate-evidence-intake.md index 9a17e2a9e..ab66b3088 100644 --- a/docs/adr/0015-governed-candidate-evidence-intake.md +++ b/docs/adr/0025-governed-candidate-evidence-intake.md @@ -1,4 +1,4 @@ -# ADR 0015: Govern candidate evidence intake as reference-only evidence +# ADR 0025: Govern candidate evidence intake as reference-only evidence - **Status:** Proposed — active PR only - **Date:** 2026-08-19