diff --git a/.github/requirements/foundation-test.txt b/.github/requirements/foundation-test.txt index 40d926005..6fc4e5ab9 100644 --- a/.github/requirements/foundation-test.txt +++ b/.github/requirements/foundation-test.txt @@ -1,6 +1,6 @@ -# Reviewed Foundation CI test toolchain for CPython 3.14 on GitHub-hosted Ubuntu x86_64. +# Reviewed Foundation CI test toolchain for CPython 3.12-3.14 on GitHub-hosted Ubuntu x86_64. # Version and artifact hash changes must be reverified against the official PyPI release JSON. -coverage==7.14.2 --hash=sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64 +coverage==7.14.2 --hash=sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240 --hash=sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e --hash=sha256:cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64 iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 diff --git a/.github/workflows/semantic-job-evidence-adapter-quality.yml b/.github/workflows/semantic-job-evidence-adapter-quality.yml new file mode 100644 index 000000000..a6cdca13e --- /dev/null +++ b/.github/workflows/semantic-job-evidence-adapter-quality.yml @@ -0,0 +1,98 @@ +name: Semantic Job Evidence Adapter Quality + +on: + pull_request: + branches: + - bootstrap + - develop + - main + paths: + - "packages/semantic-job-evidence-adapter/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/semantic-job-evidence-adapter-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: semantic-job-evidence-adapter-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Semantic source evidence contract and 100% coverage (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13", "3.14"] + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python-version }} + check-latest: false + - name: Install reviewed test and build toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + printf '%s\n' 'setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670' > /tmp/orgmetra-semantic-job-evidence-build.txt + python -m pip install --require-hashes --no-deps --only-binary=:all: -r /tmp/orgmetra-semantic-job-evidence-build.txt + python -m pip check + - name: Compile adapter boundary + run: python -m compileall -q packages/semantic-job-evidence-adapter/src packages/semantic-job-evidence-adapter/tests + - name: Build and install exact package artifact + run: | + rm -rf /tmp/orgmetra-semantic-job-evidence-adapter-build /tmp/orgmetra-semantic-job-evidence-adapter-dist /tmp/orgmetra-semantic-job-evidence-adapter-venv + cp -a packages/semantic-job-evidence-adapter /tmp/orgmetra-semantic-job-evidence-adapter-build + mkdir -p /tmp/orgmetra-semantic-job-evidence-adapter-dist + python -m pip wheel --no-deps --no-build-isolation --wheel-dir /tmp/orgmetra-semantic-job-evidence-adapter-dist /tmp/orgmetra-semantic-job-evidence-adapter-build + test "$(find /tmp/orgmetra-semantic-job-evidence-adapter-dist -maxdepth 1 -type f -name '*.whl' | wc -l)" -eq 1 + python -m venv /tmp/orgmetra-semantic-job-evidence-adapter-venv + /tmp/orgmetra-semantic-job-evidence-adapter-venv/bin/python -m pip install --require-hashes --no-deps --only-binary=:all: -r "$GITHUB_WORKSPACE/.github/requirements/foundation-test.txt" + wheel_path="$(find /tmp/orgmetra-semantic-job-evidence-adapter-dist -maxdepth 1 -type f -name '*.whl' -print -quit)" + wheel_sha="$(sha256sum "$wheel_path" | awk '{print $1}')" + printf 'orgmetra-semantic-job-evidence-adapter[test] @ file://%s --hash=sha256:%s\n' "$wheel_path" "$wheel_sha" > /tmp/orgmetra-semantic-job-evidence-install.txt + /tmp/orgmetra-semantic-job-evidence-adapter-venv/bin/python -m pip install --require-hashes --no-deps -r /tmp/orgmetra-semantic-job-evidence-install.txt + /tmp/orgmetra-semantic-job-evidence-adapter-venv/bin/python -m pip check + /tmp/orgmetra-semantic-job-evidence-adapter-venv/bin/python - <<'PY' + from importlib.metadata import metadata + from pathlib import Path + import coverage + import pytest + import pytest_cov + import orgmetra_semantic_job_evidence_adapter + + venv_root = Path("/tmp/orgmetra-semantic-job-evidence-adapter-venv").resolve() + module_path = Path(orgmetra_semantic_job_evidence_adapter.__file__).resolve() + if not module_path.is_relative_to(venv_root): + raise SystemExit(f"package imported outside isolated environment: {module_path}") + for module in (coverage, pytest, pytest_cov): + dependency_path = Path(module.__file__).resolve() + if not dependency_path.is_relative_to(venv_root): + raise SystemExit( + f"test dependency imported outside isolated environment: {dependency_path}" + ) + if "test" not in (metadata("orgmetra-semantic-job-evidence-adapter").get_all("Provides-Extra") or []): + raise SystemExit("built distribution does not expose the reviewed test extra") + PY + - name: Test installed semantic source evidence with exact statement and branch coverage + env: + COVERAGE_FILE: /tmp/orgmetra-semantic-job-evidence-adapter.coverage + run: | + cd /tmp + /tmp/orgmetra-semantic-job-evidence-adapter-venv/bin/python -m pytest -c "$GITHUB_WORKSPACE/packages/semantic-job-evidence-adapter/pyproject.toml" "$GITHUB_WORKSPACE/packages/semantic-job-evidence-adapter/tests" + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/docs/adr/semantic-job-source-evidence.md b/docs/adr/semantic-job-source-evidence.md new file mode 100644 index 000000000..e6b70a654 --- /dev/null +++ b/docs/adr/semantic-job-source-evidence.md @@ -0,0 +1,46 @@ +# ADR — Govern Semantic Data Portal source evidence at the Orgmetra Job Analysis boundary + +## Status + +Active PR. This document does not describe protected-main truth until the owning PR merges. + +## Context + +Orgmetra's protected Job Analysis model already distinguishes authoritative human-reviewed evidence from draft/model-derived material, while protected traceability still lists Semantic Data Portal integration as planned. Semantic Data Portal is a separately owned CWL product and publishes ontology-resolution APIs. Direct table access or copying its implementation into Orgmetra would violate the dedicated-writer and modular-service boundary. + +Ontology resolution can improve Task/FJA/KSAO evidence discovery, but a semantic match is not itself an authoritative Job-analysis conclusion and must not become an autonomous employment decision. Orgmetra therefore needs a local governance artifact that records exactly what external contract and evidence version were reviewed without storing the raw ontology query/response in the audit correlation object. + +## Decision + +Orgmetra owns a final, immutable `SemanticJobEvidenceEnvelope` that binds: + +1. tenant and Job Analysis scope; +2. an opaque Orgmetra ontology-request reference; +3. distinct requesting and human-reviewing opaque `actor:` UUIDv4 references; +4. the closed use `job_analysis_source_evidence`; +5. SHA-256 digests for query-term evidence, response evidence, and source-catalog state; +6. the reviewed Semantic Data Portal revision `e48aa13c4af7a4875d4b53e6a60b50405c265a2f` and `POST /ontology/resolve` operation; +7. evidence version and exact UTC system-recorded time. + +The canonical evidence always records `external_source_evidence`, `requires_human_review`, and `not_authorized_for_job_or_employment_decision`. + +Semantic Data Portal remains read-only to this Orgmetra lane. No foreign application table is queried. Provider revision/API drift fails closed until explicitly reviewed. Raw ontology content, PII, credentials, scores, and decisions stay outside this value-minimized envelope. + +Trust-bearing runtime primitives are accepted only as exact built-in types before caller-overridable equality, hashing, comparison, parsing, or serialization can run. Creation-time evidence is sealed in process and its authoritative seal is held in a lock-protected issuance registry outside envelope-writable slots. Canonical export verifies one canonical payload snapshot and returns that same snapshot/JSON rather than rereading live fields after the integrity decision, so checked and emitted evidence cannot diverge through an intervening same-process mutation. + +The issuance registry and process MAC key are intentionally process-local. Copy/deepcopy, pickle/unpickle, worker transfer, and process restart do not recreate an envelope's issuance authority; restored envelope objects fail closed. Durable systems must persist the already-emitted canonical JSON and its evidence digest through Orgmetra's immutable audit/outbox boundary, not serialize a live envelope and expect it to regain process-local validation state. If long-term independent revalidation becomes a requirement, a separately governed managed and rotatable key/signing boundary must be designed; it is not claimed by this slice. + +The package's supported runtime is deliberately bounded to Python `>=3.12,<3.15` and the dedicated quality workflow executes the installed artifact on 3.12, 3.13, and 3.14 before support is claimed. New Python minors require explicit compatibility evidence before widening that range. + +## Consequences + +- Buyers can trace a Job Analysis source claim to an exact external contract revision and evidence digests without treating that source as authoritative by syntax alone. +- Human review remains explicit and separable from source retrieval. +- A future Semantic Data Portal contract change requires an Orgmetra review/update rather than silently changing evidence semantics. +- Process-local tamper evidence is safe to use only in the issuing process; durable evidence uses canonical bytes/digest plus the repository's immutable audit/outbox controls. +- This slice does not implement network transport, foreign retries, foreign authorization, raw ontology storage, Job Analysis approval, employment decisions, or durable signing-key management. +- The approach is compatible with W3C provenance principles: source entities and activities remain externally owned while Orgmetra records bounded provenance needed for its own evidence chain. + +## Verification + +The package quality lane requires exact-current-head tests, exact 100% owned production statement and branch coverage, installed-wheel execution across the declared Python minor range, adversarial runtime-integrity regressions, and a clean checkout. Repository-level Foundation/SAST/Security/Recovery evidence remains separately required by live merge governance. diff --git a/docs/doctoring/semantic-job-evidence-references.md b/docs/doctoring/semantic-job-evidence-references.md new file mode 100644 index 000000000..73d2b0c0a --- /dev/null +++ b/docs/doctoring/semantic-job-evidence-references.md @@ -0,0 +1,23 @@ +# Semantic Job Evidence — primary references + +## Scope + +These references support the active-PR decision to keep foreign ontology output as provenance-bearing source evidence that requires human review, rather than copying a dedicated-writer service or treating semantic resolution as authoritative Job/employment decision evidence. They also record the primary package metadata used to bind the adapter's declared Python support to its hosted compatibility evidence. + +## References (APA 7) + +ContextualWisdomLab. (2026). *Semantic Data Portal* (Revision e48aa13c4af7a4875d4b53e6a60b50405c265a2f) [Computer software]. GitHub. https://github.com/ContextualWisdomLab/semantic-data-portal/tree/e48aa13c4af7a4875d4b53e6a60b50405c265a2f + +Lebo, T., Sahoo, S., & McGuinness, D. (Eds.). (2013). *PROV-O: The PROV ontology* (W3C Recommendation). World Wide Web Consortium. https://www.w3.org/TR/prov-o/ + +Python Software Foundation. (2026). *coverage 7.14.2 release metadata* [JSON metadata]. Python Package Index. https://pypi.org/pypi/coverage/7.14.2/json + +Tabassi, E. (2023). *Artificial intelligence risk management framework (AI RMF 1.0)* (NIST AI 100-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.100-1 + +## Evidence notes + +- The pinned Semantic Data Portal README publishes `POST /ontology/resolve` as an ontology/terminology API. That is the exact foreign operation recorded by this Orgmetra adapter; the dependency remains read-only. +- W3C PROV-O is a W3C Recommendation for interoperable provenance representation across heterogeneous systems. The Orgmetra envelope uses a small application-specific provenance record rather than claiming PROV-O serialization compliance. +- NIST AI RMF 1.0 remains the published final framework while NIST develops revisions/profiles. Its risk-management framing supports keeping model/semantic outputs governed and reviewable. This package does not claim AI RMF conformity or certification. +- The official PyPI JSON for coverage 7.14.2 advertises Python 3.12, 3.13, and 3.14 support and publishes distinct SHA-256 digests for their CPython Linux wheels. The Orgmetra hash lock admits only the exact reviewed 3.12/3.13/3.14 coverage wheel hashes used by the hosted matrix; it does not disable `--require-hashes` or broaden dependency versions. +- No psychometric/statistical estimator is implemented in this slice, so no research-only statistical claim is introduced and no foreign psychometric kernel is duplicated. diff --git a/docs/traceability/semantic-job-evidence.md b/docs/traceability/semantic-job-evidence.md new file mode 100644 index 000000000..de5a289af --- /dev/null +++ b/docs/traceability/semantic-job-evidence.md @@ -0,0 +1,32 @@ +# Semantic Job Evidence Traceability + +## Maturity + +`active_pr`. Protected `develop` still lists Semantic Data Portal / ontology integration as planned. This document records only the executable scope of the owning PR and must not be read as protected-main truth until merge. + +| Requirement | Executable evidence | Boundary | +|---|---|---| +| Consume only a published foreign contract | reviewed Semantic Data Portal revision `e48aa13c4af7a4875d4b53e6a60b50405c265a2f`; exact `POST /ontology/resolve` operation | read-only dependency; no foreign table access | +| Bind source evidence to Orgmetra scope | canonical tenant, `job_analysis:` and `ontology_request:` references | Orgmetra-owned evidence envelope | +| Require accountable human review | distinct `actor:` UUIDv4 requester and reviewer; canonical state `requires_human_review` | syntax is correlation only; host identity/scope resolution remains authoritative | +| Prevent semantic evidence from becoming a decision | canonical state `not_authorized_for_job_or_employment_decision` | source evidence cannot authorize Job/employment action | +| Minimize HR/audit exposure | query term, response and source catalog represented only by SHA-256 digests | no raw query/response, PII, credential, score, or decision in canonical evidence | +| Preserve exact source provenance | foreign revision, API operation, source-system/trust-state, evidence version, UTC recorded time | provider drift fails closed | +| Prevent runtime evidence forgery | exact built-in primitives, UUID/reference/digest validation, final runtime type | adversarial subclass regressions | +| Prevent post-issuance rewrite | packet consistency seal plus lock-protected process-local authoritative issuance seal | payload-only, seal-only, payload+recomputed-seal, replace, and marker-tamper regressions | +| Prevent checked/emitted evidence divergence | canonical export returns the exact payload/JSON snapshot used for live seal verification | deterministic mutation-between-check-and-return regression | +| Keep process-local issuance semantics explicit | restored/copied envelope objects do not regain issuance registry state; durable systems persist emitted canonical JSON + digest | README/ADR boundary; managed rotatable long-term seal is future work only | +| Bound declared Python compatibility to evidence | `requires-python = ">=3.12,<3.15"`; hosted matrix executes 3.12, 3.13, 3.14 | support range cannot widen without new current-head CI evidence | +| Maintain exact owned production coverage | dedicated `Semantic Job Evidence Adapter Quality` workflow | 100% statement and branch coverage required on every matrix runtime | + +## Test mapping + +`packages/semantic-job-evidence-adapter/tests/test_envelope.py` verifies canonical value minimization, reviewed trust states, tenant/reference validity, requester/reviewer separation, source revision/API binding, bounded evidence versions, exact UTC recorded time, hostile runtime subclasses, post-construction mutation, dataclass replacement/seal reset, marker/seal tampering, checked-snapshot export, redacted repr, and final runtime type. + +`packages/semantic-job-evidence-adapter/tests/test_creation_seal_integrity.py` proves that rewriting a valid trust-bearing field together with a freshly recomputed packet-owned HMAC cannot authorize changed evidence because the authoritative creation seal is stored outside envelope-writable slots. + +`packages/semantic-job-evidence-adapter/tests/test_python_support_contract.py` binds public Python support metadata to the hosted 3.12/3.13/3.14 compatibility matrix. + +## Non-claims + +This active PR does not prove the truth of Semantic Data Portal content, does not authenticate actor syntax, does not implement the network client, does not directly approve a Job Analysis, and does not authorize a hiring or other employment decision. The process-local issuance seal is tamper evidence, not durable cryptographic attestation or a managed signing service. Those authorities remain with their owning Orgmetra and dedicated-writer boundaries. diff --git a/packages/semantic-job-evidence-adapter/CHANGELOG.md b/packages/semantic-job-evidence-adapter/CHANGELOG.md new file mode 100644 index 000000000..94518ae3d --- /dev/null +++ b/packages/semantic-job-evidence-adapter/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## Unreleased + +- Add a value-minimized, human-review-required Semantic Data Portal ontology evidence envelope for Job Analysis. +- Pin the reviewed read-only dependency revision and `POST /ontology/resolve` API operation. +- Bind tenant, Job Analysis scope, accountable actors, source/query/response digests, evidence version, and UTC system-recorded time without copying raw ontology or HR content. +- Require opaque canonical `actor:` UUIDv4 correlations so human-readable actor handles cannot enter durable evidence. +- Fail closed on malformed governance evidence, self-review, caller-defined primitive subclasses, dependency-contract drift, post-construction mutation, seal reset, and runtime type extension. +- Repair the post-issuance integrity path so rewriting a payload together with a recomputed packet-owned seal still fails: the authoritative creation seal is held in a process-local, lock-protected issuance registry outside envelope-writable slots. +- Return the exact canonical payload/JSON snapshot that passed seal verification so an intervening same-process mutation cannot make checked bytes and emitted bytes diverge. +- Document the process-local issuance boundary: copied/restored envelope objects fail closed, while durable audit/outbox persistence stores emitted canonical JSON and evidence digest rather than the live envelope object. +- Bound declared runtime support to Python `>=3.12,<3.15` and execute the installed wheel on Python 3.12, 3.13, and 3.14 before claiming compatibility. +- Add an exact-head quality workflow with exact 100% owned production statement and branch coverage plus clean-checkout enforcement. +- Build a wheel and execute the quality suite against the SHA-256-bound installed artifact in a fully isolated virtual environment; install the reviewed hash-pinned pytest/coverage toolchain inside that environment and fail closed if package or test-tool imports resolve outside it. diff --git a/packages/semantic-job-evidence-adapter/README.md b/packages/semantic-job-evidence-adapter/README.md new file mode 100644 index 000000000..f9ac6edef --- /dev/null +++ b/packages/semantic-job-evidence-adapter/README.md @@ -0,0 +1,42 @@ +# Orgmetra Semantic Job Evidence Adapter + +This package is the Orgmetra-owned trust boundary for ontology-resolution evidence imported from the read-only Semantic Data Portal dependency. + +## What it does + +`SemanticJobEvidenceEnvelope` binds one ontology-resolution result to: + +- one Orgmetra tenant and one opaque Job Analysis reference; +- one opaque ontology-request reference; +- distinct requesting and human-reviewing actors; +- the approved non-decision use `job_analysis_source_evidence`; +- SHA-256 digests of the submitted term evidence, returned response evidence, and reviewed source-catalog state; +- the reviewed Semantic Data Portal revision `e48aa13c4af7a4875d4b53e6a60b50405c265a2f`; +- the reviewed `POST /ontology/resolve` operation; +- an evidence version and exact UTC system-recorded timestamp. + +The canonical document always declares the imported material to be `external_source_evidence`, `requires_human_review`, and `not_authorized_for_job_or_employment_decision`. + +## What it deliberately does not do + +The envelope does not carry the raw ontology query term, raw response, candidate/worker PII, credentials, scores, or a Job/employment decision. A syntactically valid actor reference is correlation evidence, not proof of identity. The host must resolve actors and tenant/Job Analysis scope through Orgmetra's authoritative boundaries before source evidence is accepted into a reviewed Job Analysis snapshot. + +Orgmetra does not read Semantic Data Portal application tables. The foreign service remains independently deployable and is consumed only through its published API contract. A changed provider revision or API operation fails closed until reviewed and explicitly updated here. + +## Evidence integrity + +Trust-bearing text, integers, and timestamps must be exact built-in runtime types before equality, membership, bounds, UUID parsing, or serialization. Packet-owned references, including actor correlations, use canonical UUIDv4 suffixes; the tenant ID follows Orgmetra's canonical non-sentinel operational UUID contract. The envelope is final and detects post-construction rewriting before canonical evidence leaves the boundary. Its packet-owned HMAC is only a consistency value: the authoritative creation seal is held in a lock-protected process-local issuance registry outside envelope-writable slots, so rewriting both payload and packet seal still fails closed. + +Canonical export returns the exact payload snapshot whose seal was verified; it does not re-read live fields after the integrity decision. This closes a same-process mutation window in which the checked bytes and emitted bytes could otherwise diverge. + +The issuance registry and process seal key are intentionally process-local tamper evidence, not durable cryptographic attestation. Copy/deepcopy, pickle/unpickle, worker-process transfer, or process restart does not recreate issuance authority; a restored envelope fails closed. If durable evidence is needed, persist the already-emitted `canonical_json()` bytes and `evidence_digest()` in Orgmetra's immutable audit/outbox boundary rather than serializing the live envelope object. A future requirement for independent long-term envelope revalidation would need a separately governed managed/rotatable signing or MAC key boundary; this package does not claim one. + +## Python compatibility + +The package currently declares and tests Python `>=3.12,<3.15`. Hosted quality evidence runs the installed wheel and its reviewed test toolchain on Python 3.12, 3.13, and 3.14. A new Python minor must be added to the hosted compatibility matrix before the public support range is widened. + +## Testing + +The dedicated quality lane runs the package tests with exact 100% owned production statement and branch coverage and requires a clean checkout. Adversarial regressions cover malformed references/digests, self-review, runtime-subclass forgery, invalid dependency revision/API use, payload-only mutation, packet-seal mutation, payload plus recomputed-seal forgery, checked-snapshot export, replacement/seal reset, and runtime-type extension. + +See `docs/traceability/semantic-job-evidence.md`, `docs/adr/semantic-job-source-evidence.md`, and `docs/doctoring/semantic-job-evidence-references.md` for the governed rationale and evidence map. diff --git a/packages/semantic-job-evidence-adapter/pyproject.toml b/packages/semantic-job-evidence-adapter/pyproject.toml new file mode 100644 index 000000000..2b58166d2 --- /dev/null +++ b/packages/semantic-job-evidence-adapter/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-semantic-job-evidence-adapter" +version = "0.1.0" +description = "Fail-closed Semantic Data Portal ontology evidence boundary for Orgmetra job analysis." +requires-python = ">=3.12,<3.15" + +[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_semantic_job_evidence_adapter", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/__init__.py b/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/__init__.py new file mode 100644 index 000000000..6ab7f307e --- /dev/null +++ b/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/__init__.py @@ -0,0 +1,5 @@ +"""Public contract for governed Semantic Data Portal job-analysis evidence.""" + +from .envelope import SemanticJobEvidenceEnvelope + +__all__ = ["SemanticJobEvidenceEnvelope"] diff --git a/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/envelope.py b/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/envelope.py new file mode 100644 index 000000000..21dfb6924 --- /dev/null +++ b/packages/semantic-job-evidence-adapter/src/orgmetra_semantic_job_evidence_adapter/envelope.py @@ -0,0 +1,240 @@ +"""Governed, value-minimized Semantic Data Portal ontology evidence for job analysis.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime, timezone +from hashlib import sha256 +import hmac +import json +import re +import secrets +from threading import RLock +from typing import ClassVar +from uuid import UUID +from weakref import finalize + + +_SEMANTIC_DATA_PORTAL_REVISION = "e48aa13c4af7a4875d4b53e6a60b50405c265a2f" +_PROCESS_SEAL_KEY = secrets.token_bytes(32) +_NEW_ISSUANCE_MARKER = object() +_USED_ISSUANCE_MARKER = object() +_DIGEST_PATTERN = re.compile(r"[0-9a-f]{64}") +_ALLOWED_RESOLUTION_USES = frozenset({"job_analysis_source_evidence"}) +_CREATION_SEALS: dict[int, str] = {} +_CREATION_SEALS_LOCK = RLock() + + +def _discard_creation_seal(envelope_id: int) -> None: + """Discard the process-local authoritative seal when its envelope is collected.""" + with _CREATION_SEALS_LOCK: + _CREATION_SEALS.pop(envelope_id, None) + + +def _register_creation_seal(envelope: object, seal: str) -> None: + """Bind one live envelope identity to creation evidence outside writable slots.""" + envelope_id = id(envelope) + with _CREATION_SEALS_LOCK: + _CREATION_SEALS[envelope_id] = seal + finalize(envelope, _discard_creation_seal, envelope_id) + + +def _authoritative_creation_seal(envelope: object) -> str | None: + """Return process-local creation evidence without trusting packet-owned state.""" + with _CREATION_SEALS_LOCK: + return _CREATION_SEALS.get(id(envelope)) + + +def _require_text(value: object, field_name: str) -> str: + """Return exact built-in non-empty text before caller-defined behavior can run.""" + if type(value) is not str or not value: + raise ValueError(f"{field_name} must be exact non-empty text") + return value + + +def _validate_operational_uuid(value: object, field_name: str) -> str: + """Require one canonical non-sentinel operational UUID string.""" + text = _require_text(value, field_name) + try: + parsed = UUID(text) + except (ValueError, AttributeError, TypeError) as error: + raise ValueError(f"{field_name} must be a canonical operational UUID") from error + if str(parsed) != text or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical non-sentinel operational UUID") + return text + + +def _validate_reference(value: object, field_name: str, namespace: str) -> str: + """Require a bounded namespaced reference with a canonical UUIDv4 suffix.""" + text = _require_text(value, field_name) + prefix = f"{namespace}:" + if len(text) > 180 or not text.startswith(prefix): + raise ValueError(f"{field_name} must be a bounded {namespace}: UUIDv4 reference") + suffix = text[len(prefix) :] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as error: + raise ValueError(f"{field_name} must end in a canonical UUIDv4") from error + if str(parsed) != suffix or parsed.version != 4: + raise ValueError(f"{field_name} must end in a canonical UUIDv4") + return text + + +def _validate_actor_reference(value: object, field_name: str) -> str: + """Require opaque actor correlation with a canonical UUIDv4 suffix.""" + return _validate_reference(value, field_name, "actor") + + +def _validate_digest(value: object, field_name: str) -> str: + """Require one lowercase SHA-256 evidence digest.""" + text = _require_text(value, field_name) + if _DIGEST_PATTERN.fullmatch(text) is None: + raise ValueError(f"{field_name} must be a lowercase SHA-256 digest") + return text + + +def _validate_recorded_at(value: object) -> datetime: + """Require exact built-in UTC system-recorded time for immutable evidence.""" + if type(value) is not datetime or value.tzinfo is not timezone.utc: + raise ValueError("recorded_at must be an exact built-in UTC datetime") + return value + + +def _canonical_timestamp(value: datetime) -> str: + """Render an already-governed UTC timestamp in deterministic RFC 3339 form.""" + return value.isoformat().replace("+00:00", "Z") + + +def _seal(payload_json: str) -> str: + """Bind one in-process issuance to its exact creation-time canonical payload.""" + return hmac.new(_PROCESS_SEAL_KEY, payload_json.encode("utf-8"), "sha256").hexdigest() + + +@dataclass(frozen=True, slots=True, repr=False, weakref_slot=True) +class SemanticJobEvidenceEnvelope: + """Bind ontology source provenance without granting Job or employment decision authority.""" + + tenant_record_id: str + job_analysis_reference: str + ontology_request_reference: str + requesting_actor_reference: str + reviewing_actor_reference: str + resolution_use_code: str + query_term_digest: str + response_evidence_digest: str + source_catalog_digest: str + semantic_data_portal_revision: str + api_operation: str + evidence_version: int + recorded_at: datetime + _creation_seal: str | None = field(default=None, repr=False, compare=False) + _issuance_marker: object = field(default=_NEW_ISSUANCE_MARKER, repr=False, compare=False) + + SOURCE_SYSTEM: ClassVar[str] = "semantic-data-portal" + SOURCE_TRUST_STATE: ClassVar[str] = "external_source_evidence" + REVIEW_STATE: ClassVar[str] = "requires_human_review" + DECISION_AUTHORITY_STATE: ClassVar[str] = "not_authorized_for_job_or_employment_decision" + + def __init_subclass__(cls, **kwargs: object) -> None: + """Keep the trust-bearing runtime type final.""" + raise TypeError("SemanticJobEvidenceEnvelope is final") + + def __post_init__(self) -> None: + """Validate the reviewed boundary and seal its exact creation-time evidence.""" + if self._issuance_marker is not _NEW_ISSUANCE_MARKER: + raise ValueError("semantic job evidence changed after construction") + if self._creation_seal is not None: + raise ValueError("semantic job evidence changed after construction") + self._validate_fields() + seal = _seal(self._canonical_payload_json()) + object.__setattr__(self, "_creation_seal", seal) + object.__setattr__(self, "_issuance_marker", _USED_ISSUANCE_MARKER) + _register_creation_seal(self, seal) + + def _validate_fields(self) -> None: + """Fail closed on scope, source provenance, actor separation, and reviewed state.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference(self.job_analysis_reference, "job_analysis_reference", "job_analysis") + _validate_reference(self.ontology_request_reference, "ontology_request_reference", "ontology_request") + requester = _validate_actor_reference(self.requesting_actor_reference, "requesting_actor_reference") + reviewer = _validate_actor_reference(self.reviewing_actor_reference, "reviewing_actor_reference") + if requester == reviewer: + raise ValueError("reviewing_actor_reference must differ from requesting_actor_reference") + resolution_use = _require_text(self.resolution_use_code, "resolution_use_code") + if resolution_use not in _ALLOWED_RESOLUTION_USES: + raise ValueError("resolution_use_code is not an approved source-evidence use") + _validate_digest(self.query_term_digest, "query_term_digest") + _validate_digest(self.response_evidence_digest, "response_evidence_digest") + _validate_digest(self.source_catalog_digest, "source_catalog_digest") + revision = _require_text(self.semantic_data_portal_revision, "semantic_data_portal_revision") + if revision != _SEMANTIC_DATA_PORTAL_REVISION: + raise ValueError("semantic_data_portal_revision must match the reviewed dependency revision") + operation = _require_text(self.api_operation, "api_operation") + if operation != "POST /ontology/resolve": + raise ValueError("api_operation must use the reviewed ontology-resolution contract") + if type(self.evidence_version) is not int or not 1 <= self.evidence_version <= 1_000_000: + raise ValueError("evidence_version must be an exact positive bounded integer") + _validate_recorded_at(self.recorded_at) + + def _payload(self) -> dict[str, object]: + """Return value-minimized canonical evidence without raw ontology or HR content.""" + return { + "api_operation": self.api_operation, + "decision_authority_state": self.DECISION_AUTHORITY_STATE, + "evidence_version": self.evidence_version, + "job_analysis_reference": self.job_analysis_reference, + "ontology_request_reference": self.ontology_request_reference, + "query_term_digest": self.query_term_digest, + "recorded_at": _canonical_timestamp(self.recorded_at), + "requesting_actor_reference": self.requesting_actor_reference, + "resolution_use_code": self.resolution_use_code, + "response_evidence_digest": self.response_evidence_digest, + "review_state": self.REVIEW_STATE, + "reviewing_actor_reference": self.reviewing_actor_reference, + "semantic_data_portal_revision": self.semantic_data_portal_revision, + "source_catalog_digest": self.source_catalog_digest, + "source_system": self.SOURCE_SYSTEM, + "source_trust_state": self.SOURCE_TRUST_STATE, + "tenant_record_id": self.tenant_record_id, + } + + def _canonical_payload_json(self) -> str: + """Serialize the live evidence deterministically without consulting its creation seal.""" + return json.dumps(self._payload(), sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def _assert_integrity(self) -> tuple[dict[str, object], str]: + """Return the exact checked snapshot while rejecting post-construction rewriting.""" + self._validate_fields() + if self._issuance_marker is not _USED_ISSUANCE_MARKER: + raise ValueError("semantic job evidence changed after construction") + packet_seal = self._creation_seal + authoritative_seal = _authoritative_creation_seal(self) + payload = self._payload() + payload_json = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + live_seal = _seal(payload_json) + if ( + type(packet_seal) is not str + or type(authoritative_seal) is not str + or not hmac.compare_digest(packet_seal, authoritative_seal) + or not hmac.compare_digest(live_seal, authoritative_seal) + ): + raise ValueError("semantic job evidence changed after construction") + return payload, payload_json + + def canonical_document(self) -> dict[str, object]: + """Return the exact canonical document snapshot that passed integrity verification.""" + payload, _ = self._assert_integrity() + return payload + + def canonical_json(self) -> str: + """Return the exact deterministic JSON snapshot that passed integrity verification.""" + _, payload_json = self._assert_integrity() + return payload_json + + def evidence_digest(self) -> str: + """Return SHA-256 of the exact canonical evidence bytes.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + def __repr__(self) -> str: + """Avoid leaking tenant, actors, Job-analysis scope, or source correlation into logs.""" + return "SemanticJobEvidenceEnvelope()" diff --git a/packages/semantic-job-evidence-adapter/tests/conftest.py b/packages/semantic-job-evidence-adapter/tests/conftest.py new file mode 100644 index 000000000..07a27756e --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/conftest.py @@ -0,0 +1,29 @@ +"""Shared pytest fixtures for the semantic job evidence adapter contract.""" + +from datetime import datetime, timezone +from uuid import uuid4 + +import pytest + + +SDP_REVISION = "e48aa13c4af7a4875d4b53e6a60b50405c265a2f" + + +@pytest.fixture +def semantic_values() -> dict[str, object]: + """Return one fresh valid value-minimized ontology source-evidence fixture.""" + return { + "tenant_record_id": str(uuid4()), + "job_analysis_reference": f"job_analysis:{uuid4()}", + "ontology_request_reference": f"ontology_request:{uuid4()}", + "requesting_actor_reference": f"actor:{uuid4()}", + "reviewing_actor_reference": f"actor:{uuid4()}", + "resolution_use_code": "job_analysis_source_evidence", + "query_term_digest": "a" * 64, + "response_evidence_digest": "b" * 64, + "source_catalog_digest": "c" * 64, + "semantic_data_portal_revision": SDP_REVISION, + "api_operation": "POST /ontology/resolve", + "evidence_version": 1, + "recorded_at": datetime(2026, 8, 22, 14, 50, 12, 123456, tzinfo=timezone.utc), + } diff --git a/packages/semantic-job-evidence-adapter/tests/test_artifact_execution.py b/packages/semantic-job-evidence-adapter/tests/test_artifact_execution.py new file mode 100644 index 000000000..2fc021517 --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/test_artifact_execution.py @@ -0,0 +1,28 @@ +"""Regression contract for exact installed-wheel quality execution.""" + +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[3] +_WORKFLOW_PATH = _REPOSITORY_ROOT / ".github/workflows/semantic-job-evidence-adapter-quality.yml" +_VENV_PATH = "/tmp/orgmetra-semantic-job-evidence-adapter-venv" + + +def test_quality_lane_executes_the_hash_bound_installed_wheel() -> None: + """Require tests and reviewed test dependencies to execute inside an isolated venv.""" + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + + assert "PYTHONPATH: packages/semantic-job-evidence-adapter/src" not in workflow + assert f"python -m venv {_VENV_PATH}" in workflow + assert ( + f'{_VENV_PATH}/bin/python -m pip install --require-hashes --no-deps ' + f'--only-binary=:all: -r "$GITHUB_WORKSPACE/.github/requirements/foundation-test.txt"' + in workflow + ) + assert "wheel_sha=\"$(sha256sum \"$wheel_path\" | awk '{print $1}')\"" in workflow + assert "for module in (coverage, pytest, pytest_cov):" in workflow + assert ( + f"{_VENV_PATH}/bin/python -m pytest " + '-c "$GITHUB_WORKSPACE/packages/semantic-job-evidence-adapter/pyproject.toml"' + in workflow + ) diff --git a/packages/semantic-job-evidence-adapter/tests/test_creation_seal_integrity.py b/packages/semantic-job-evidence-adapter/tests/test_creation_seal_integrity.py new file mode 100644 index 000000000..158743afa --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/test_creation_seal_integrity.py @@ -0,0 +1,19 @@ +"""Regression for authoritative creation evidence living outside packet-writable slots.""" + +import pytest + +import orgmetra_semantic_job_evidence_adapter.envelope as envelope_module +from orgmetra_semantic_job_evidence_adapter import SemanticJobEvidenceEnvelope + + +def test_recomputed_packet_owned_seal_cannot_authorize_rewritten_evidence( + semantic_values: dict[str, object], +) -> None: + """Rewriting payload plus its packet-owned seal must still fail closed.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + object.__setattr__(packet, "response_evidence_digest", "d" * 64) + forged_seal = envelope_module._seal(packet._canonical_payload_json()) + object.__setattr__(packet, "_creation_seal", forged_seal) + + with pytest.raises(ValueError, match="changed after construction"): + packet.canonical_json() diff --git a/packages/semantic-job-evidence-adapter/tests/test_docstrings.py b/packages/semantic-job-evidence-adapter/tests/test_docstrings.py new file mode 100644 index 000000000..be75b82cc --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/test_docstrings.py @@ -0,0 +1,33 @@ +"""Executable documentation-completeness contract for semantic source evidence.""" + +from __future__ import annotations + +import ast +from pathlib import Path + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +SOURCE_ROOT = PACKAGE_ROOT / "src" / "orgmetra_semantic_job_evidence_adapter" +TEST_ROOT = PACKAGE_ROOT / "tests" + + +def _python_files() -> tuple[Path, ...]: + """Return every owned Python source/test file recursively in deterministic order.""" + return tuple(sorted({*SOURCE_ROOT.rglob("*.py"), *TEST_ROOT.rglob("*.py")})) + + +def test_owned_python_modules_and_callables_are_documented() -> None: + """Require beginner-readable docstrings on every owned module, class, and callable.""" + paths = _python_files() + assert paths, f"No owned Python files discovered under {SOURCE_ROOT} or {TEST_ROOT}" + missing: list[str] = [] + for path in paths: + 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) diff --git a/packages/semantic-job-evidence-adapter/tests/test_envelope.py b/packages/semantic-job-evidence-adapter/tests/test_envelope.py new file mode 100644 index 000000000..0af059612 --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/test_envelope.py @@ -0,0 +1,204 @@ +"""Adversarial contract tests for governed Semantic Data Portal source evidence.""" + +from dataclasses import replace +from datetime import datetime, timezone +from hashlib import sha256 +import json +from uuid import uuid1, uuid4 + +import pytest + +from orgmetra_semantic_job_evidence_adapter import SemanticJobEvidenceEnvelope + + +def test_canonical_evidence_is_value_minimized_and_deterministic( + semantic_values: dict[str, object], +) -> None: + """Canonical evidence contains governance/provenance only and has stable bytes.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + document = packet.canonical_document() + + assert document["source_system"] == "semantic-data-portal" + assert document["source_trust_state"] == "external_source_evidence" + assert document["review_state"] == "requires_human_review" + assert document["decision_authority_state"] == "not_authorized_for_job_or_employment_decision" + assert document["recorded_at"] == "2026-08-22T14:50:12.123456Z" + assert "query_term" not in document + assert "response" not in document + assert "person" not in document + expected_json = json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + assert packet.canonical_json() == expected_json + assert packet.evidence_digest() == sha256(expected_json.encode("utf-8")).hexdigest() + assert repr(packet) == "SemanticJobEvidenceEnvelope()" + + +@pytest.mark.parametrize( + ("field_name", "bad_value"), + [ + ("tenant_record_id", "00000000-0000-0000-0000-000000000000"), + ("tenant_record_id", "not-a-uuid"), + ("tenant_record_id", str(uuid4()).upper()), + ("job_analysis_reference", f"job_analysis:{uuid1()}"), + ("job_analysis_reference", f"person:{uuid4()}"), + ("job_analysis_reference", "job_analysis:" + "a" * 181), + ("ontology_request_reference", f"ontology_request:{uuid1()}"), + ("ontology_request_reference", "ontology_request:not-a-uuid"), + ("requesting_actor_reference", "staff:analyst"), + ("requesting_actor_reference", "actor:hr-analyst"), + ("reviewing_actor_reference", "actor:has space"), + ("query_term_digest", "A" * 64), + ("response_evidence_digest", "b" * 63), + ("source_catalog_digest", "not-a-digest"), + ("semantic_data_portal_revision", "0" * 40), + ("api_operation", "POST /search/semantic"), + ("api_operation", ""), + ("resolution_use_code", "automated_job_decision"), + ("evidence_version", 0), + ("evidence_version", 1_000_001), + ("evidence_version", True), + ("recorded_at", datetime(2026, 8, 22, 14, 50, 12)), + ], +) +def test_rejects_invalid_governance_evidence( + semantic_values: dict[str, object], field_name: str, bad_value: object +) -> None: + """Malformed, unsafe, or unreviewed evidence fails closed at construction.""" + candidate = semantic_values.copy() + candidate[field_name] = bad_value + with pytest.raises(ValueError): + SemanticJobEvidenceEnvelope(**candidate) + + +def test_rejects_same_requester_and_reviewer(semantic_values: dict[str, object]) -> None: + """One actor cannot self-review ontology evidence for Job Analysis.""" + candidate = semantic_values.copy() + candidate["reviewing_actor_reference"] = candidate["requesting_actor_reference"] + with pytest.raises(ValueError, match="must differ"): + SemanticJobEvidenceEnvelope(**candidate) + + +class ForgedText(str): + """Simulate caller text that lies during reviewed equality/hash operations.""" + + def __eq__(self, other: object) -> bool: + """Pretend every comparison is equal.""" + return True + + def __ne__(self, other: object) -> bool: + """Pretend every comparison is not unequal.""" + return False + + def __hash__(self) -> int: + """Pretend to hash like an approved use code.""" + return hash("job_analysis_source_evidence") + + +class ForgedInt(int): + """Simulate caller numeric evidence that lies during bounds checks.""" + + def __le__(self, other: object) -> bool: + """Forge less-than-or-equal comparisons.""" + return True + + def __ge__(self, other: object) -> bool: + """Forge greater-than-or-equal comparisons.""" + return True + + def __lt__(self, other: object) -> bool: + """Forge strict less-than comparisons.""" + return False + + def __gt__(self, other: object) -> bool: + """Forge strict greater-than comparisons.""" + return False + + +class ForgedDateTime(datetime): + """Represent caller-executable temporal behavior at the trust boundary.""" + + +def test_rejects_runtime_subclasses_before_governance_comparison( + semantic_values: dict[str, object], +) -> None: + """Caller-defined primitives cannot forge reviewed state or canonical evidence.""" + for field_name, bad_value in ( + ("resolution_use_code", ForgedText("automated_job_decision")), + ("api_operation", ForgedText("POST /search/semantic")), + ("evidence_version", ForgedInt(1)), + ("recorded_at", ForgedDateTime(2026, 8, 22, tzinfo=timezone.utc)), + ): + candidate = semantic_values.copy() + candidate[field_name] = bad_value + with pytest.raises(ValueError): + SemanticJobEvidenceEnvelope(**candidate) + + +def test_rejects_post_construction_rewrite(semantic_values: dict[str, object]) -> None: + """Valid-looking field replacement cannot rewrite already-issued evidence.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + object.__setattr__(packet, "response_evidence_digest", "d" * 64) + with pytest.raises(ValueError, match="changed after construction"): + packet.canonical_json() + + +def test_replace_cannot_reseal_changed_evidence(semantic_values: dict[str, object]) -> None: + """Dataclass replacement cannot reset the issuance seal and create new authority.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + with pytest.raises(ValueError, match="changed after construction"): + replace(packet, response_evidence_digest="d" * 64, _creation_seal=None) + + +def test_rejects_caller_supplied_seal_and_marker_rewrite( + semantic_values: dict[str, object], +) -> None: + """Private seal and issuance marker fields remain fail-closed under hostile access.""" + candidate = semantic_values.copy() + candidate["_creation_seal"] = "0" * 64 + with pytest.raises(ValueError, match="changed after construction"): + SemanticJobEvidenceEnvelope(**candidate) + + packet = SemanticJobEvidenceEnvelope(**semantic_values) + object.__setattr__(packet, "_issuance_marker", object()) + with pytest.raises(ValueError, match="changed after construction"): + packet.canonical_document() + + +def test_rejects_creation_seal_rewrite_even_when_payload_is_unchanged( + semantic_values: dict[str, object], +) -> None: + """The authoritative in-process seal cannot be replaced independently.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + object.__setattr__(packet, "_creation_seal", object()) + with pytest.raises(ValueError, match="changed after construction"): + packet.canonical_json() + + +def test_canonical_export_reuses_the_exact_integrity_checked_snapshot( + semantic_values: dict[str, object], monkeypatch: pytest.MonkeyPatch +) -> None: + """A mutation after the checked snapshot cannot leak different canonical evidence.""" + packet = SemanticJobEvidenceEnvelope(**semantic_values) + expected_json = packet.canonical_json() + original_payload = SemanticJobEvidenceEnvelope._payload + mutated = False + + def mutate_after_snapshot(self: SemanticJobEvidenceEnvelope) -> dict[str, object]: + """Mutate the live packet immediately after returning one payload snapshot.""" + nonlocal mutated + payload = original_payload(self) + if not mutated: + mutated = True + object.__setattr__(self, "response_evidence_digest", "d" * 64) + return payload + + monkeypatch.setattr(SemanticJobEvidenceEnvelope, "_payload", mutate_after_snapshot) + + assert packet.canonical_json() == expected_json + with pytest.raises(ValueError, match="changed after construction"): + packet.canonical_json() + + +def test_runtime_type_is_final() -> None: + """Subclasses cannot override derived trust state on the governed envelope.""" + with pytest.raises(TypeError, match="final"): + type("DerivedEnvelope", (SemanticJobEvidenceEnvelope,), {}) diff --git a/packages/semantic-job-evidence-adapter/tests/test_python_support_contract.py b/packages/semantic-job-evidence-adapter/tests/test_python_support_contract.py new file mode 100644 index 000000000..85d9879b9 --- /dev/null +++ b/packages/semantic-job-evidence-adapter/tests/test_python_support_contract.py @@ -0,0 +1,20 @@ +"""Executable compatibility contract for the semantic evidence package support range.""" + +from pathlib import Path +import tomllib + + +PACKAGE_ROOT = Path(__file__).resolve().parents[1] +REPOSITORY_ROOT = PACKAGE_ROOT.parents[1] +WORKFLOW_PATH = REPOSITORY_ROOT / ".github" / "workflows" / "semantic-job-evidence-adapter-quality.yml" + + +def test_declared_python_range_matches_the_hosted_compatibility_matrix() -> None: + """Bound public Python support to the minor versions exercised by hosted CI.""" + pyproject = tomllib.loads((PACKAGE_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + assert pyproject["project"]["requires-python"] == ">=3.12,<3.15" + + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + assert "matrix:" in workflow + assert 'python-version: ["3.12", "3.13", "3.14"]' in workflow + assert "python-version: ${{ matrix.python-version }}" in workflow diff --git a/tests/test_foundation_ci_dependency_hygiene.sh b/tests/test_foundation_ci_dependency_hygiene.sh index 6a6cb51a8..1e3c013c3 100644 --- a/tests/test_foundation_ci_dependency_hygiene.sh +++ b/tests/test_foundation_ci_dependency_hygiene.sh @@ -65,7 +65,7 @@ if [[ "${#package_lines[@]}" -ne 7 ]]; then fi for package_line in "${package_lines[@]}"; do - if [[ ! "${package_line}" =~ ^[A-Za-z0-9._-]+==[0-9][A-Za-z0-9._-]*[[:space:]]--hash=sha256:[0-9a-f]{64}$ ]]; then + if [[ ! "${package_line}" =~ ^[A-Za-z0-9._-]+==[0-9][A-Za-z0-9._-]*([[:space:]]--hash=sha256:[0-9a-f]{64})+$ ]]; then printf 'Unpinned or unhashed Foundation CI requirement: %s\n' "${package_line}" >&2 exit 1 fi @@ -77,3 +77,22 @@ for package_name in coverage iniconfig packaging pluggy Pygments pytest pytest-c exit 1 fi done + +coverage_line="$(printf '%s\n' "${package_lines[@]}" | grep -E '^coverage==')" +expected_coverage_hashes=( + "8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240" + "1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e" + "cda36d8e7bfd63b3e44e75163265429caa5d935b672b00f71bccc8c010518c64" +) +for expected_hash in "${expected_coverage_hashes[@]}"; do + if [[ "${coverage_line}" != *"--hash=sha256:${expected_hash}"* ]]; then + printf 'Foundation CI coverage requirement is missing reviewed artifact hash: %s\n' "${expected_hash}" >&2 + exit 1 + fi +done + +actual_coverage_hash_count="$(grep -o -- '--hash=sha256:[0-9a-f]\{64\}' <<<"${coverage_line}" | wc -l)" +if [[ "${actual_coverage_hash_count}" -ne "${#expected_coverage_hashes[@]}" ]]; then + printf 'Foundation CI coverage requirement must contain exactly the reviewed Python 3.12-3.14 artifact hashes.\n' >&2 + exit 1 +fi