diff --git a/.github/requirements/foundation-compatibility-test.txt b/.github/requirements/foundation-compatibility-test.txt new file mode 100644 index 000000000..0c83fb3ed --- /dev/null +++ b/.github/requirements/foundation-compatibility-test.txt @@ -0,0 +1,9 @@ +# Reviewed Foundation compatibility toolchain for CPython 3.12-3.13 on GitHub-hosted Ubuntu x86_64. +# Coverage wheel hashes are the reviewed CPython 3.12 and 3.13 manylinux x86_64 artifacts. +coverage==7.14.2 --hash=sha256:8b4910cce599cd2438f8da65f5ef199a70a1cdb6ab314926df78271ca5954240 --hash=sha256:1d9a1b5813d00ea6151f6ccf64d1fa16892771dfdda12ba87162d15ec4ea3e1e +iniconfig==2.3.0 --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 +packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e +pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 +Pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 +pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c +pytest-cov==7.1.0 --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 diff --git a/.github/workflows/foundation-ci.yml b/.github/workflows/foundation-ci.yml index 6b475d6f2..a2ef7aba6 100644 --- a/.github/workflows/foundation-ci.yml +++ b/.github/workflows/foundation-ci.yml @@ -33,7 +33,7 @@ jobs: 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 + - name: Set up primary Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" @@ -56,16 +56,19 @@ jobs: run: | python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt python -m pip check - - name: Run owned unit and service contracts once + - name: Run owned package contracts once + run: | + set -euo pipefail + for pyproject in packages/*/pyproject.toml; do + package_dir="${pyproject%/pyproject.toml}" + [[ -d "$package_dir/src" && -d "$package_dir/tests" ]] || continue + package_name="$(basename "$package_dir")" + PYTHONPATH="$package_dir/src" \ + COVERAGE_FILE="/tmp/orgmetra-${package_name}.coverage" \ + python -m pytest -c "$pyproject" "$package_dir/tests" + done + - name: Run owned service contracts once run: | - PYTHONPATH=packages/candidate-evidence/src COVERAGE_FILE=/tmp/orgmetra-candidate-evidence.coverage python -m pytest -c packages/candidate-evidence/pyproject.toml packages/candidate-evidence/tests - PYTHONPATH=packages/hris-kernel/src COVERAGE_FILE=/tmp/orgmetra-hris-kernel.coverage python -m pytest -c packages/hris-kernel/pyproject.toml packages/hris-kernel/tests - PYTHONPATH=packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-keyverse-adapter.coverage python -m pytest -c packages/keyverse-adapter/pyproject.toml packages/keyverse-adapter/tests - PYTHONPATH=packages/migration-adapter/src COVERAGE_FILE=/tmp/orgmetra-migration-adapter.coverage python -m pytest -c packages/migration-adapter/pyproject.toml packages/migration-adapter/tests - PYTHONPATH=packages/naruon-adapter/src COVERAGE_FILE=/tmp/orgmetra-naruon-adapter.coverage python -m pytest -c packages/naruon-adapter/pyproject.toml packages/naruon-adapter/tests - PYTHONPATH=packages/offer-approval/src COVERAGE_FILE=/tmp/orgmetra-offer-approval.coverage python -m pytest -c packages/offer-approval/pyproject.toml packages/offer-approval/tests - PYTHONPATH=packages/requisition-review/src COVERAGE_FILE=/tmp/orgmetra-requisition-review.coverage python -m pytest -c packages/requisition-review/pyproject.toml packages/requisition-review/tests - PYTHONPATH=packages/selection-review/src COVERAGE_FILE=/tmp/orgmetra-selection-review.coverage python -m pytest -c packages/selection-review/pyproject.toml packages/selection-review/tests PYTHONPATH=services/job-analysis-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-job-analysis-api.coverage python -m pytest -c services/job-analysis-api/pyproject.toml services/job-analysis-api/tests PYTHONPATH=services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src COVERAGE_FILE=/tmp/orgmetra-people-api.coverage python -m pytest -c services/people-api/pyproject.toml services/people-api/tests - name: Run PostgreSQL contracts in isolated containers @@ -116,6 +119,129 @@ jobs: docker rm --force "$container_name" >/dev/null trap - EXIT done + - name: Prove compatibility toolchain provenance + run: | + expected_sha256=cebb36181e8ac995a36d73a02a45094a204ff5adb3cbcdc0c9eccff309ac6aab + actual_sha256="$(sha256sum .github/requirements/foundation-compatibility-test.txt | awk '{print $1}')" + test "$actual_sha256" = "$expected_sha256" + - name: Set up Python 3.12 compatibility runtime + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + check-latest: false + - name: Install reviewed Python 3.12 compatibility toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-compatibility-test.txt + python -m pip check + - name: Execute every package that declares Python 3.12 supported + env: + ORGMETRA_PYTHON_MINOR: "3.12" + run: | + set -euo pipefail + executed=0 + for pyproject in packages/*/pyproject.toml; do + package_dir="${pyproject%/pyproject.toml}" + [[ -d "$package_dir/src" && -d "$package_dir/tests" ]] || continue + compatibility_decision="$( + python - "$pyproject" <<'PY' + import sys + import tomllib + from pathlib import Path + from packaging.specifiers import InvalidSpecifier, SpecifierSet + from packaging.version import Version + + pyproject = Path(sys.argv[1]) + project = tomllib.loads(pyproject.read_text(encoding="utf-8")).get("project", {}) + requires_python = project.get("requires-python") + if not isinstance(requires_python, str) or not requires_python.strip(): + raise SystemExit(f"{pyproject}: project.requires-python is required") + runtime = Version(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") + try: + supported = runtime in SpecifierSet(requires_python) + except InvalidSpecifier as exc: + raise SystemExit( + f"{pyproject}: invalid project.requires-python: {requires_python!r}" + ) from exc + print("supported" if supported else "unsupported") + PY + )" + if [[ "$compatibility_decision" == "unsupported" ]]; then + continue + fi + if [[ "$compatibility_decision" != "supported" ]]; then + printf 'Unexpected compatibility decision for %s: %s\n' "$pyproject" "$compatibility_decision" >&2 + exit 1 + fi + package_name="$(basename "$package_dir")" + python -m compileall -q "$package_dir/src" "$package_dir/tests" + PYTHONPATH="$package_dir/src" \ + COVERAGE_FILE="/tmp/orgmetra-${package_name}-${ORGMETRA_PYTHON_MINOR}.coverage" \ + python -m pytest -c "$pyproject" "$package_dir/tests" + executed=$((executed + 1)) + done + if [[ "$executed" -eq 0 ]]; then + printf 'No owned package declared Python %s support; compatibility evidence would be vacuous.\n' "$ORGMETRA_PYTHON_MINOR" >&2 + exit 1 + fi + - name: Set up Python 3.13 compatibility runtime + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + check-latest: false + - name: Install reviewed Python 3.13 compatibility toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-compatibility-test.txt + python -m pip check + - name: Execute every package that declares Python 3.13 supported + env: + ORGMETRA_PYTHON_MINOR: "3.13" + run: | + set -euo pipefail + executed=0 + for pyproject in packages/*/pyproject.toml; do + package_dir="${pyproject%/pyproject.toml}" + [[ -d "$package_dir/src" && -d "$package_dir/tests" ]] || continue + compatibility_decision="$( + python - "$pyproject" <<'PY' + import sys + import tomllib + from pathlib import Path + from packaging.specifiers import InvalidSpecifier, SpecifierSet + from packaging.version import Version + + pyproject = Path(sys.argv[1]) + project = tomllib.loads(pyproject.read_text(encoding="utf-8")).get("project", {}) + requires_python = project.get("requires-python") + if not isinstance(requires_python, str) or not requires_python.strip(): + raise SystemExit(f"{pyproject}: project.requires-python is required") + runtime = Version(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") + try: + supported = runtime in SpecifierSet(requires_python) + except InvalidSpecifier as exc: + raise SystemExit( + f"{pyproject}: invalid project.requires-python: {requires_python!r}" + ) from exc + print("supported" if supported else "unsupported") + PY + )" + if [[ "$compatibility_decision" == "unsupported" ]]; then + continue + fi + if [[ "$compatibility_decision" != "supported" ]]; then + printf 'Unexpected compatibility decision for %s: %s\n' "$pyproject" "$compatibility_decision" >&2 + exit 1 + fi + package_name="$(basename "$package_dir")" + python -m compileall -q "$package_dir/src" "$package_dir/tests" + PYTHONPATH="$package_dir/src" \ + COVERAGE_FILE="/tmp/orgmetra-${package_name}-${ORGMETRA_PYTHON_MINOR}.coverage" \ + python -m pytest -c "$pyproject" "$package_dir/tests" + executed=$((executed + 1)) + done + if [[ "$executed" -eq 0 ]]; then + printf 'No owned package declared Python %s support; compatibility evidence would be vacuous.\n' "$ORGMETRA_PYTHON_MINOR" >&2 + exit 1 + fi - name: Print exact manifest repair data if: failure() run: python tests/validate_repository.py --print-manifest diff --git a/docs/traceability/foundation-python-compatibility.md b/docs/traceability/foundation-python-compatibility.md new file mode 100644 index 000000000..e801189d9 --- /dev/null +++ b/docs/traceability/foundation-python-compatibility.md @@ -0,0 +1,102 @@ +# Foundation Python compatibility traceability + +## Authority + +- Repository: `ContextualWisdomLab/Orgmetra` +- Protected parent snapshot: `develop@eb9757f8649aaad026a9865508d9aad50c1a7a4f` +- Canonical repair issue: #258 +- Maturity: `implemented_on_active_pr` + +## Finding + +Protected Foundation CI executes repository-owned Python quality on CPython 3.14, while several merged +packages declare `requires-python = ">=3.12"`. Structured Interview Plan #40 also carried real CPython +3.12/3.13 execution in its historical package-local workflow. Protected #161 correctly retired package-local +quality workflows, but adopting that deletion without replacement evidence would weaken the declared runtime +contract. + +Three follow-up reviews found separate fail-open defects in the first compatibility implementation. Metadata +parser failures could be reclassified as unsupported-package skips; PEP 440 was evaluated against a fabricated +`major.minor.0` rather than the executed interpreter patch; and an owned `packages/*/pyproject.toml` could +escape package acceptance if either `src/` or `tests/` disappeared. Those paths now fail closed. + +Exact-head Foundation run `34050838082` exposed a fourth defect in the implementation shape. Python 3.12 and +3.13 were introduced as a second matrix job, but protected repository policy intentionally constrains +Foundation to one job to avoid recreating the previous matrix-driven Actions admission pressure. The two +compatibility jobs themselves passed, while the canonical runner/queue contract failed before repository +validation. Compatibility evidence is therefore kept, but it executes sequentially inside the existing +`quality` job rather than widening the job graph. + +Exact head `79e8757515673144b68687517360cf493e93ccb8` then produced a complete Foundation GREEN in run +`34053906336`: the one `Repository quality` job passed exact checkout, runner-image proof, Foundation validation, +dependency hygiene, primary package/service/PostgreSQL contracts, Python 3.12 compatibility, Python 3.13 +compatibility, and clean-checkout proof. + +A fifth review found a provenance gap despite that GREEN. The compatibility requirement file was hash-locked at +the package line level, but the file itself was not part of the canonical Foundation manifest inventory. A +reviewed dependency set could therefore change without changing the manifest unless another tracked artifact +bound it. The active successor now makes that binding explicit: the manifest-sealed Foundation workflow verifies +SHA-256 `cebb36181e8ac995a36d73a02a45094a204ff5adb3cbcdc0c9eccff309ac6aab` for +`.github/requirements/foundation-compatibility-test.txt` before either compatibility runtime can install it. +The workflow itself is resealed in `manifest.json`, so the dependency input is transitively integrity-bound +without adding a second quality owner or mutable external source. + +A package-local workflow is not restored. The capability remains owned by +`.github/workflows/foundation-ci.yml`. + +## Decision + +Foundation keeps one `quality` job on pinned `ubuntu-24.04`. That job runs the primary CPython 3.14 package, +service, and PostgreSQL contracts, proves the reviewed compatibility-toolchain file digest, then switches to +CPython 3.12 and CPython 3.13 in sequence with `actions/setup-python`. Each compatibility runtime installs the +same reviewed hash-locked compatibility toolchain and executes every package whose `project.requires-python` +includes the actual interpreter patch. No compatibility matrix or second Foundation job is permitted. + +Primary and compatibility package execution discover `packages/*/pyproject.toml` rather than naming packages +in workflow logic. For each compatibility runtime, Foundation reads `project.requires-python` with `tomllib` +and `packaging.specifiers.SpecifierSet`, evaluates it against +`sys.version_info.major.minor.micro`, and permits a skip only when a syntactically valid constraint excludes +that exact executed release. Missing, blank, non-string, malformed TOML, invalid specifiers, or parser failures +terminate the compatibility execution. Every selected package is compiled and runs its own pytest +configuration, preserving its exact statement and branch coverage gate. + +The repository-quality hygiene contract independently enumerates every `packages/*/pyproject.toml` and +requires both `src/` and `tests/`. Its self-regression constructs missing-`src` and missing-`tests` fixtures and +requires both to fail closed. Non-Python directories without a `pyproject.toml` remain outside this contract. + +The primary CPython 3.14 toolchain remains bound by `.github/requirements/foundation-test.txt`. CPython +3.12/3.13 use `.github/requirements/foundation-compatibility-test.txt`, installed with +`--require-hashes --no-deps --only-binary=:all:` and reviewed wheel hashes for both compatibility runtimes. The +compatibility file's complete bytes are additionally pinned by the manifest-sealed Foundation workflow before +installation. + +## Invariants + +- `.github/workflows/foundation-ci.yml` remains the only repository quality owner. +- Foundation expands to exactly one repository-owned job; compatibility must not add a matrix or second job. +- The Foundation job uses `ubuntu-24.04`; `ubuntu-latest` remains rejected by executable regression. +- CPython 3.12 and 3.13 compatibility executes sequentially after the primary CPython 3.14 quality contracts. +- The compatibility requirement file must match the reviewed SHA-256 before either compatibility install. +- Compatibility discovery is package-neutral and contains no Interview Plan or Selection Monitoring switch. +- Every discovered owned Python package has both `src/` and `tests/`; incomplete layout is a Foundation + failure rather than an accepted skip. +- Invalid or missing `project.requires-python` metadata fails closed; only a valid constraint excluding the + actual executed interpreter patch may skip a package. +- Patch-sensitive PEP 440 constraints are evaluated against the executed release, never a fabricated `.0`. +- Each compatibility runtime fails if no owned package actually declares that runtime supported. +- Repository-local packages are not installed into the checkout as an implicit dependency workaround. +- Package-local quality workflows retired by protected #161 remain retired. + +## Scope boundary + +This change proves declared compatibility for Python packages under `packages/`. The two HTTP services +currently declare Python 3.11 support but remain a separate #260 service-runtime contract. No Python 3.11 +service-compatibility claim is made by #258 or #40. + +## Adoption + +After this capability is integrated on protected `develop`, #40 can discard +`.github/workflows/interview-plan-quality.yml`, retain its `requires-python = ">=3.12"` declaration and +package tests, and receive Python 3.12/3.13/3.14 evidence through Foundation without adding a package name to +shared workflow logic. #42 should likewise adopt the protected generic package execution rather than copying +its mutable shared-dispatcher implementation. diff --git a/manifest.json b/manifest.json index f7b6cf55e..558d4d1c7 100644 --- a/manifest.json +++ b/manifest.json @@ -5,9 +5,9 @@ "files": [ { "path": ".github/workflows/foundation-ci.yml", - "sha256": "b6a4365936b66803a8112f034c77d53d33301a7a798ed4f68746a4f2d8b081d7", - "bytes": 6651, - "lines": 125 + "sha256": "27fd3f2a69181e6a31b49ab2a3d394bfc8dca2b8ef5e9ce39324273f7962773d", + "bytes": 11708, + "lines": 251 }, { "path": ".gitignore", @@ -472,4 +472,4 @@ "lines": 637 } ] -} +} \ No newline at end of file diff --git a/tests/test_foundation_ci_dependency_hygiene.sh b/tests/test_foundation_ci_dependency_hygiene.sh index 2c0f5087f..bd6185129 100644 --- a/tests/test_foundation_ci_dependency_hygiene.sh +++ b/tests/test_foundation_ci_dependency_hygiene.sh @@ -4,24 +4,69 @@ set -euo pipefail repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" workflow_path="${repository_root}/.github/workflows/foundation-ci.yml" requirements_path="${repository_root}/.github/requirements/foundation-test.txt" +compatibility_requirements_path="${repository_root}/.github/requirements/foundation-compatibility-test.txt" expected_install="python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt" +expected_compatibility_install="python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-compatibility-test.txt" expected_default_pr_target=$' pull_request:\n branches:\n - develop\n' -expected_pythonpaths=( - "packages/candidate-evidence/src" - "packages/hris-kernel/src" - "packages/keyverse-adapter/src" - "packages/migration-adapter/src" - "packages/naruon-adapter/src" - "packages/offer-approval/src" - "packages/requisition-review/src" - "packages/selection-review/src" +expected_package_discovery='for pyproject in packages/*/pyproject.toml; do' +expected_service_pythonpaths=( "services/job-analysis-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src" "services/people-api/src:packages/hris-kernel/src:packages/keyverse-adapter/src" ) +validate_owned_package_layout() { + local root="$1" + local pyproject + local package_dir + local discovered=0 + + for pyproject in "${root}"/packages/*/pyproject.toml; do + [[ -f "${pyproject}" ]] || continue + discovered=$((discovered + 1)) + package_dir="${pyproject%/pyproject.toml}" + if [[ ! -d "${package_dir}/src" || ! -d "${package_dir}/tests" ]]; then + printf 'Owned Python package must provide both src and tests directories: %s\n' "${package_dir}" >&2 + return 1 + fi + done + + if [[ "${discovered}" -eq 0 ]]; then + printf 'Foundation CI package discovery found no owned Python packages.\n' >&2 + return 1 + fi +} + +validate_owned_package_layout "${repository_root}" + +layout_fixture_root="$(mktemp -d)" +trap 'rm -rf "${layout_fixture_root}"' EXIT +for missing_directory in src tests; do + fixture_package="${layout_fixture_root}/packages/missing-${missing_directory}" + mkdir -p "${fixture_package}/src" "${fixture_package}/tests" + rm -rf "${fixture_package}/${missing_directory}" + cat >"${fixture_package}/pyproject.toml" <<'EOF' +[project] +name = "orgmetra-foundation-layout-fixture" +version = "0.0.0" +requires-python = ">=3.12" +EOF + if validate_owned_package_layout "${layout_fixture_root}" >/dev/null 2>&1; then + printf 'Foundation package discovery must fail closed when an owned package omits %s.\n' "${missing_directory}" >&2 + exit 1 + fi + rm -rf "${fixture_package}" +done +rm -rf "${layout_fixture_root}" +trap - EXIT + if ! grep -Fq -- "${expected_install}" "${workflow_path}"; then - printf 'Foundation CI must install only the hash-locked test toolchain.\n' >&2 + printf 'Foundation CI must install only the hash-locked primary test toolchain.\n' >&2 + exit 1 +fi + +if [[ "$(grep -Fc -- "${expected_compatibility_install}" "${workflow_path}")" -ne 2 ]]; then + printf 'Foundation CI must install the hash-locked compatibility toolchain once for each declared compatibility runtime.\n' >&2 exit 1 fi @@ -30,9 +75,73 @@ if grep -Eq -- 'python -m pip install .*packages/' "${workflow_path}"; then exit 1 fi -for expected_pythonpath in "${expected_pythonpaths[@]}"; do +if grep -Fq -- 'runs-on: ubuntu-latest' "${workflow_path}"; then + printf 'Foundation CI must pin every GitHub-hosted job to ubuntu-24.04.\n' >&2 + exit 1 +fi + +if [[ "$(grep -Fc -- 'runs-on: ubuntu-24.04' "${workflow_path}")" -ne 1 ]]; then + printf 'Foundation CI must preserve one repository-owned job on ubuntu-24.04.\n' >&2 + exit 1 +fi + +if grep -Fq -- 'matrix:' "${workflow_path}" || grep -Fq -- 'python-compatibility:' "${workflow_path}"; then + printf 'Foundation compatibility must not recreate matrix-driven or second-job admission pressure.\n' >&2 + exit 1 +fi + +for python_minor in 3.12 3.13; do + if [[ "$(grep -Fc -- "python-version: \"${python_minor}\"" "${workflow_path}")" -ne 1 ]]; then + printf 'Foundation CI must set up Python %s exactly once for sequential compatibility evidence.\n' "${python_minor}" >&2 + exit 1 + fi + if [[ "$(grep -Fc -- "ORGMETRA_PYTHON_MINOR: \"${python_minor}\"" "${workflow_path}")" -ne 1 ]]; then + printf 'Foundation CI must bind compatibility execution to Python %s.\n' "${python_minor}" >&2 + exit 1 + fi +done + +if [[ "$(grep -Fc -- "${expected_package_discovery}" "${workflow_path}")" -ne 3 ]]; then + printf 'Foundation CI must use package-neutral discovery in primary, Python 3.12, and Python 3.13 execution.\n' >&2 + exit 1 +fi + +if grep -Eq -- 'packages/(interview-plan|selection-monitoring)' "${workflow_path}"; then + printf 'Foundation CI must not encode package-name switchboards for compatibility adoption.\n' >&2 + exit 1 +fi + +if grep -Fq -- 'if ! python - "$pyproject"' "${workflow_path}"; then + printf 'Foundation compatibility selection must not convert metadata/parser failures into unsupported-package skips.\n' >&2 + exit 1 +fi + +if [[ "$(grep -Fc -- 'compatibility_decision="$(' "${workflow_path}")" -ne 2 ]] || + [[ "$(grep -Fc -- 'from packaging.specifiers import InvalidSpecifier, SpecifierSet' "${workflow_path}")" -ne 2 ]] || + [[ "$(grep -Fc -- 'Unexpected compatibility decision for %s: %s' "${workflow_path}")" -ne 2 ]]; then + printf 'Foundation compatibility selection must fail closed in both sequential compatibility executions.\n' >&2 + exit 1 +fi + +expected_exact_runtime='runtime = Version(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}")' +if [[ "$(grep -Fc -- "${expected_exact_runtime}" "${workflow_path}")" -ne 2 ]]; then + printf 'Foundation compatibility selection must evaluate both runtimes against the exact executed interpreter patch.\n' >&2 + exit 1 +fi + +if grep -Fq -- 'runtime = Version(f"{sys.version_info.major}.{sys.version_info.minor}.0")' "${workflow_path}"; then + printf 'Foundation compatibility selection must not fabricate a .0 patch for requires-python checks.\n' >&2 + exit 1 +fi + +if [[ "$(grep -Fc -- 'No owned package declared Python %s support; compatibility evidence would be vacuous.' "${workflow_path}")" -ne 2 ]]; then + printf 'Foundation compatibility must fail non-vacuously for both sequential runtimes.\n' >&2 + exit 1 +fi + +for expected_pythonpath in "${expected_service_pythonpaths[@]}"; do if ! grep -Fq -- "PYTHONPATH=${expected_pythonpath} COVERAGE_FILE=" "${workflow_path}"; then - printf 'Foundation CI must import repository-local src tree directly: %s\n' "${expected_pythonpath}" >&2 + printf 'Foundation CI must preserve reviewed service source-tree execution: %s\n' "${expected_pythonpath}" >&2 exit 1 fi done @@ -84,9 +193,41 @@ for package_line in "${package_lines[@]}"; do fi done +if [[ ! -f "${compatibility_requirements_path}" ]]; then + printf 'Hash-locked Foundation compatibility requirements are missing.\n' >&2 + exit 1 +fi + +mapfile -t compatibility_package_lines < <( + grep -Ev '^[[:space:]]*(#|$)' "${compatibility_requirements_path}" +) +if [[ "${#compatibility_package_lines[@]}" -ne 7 ]]; then + printf 'Foundation compatibility requirements must contain the seven reviewed test packages.\n' >&2 + exit 1 +fi + +for package_line in "${compatibility_package_lines[@]}"; do + 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 compatibility requirement: %s\n' "${package_line}" >&2 + exit 1 + fi +done + for package_name in coverage iniconfig packaging pluggy Pygments pytest pytest-cov; do if ! printf '%s\n' "${package_lines[@]}" | grep -Eq "^${package_name}=="; then printf 'Foundation CI requirement is missing: %s\n' "${package_name}" >&2 exit 1 fi + if ! printf '%s\n' "${compatibility_package_lines[@]}" | grep -Eq "^${package_name}=="; then + printf 'Foundation compatibility requirement is missing: %s\n' "${package_name}" >&2 + exit 1 + fi done + +coverage_compatibility_line="$( + printf '%s\n' "${compatibility_package_lines[@]}" | grep '^coverage==' +)" +if [[ "$(grep -o -- '--hash=sha256:[0-9a-f]\{64\}' <<<"${coverage_compatibility_line}" | wc -l | tr -d ' ')" -ne 2 ]]; then + printf 'Coverage compatibility requirement must bind reviewed CPython 3.12 and 3.13 wheels.\n' >&2 + exit 1 +fi