diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 2a170fa8a..cda5e7f62 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -90,13 +90,13 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: category: "/language:${{ matrix.language }}" upload: false @@ -197,13 +197,13 @@ jobs: ref: ${{ format('refs/pull/{0}/merge', github.event.pull_request.number) }} - name: Initialize CodeQL - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5 with: category: "/language:${{ matrix.language }}-merge" upload: false diff --git a/.github/workflows/opencode-coverage-diagnostics-ci.yml b/.github/workflows/opencode-coverage-diagnostics-ci.yml new file mode 100644 index 000000000..3206c46ec --- /dev/null +++ b/.github/workflows/opencode-coverage-diagnostics-ci.yml @@ -0,0 +1,242 @@ +name: OpenCode Coverage Diagnostics CI + +on: + pull_request: + branches: [main] + paths: + - "scripts/ci/coverage_failure_summary.py" + - "scripts/ci/javascript_coverage_gate.py" + - "scripts/ci/materialize_base_javascript_packages.py" + - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/sanitize_github_output_summary.py" + - "scripts/ci/strix_model_utils.sh" + - "tests/test_javascript_coverage_gate.py" + - "tests/test_javascript_coverage_gate_type_only.py" + - "tests/test_materialize_base_javascript_packages.py" + - "tests/test_materialize_base_python_requirements.py" + - "tests/test_control_plane_branch_writer_absence.py" + - "tests/test_coverage_materializer_failure_diagnostics.py" + - "tests/test_coverage_native_fuzz_lock_boundary.py" + - "tests/conftest.py" + - "tests/test_materialize_uv_export_hash_contract.py" + - "tests/test_trusted_uv_download_contract.py" + - "tests/test_trusted_uv_materializer_quality_workflow_contract.py" + - "tests/test_trusted_uv_portability_and_streaming.py" + - "tests/test_uv_export_isolation_contract.py" + - "tests/test_uv_redirect_and_coverage_contract.py" + - "tests/test_uv_redirect_boundary.py" + - "tests/test_uv_workspace_fail_closed.py" + - "tests/test_sanitize_github_output_summary.py" + - "tests/test_strix_dependency_security_floor.py" + - "tests/test_strix_model_utils_source_dirs.py" + - "requirements-opencode-python310-ci-hashes.txt" + - "requirements-opencode-review-ci-hashes.txt" + - "requirements-strix-ci.txt" + - "requirements-strix-ci-hashes.txt" + - "pyproject.toml" + - ".github/workflows/opencode-coverage-diagnostics-ci.yml" + push: + branches: [main] + paths: + - "scripts/ci/coverage_failure_summary.py" + - "scripts/ci/javascript_coverage_gate.py" + - "scripts/ci/materialize_base_javascript_packages.py" + - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/sanitize_github_output_summary.py" + - "scripts/ci/strix_model_utils.sh" + - "tests/test_javascript_coverage_gate.py" + - "tests/test_javascript_coverage_gate_type_only.py" + - "tests/test_materialize_base_javascript_packages.py" + - "tests/test_materialize_base_python_requirements.py" + - "tests/test_control_plane_branch_writer_absence.py" + - "tests/test_coverage_materializer_failure_diagnostics.py" + - "tests/test_coverage_native_fuzz_lock_boundary.py" + - "tests/conftest.py" + - "tests/test_materialize_uv_export_hash_contract.py" + - "tests/test_trusted_uv_download_contract.py" + - "tests/test_trusted_uv_materializer_quality_workflow_contract.py" + - "tests/test_trusted_uv_portability_and_streaming.py" + - "tests/test_uv_export_isolation_contract.py" + - "tests/test_uv_redirect_and_coverage_contract.py" + - "tests/test_uv_redirect_boundary.py" + - "tests/test_uv_workspace_fail_closed.py" + - "tests/test_sanitize_github_output_summary.py" + - "tests/test_strix_dependency_security_floor.py" + - "tests/test_strix_model_utils_source_dirs.py" + - "requirements-opencode-python310-ci-hashes.txt" + - "requirements-opencode-review-ci-hashes.txt" + - "requirements-strix-ci.txt" + - "requirements-strix-ci-hashes.txt" + - "pyproject.toml" + - ".github/workflows/opencode-coverage-diagnostics-ci.yml" + +concurrency: + group: opencode-coverage-diagnostics-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + minimum-python-contract: + name: Python 3.10 runtime contract + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Set up minimum supported Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + + - name: Install hash-locked Python 3.10 compatibility dependency + run: >- + python -m pip install --disable-pip-version-check --require-hashes + --only-binary=:all: + -r requirements-opencode-python310-ci-hashes.txt + + - name: Compile production modules on Python 3.10 + run: | + python -m compileall -q \ + scripts/ci/coverage_failure_summary.py \ + scripts/ci/javascript_coverage_gate.py \ + scripts/ci/materialize_base_javascript_packages.py \ + scripts/ci/materialize_base_python_requirements.py \ + scripts/ci/sanitize_github_output_summary.py + + - name: Exercise exact failure evidence on Python 3.10 + run: | + python - <<'PY' + import os + import pathlib + import tempfile + + from scripts.ci import materialize_base_javascript_packages as javascript_materializer + from scripts.ci import materialize_base_python_requirements as python_materializer + + with tempfile.TemporaryDirectory() as directory: + output = pathlib.Path(directory) / "github-output" + os.environ["GITHUB_OUTPUT"] = str(output) + exact_reason = ( + "current-head npm lock package-lock.json package " + "apps/desktop/node_modules/@types/react-dom must pin a registry " + "tarball and SHA-512 integrity" + ) + javascript_materializer._publish_coverage_failure_summary( + "Base JavaScript package lock materialization", + ValueError(exact_reason), + "Repair the lock and rerun coverage-evidence.", + ) + python_materializer._publish_coverage_failure_summary( + "Base Python lock materialization", + OSError("fixture \nCWL_COVERAGE_SUMMARY_EOF"), + "Repair the trusted lock and rerun coverage-evidence.", + ) + published = output.read_text(encoding="utf-8") + assert f"ValueError: {exact_reason}" in published + assert "OSError: fixture <unsafe> CWL_COVERAGE_SUMMARY_END" in published + assert published.count("coverage_summary<- + python -m pip install --disable-pip-version-check --require-hashes + -r requirements-opencode-review-ci-hashes.txt + + - name: Run diagnostics and lock contracts with full branch coverage + run: | + python -m pytest \ + tests/test_javascript_coverage_gate.py \ + tests/test_javascript_coverage_gate_type_only.py \ + tests/test_materialize_base_javascript_packages.py \ + tests/test_materialize_base_python_requirements.py \ + tests/test_control_plane_branch_writer_absence.py \ + tests/test_coverage_materializer_failure_diagnostics.py \ + tests/test_coverage_native_fuzz_lock_boundary.py \ + tests/test_materialize_uv_export_hash_contract.py \ + tests/test_trusted_uv_download_contract.py \ + tests/test_trusted_uv_materializer_quality_workflow_contract.py \ + tests/test_trusted_uv_portability_and_streaming.py \ + tests/test_uv_export_isolation_contract.py \ + tests/test_uv_redirect_and_coverage_contract.py \ + tests/test_uv_redirect_boundary.py \ + tests/test_uv_workspace_fail_closed.py \ + tests/test_sanitize_github_output_summary.py \ + tests/test_strix_dependency_security_floor.py \ + tests/test_strix_model_utils_source_dirs.py \ + --cov=scripts.ci.coverage_failure_summary \ + --cov=scripts.ci.javascript_coverage_gate \ + --cov=scripts.ci.materialize_base_javascript_packages \ + --cov=scripts.ci.materialize_base_python_requirements \ + --cov=scripts.ci.sanitize_github_output_summary \ + --cov-branch \ + --cov-fail-under=100 \ + -q + + - name: Enforce complete production docstrings + run: | + python -m interrogate \ + --fail-under 100 \ + scripts/ci/coverage_failure_summary.py \ + scripts/ci/javascript_coverage_gate.py \ + scripts/ci/materialize_base_javascript_packages.py \ + scripts/ci/materialize_base_python_requirements.py \ + scripts/ci/sanitize_github_output_summary.py + + - name: Compile changed Python surfaces + run: | + python -m compileall -q \ + scripts/ci/coverage_failure_summary.py \ + scripts/ci/javascript_coverage_gate.py \ + scripts/ci/materialize_base_javascript_packages.py \ + scripts/ci/materialize_base_python_requirements.py \ + scripts/ci/sanitize_github_output_summary.py \ + tests/test_javascript_coverage_gate.py \ + tests/test_javascript_coverage_gate_type_only.py \ + tests/test_control_plane_branch_writer_absence.py \ + tests/test_coverage_materializer_failure_diagnostics.py \ + tests/test_coverage_native_fuzz_lock_boundary.py \ + tests/test_materialize_uv_export_hash_contract.py \ + tests/test_trusted_uv_download_contract.py \ + tests/test_trusted_uv_materializer_quality_workflow_contract.py \ + tests/test_trusted_uv_portability_and_streaming.py \ + tests/test_uv_export_isolation_contract.py \ + tests/test_uv_redirect_and_coverage_contract.py \ + tests/test_uv_redirect_boundary.py \ + tests/test_uv_workspace_fail_closed.py \ + tests/test_sanitize_github_output_summary.py \ + tests/test_strix_dependency_security_floor.py \ + tests/test_strix_model_utils_source_dirs.py diff --git a/.github/workflows/opencode-coverage-toolchain-quality-ci.yml b/.github/workflows/opencode-coverage-toolchain-quality-ci.yml new file mode 100644 index 000000000..fa67d1ed1 --- /dev/null +++ b/.github/workflows/opencode-coverage-toolchain-quality-ci.yml @@ -0,0 +1,144 @@ +name: OpenCode Coverage Toolchain Quality + +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - ready_for_review + paths: + - ".github/workflows/opencode-review-dispatch.yml" + - ".github/workflows/opencode-coverage-toolchain-quality-ci.yml" + - "tests/test_opencode_llvm_coverage_current_main.py" + - "tests/test_opencode_rust_coverage_toolchain_contract.py" + - "docs/doctoring/opencode-llvm-coverage-toolchain.md" + - "docs/doctoring/rust-llvm-coverage-toolchain.md" + - "CHANGELOG.md" + - "requirements-opencode-review-ci-hashes.txt" + - "pyproject.toml" + +permissions: + contents: read + +concurrency: + group: opencode-coverage-toolchain-quality-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + exact-head-contract: + name: Exact-head toolchain contract + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact pull-request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Refuse stale or merge-tree evidence + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" + test -z "$(git status --short)" + + - name: Execute dependency-free coverage-toolchain contracts + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python3 - <<'PY' + import importlib.util + from pathlib import Path + + contract_paths = ( + Path("tests/test_opencode_llvm_coverage_current_main.py"), + Path("tests/test_opencode_rust_coverage_toolchain_contract.py"), + ) + for test_path in contract_paths: + module_name = f"opencode_toolchain_contract_{test_path.stem}" + spec = importlib.util.spec_from_file_location(module_name, test_path) + if spec is None or spec.loader is None: + raise SystemExit( + f"unable to load OpenCode toolchain contract: {test_path}" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + tests = sorted( + name + for name in vars(module) + if name.startswith("test_") and callable(getattr(module, name)) + ) + if not tests: + raise SystemExit( + f"no OpenCode toolchain contract tests discovered: {test_path}" + ) + for test_name in tests: + getattr(module, test_name)() + print(f"PASS {test_path}:{test_name}") + PY + python3 -m compileall -q \ + tests/test_opencode_llvm_coverage_current_main.py \ + tests/test_opencode_rust_coverage_toolchain_contract.py + git diff --check + test -z "$(git status --short)" + + full-repository-quality: + name: Full repository test, coverage, and docstring gate + needs: exact-head-contract + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout exact pull-request head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Refuse stale or merge-tree evidence + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}" + test -z "$(git status --short)" + + - name: Set up current stable Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + cache: pip + cache-dependency-path: requirements-opencode-review-ci-hashes.txt + + - name: Install hash-locked repository quality tooling + shell: bash --noprofile --norc -e -o pipefail {0} + run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt + + - name: Run complete central test and branch coverage gate + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python -m coverage erase + python -m coverage run -m pytest tests -q + python -m coverage report + + - name: Enforce complete production docstrings + shell: bash --noprofile --norc -e -o pipefail {0} + run: python -m interrogate --fail-under 100 scripts/ci + + - name: Compile production and every repository test + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + python -m compileall -q scripts/ci tests + git diff --check diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 83f6830d5..b17cf3775 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -652,11 +652,15 @@ jobs: r-base \ r-cran-covr \ r-cran-testthat \ + llvm-19 \ rustc \ util-linux \ vulkan-tools \ xz-utils \ && rm -rf /var/lib/apt/lists/* + ENV LLVM_COV=/usr/bin/llvm-cov-19 + ENV LLVM_PROFDATA=/usr/bin/llvm-profdata-19 + RUN test -x "$LLVM_COV" && test -x "$LLVM_PROFDATA" RUN curl --proto '=https' --tlsv1.2 -fsSLo /tmp/node-linux-x64.tar.xz \ https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.xz \ && echo '55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742 /tmp/node-linux-x64.tar.xz' | sha256sum -c - \ diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index e5475be1b..3d6a26cce 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -231,9 +231,9 @@ jobs: EOF jq -n --arg workspace "$TARGET_WORKSPACE" '{ "$schema": "https://opencode.ai/config.json", - "model": "github-models/openai/gpt-5", - "small_model": "github-models/deepseek/deepseek-v3-0324", - "enabled_providers": ["github-models"], + "model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5", + "small_model": "nvidia-nim/meta/llama-3.3-70b-instruct", + "enabled_providers": ["nvidia-nim"], "permission": { "edit": "allow", "bash": "deny", @@ -269,37 +269,28 @@ jobs: } }, "provider": { - "github-models": { + "nvidia-nim": { "npm": "@ai-sdk/openai-compatible", - "name": "GitHub Models", + "name": "NVIDIA NIM", "options": { - "baseURL": "https://models.github.ai/inference", - "apiKey": "{env:STRIX_GITHUB_MODELS_TOKEN}" + "baseURL": "https://integrate.api.nvidia.com/v1", + "apiKey": "{env:NVIDIA_API_KEY}" }, "models": { - "openai/gpt-5": { - "name": "OpenAI GPT-5", + "nvidia/llama-3.3-nemotron-super-49b-v1.5": { + "name": "NVIDIA Llama 3.3 Nemotron Super 49B v1.5", "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, "limit": { - "context": 200000, - "output": 100000 + "context": 131072, + "output": 8192 } }, - "deepseek/deepseek-v3-0324": { - "name": "DeepSeek V3 0324", + "meta/llama-3.3-70b-instruct": { + "name": "Meta Llama 3.3 70B Instruct (NIM)", "tool_call": true, "limit": { - "context": 128000, - "output": 4096 + "context": 131072, + "output": 8192 } } } @@ -310,16 +301,20 @@ jobs: - name: Run OpenCode review autofix if: env.RESOLVE_CONFLICT != 'true' env: - STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} - MODEL: github-models/openai/gpt-5 - USE_GITHUB_TOKEN: "true" + MODEL: nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 + USE_GITHUB_TOKEN: "false" SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_AUTOFIX_WORKDIR: ${{ runner.temp }}/opencode-autofix-project run: | set -euo pipefail + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::OpenCode PR autofix requires NVIDIA_NIM_API_KEY." + exit 1 + fi prompt_file="${RUNNER_TEMP}/opencode-autofix-prompt.md" allowed_paths_context="$( awk ' @@ -446,17 +441,21 @@ jobs: - name: Merge base branch and resolve conflicts with OpenCode if: env.RESOLVE_CONFLICT == 'true' env: - STRIX_GITHUB_MODELS_TOKEN: ${{ secrets.STRIX_GITHUB_MODELS_TOKEN || github.token }} + NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }} GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }} - MODEL: github-models/openai/gpt-5 - USE_GITHUB_TOKEN: "true" + MODEL: nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 + USE_GITHUB_TOKEN: "false" SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" OPENCODE_AUTOFIX_WORKDIR: ${{ runner.temp }}/opencode-autofix-project run: | set -euo pipefail + if [ -z "${NVIDIA_API_KEY:-}" ]; then + echo "::error::OpenCode PR conflict resolution requires NVIDIA_NIM_API_KEY." + exit 1 + fi cd "$TARGET_WORKSPACE" # Merge the base branch into the detached head. A clean merge stays diff --git a/CHANGELOG.md b/CHANGELOG.md index e601de81b..e6e1fccbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,31 @@ # Changelog -All notable changes to the organization automation repository are documented in -this file. The format follows Keep a Changelog, and versioned releases follow -Semantic Versioning where the repository publishes a release. +All notable changes to the ContextualWisdomLab central GitHub control plane are documented in this file. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioned releases follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added +- Added a permanent test-first Rust coverage-toolchain contract requiring Debian LLVM 19, explicit versioned `LLVM_COV` and `LLVM_PROFDATA` bindings, executable validation before cargo-llvm-cov installation, explicit propagation through the isolated runtime, and a second fail-closed validation before the first coverage invocation. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. ### Fixed +- Bind the write-capable OpenCode PR autofix and conflict-resolution worker exclusively to NVIDIA NIM through `NVIDIA_NIM_API_KEY`, fail closed when that model credential is absent, disable implicit GitHub Models provider discovery, and preserve the existing repository-write identity chain separately from model inference. +- Provision Debian LLVM 19 in the trusted OpenCode coverage image, bind the versioned `llvm-cov` and `llvm-profdata` executables, and preflight both paths before installing the pinned cargo-llvm-cov archive; isolated-runtime propagation and pre-invocation revalidation remain required by the permanent contract before this change can merge. +- Distinguish conservatively proven type-only TypeScript changes from executable code when `coverage-final.json` omits the changed file, permitting only multiline `import type`, balanced `interface`, comment, and delimiter lines while preserving fail-closed missing-instrumentation errors for mixed or runtime-looking changes. - Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. - Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities. +- Keep the native Atheris fuzz-engine lock in dedicated repository fuzz workflows instead of installing it in the generic OpenCode coverage image; immutable hash-pinned property and regression test locks remain eligible for central coverage materialization. +- Publish bounded, credential-redacted OpenCode coverage setup diagnostics through one shared helper, redact every `Authorization` value without relying on an enumerated authentication scheme, add a hash-locked Python 3.10 TOML compatibility dependency, and preserve exact-head validation. +- Reject unsafe Strix source-directory overrides before path joining, including traversal, absolute, nested, symlink-expanding, glob, control-character, oversized, and excessive-cardinality values while retaining validated internationalized direct directory names. +- Restore the protected-main OpenCode Git-configuration isolation and NVIDIA NIM fallback contracts, remove transient pull-request repair workflows and encoded patch payloads, and add a permanent repository-wide branch-writer absence gate. + +### Documentation + +- Document the OpenCode PR autofix NVIDIA NIM provider/credential boundary, fail-closed secret handling, unchanged GitHub write-identity chain, regression contract, rollback rules, and current official NVIDIA, OpenCode, and GitHub references in APA 7th format. +- Document the fail-closed Rust LLVM coverage boundary with APA 7 references to Debian's LLVM 19 package manifest, cargo-llvm-cov's compatibility and environment-variable contract, and the official LLVM `llvm-cov` and `llvm-profdata` command guides. +- Extend the coverage diagnostics doctoring record with the type-erasure trust boundary, Inkspan reproduction, RED/GREEN exact-head evidence, fail-closed limitations, and APA 7 references to official TypeScript documentation. +- Add APA 7 doctoring records for coverage diagnostics, the generic coverage/native fuzz-engine dependency boundary, the trusted-uv materializer, and the Strix NVIDIA fallback and source-directory boundary, including exact-base trust models, verification fixtures, limitations, and rollback requirements. diff --git a/docs/doctoring/coverage-failure-diagnostics.md b/docs/doctoring/coverage-failure-diagnostics.md new file mode 100644 index 000000000..a12a8f8da --- /dev/null +++ b/docs/doctoring/coverage-failure-diagnostics.md @@ -0,0 +1,56 @@ +# Credential-redacted coverage failure diagnostics + +## Decision + +Coverage setup failures are security-relevant review evidence, but exception text is untrusted and may contain registry URL userinfo, authorization headers, API tokens, database connection strings, passwords, or encryption keys. JavaScript and Python trusted-lock materializers therefore delegate multiline `GITHUB_OUTPUT` publication to one shared helper. The helper normalizes whitespace, applies the central credential sanitizer, bounds each field, HTML-escapes Markdown-embedded evidence, and replaces the fixed multiline delimiter before publication. + +The sanitizer applies URL-userinfo and complete authorization-header-value redaction before key-value truncation so mixed single-line failures cannot preserve an earlier credential. It deliberately does not enumerate authentication schemes: `Bearer`, `Basic`, `Token`, `Digest`, AWS signing schemes, custom provider schemes, and scheme-less values are all untrusted and replaced in full after the `Authorization` field separator. The final output retains the failure class, stage, bounded non-secret context, and remediation without exposing raw credentials. Local CLI status remains nonzero when publication is unavailable. + +## Type-only TypeScript coverage boundary + +The changed-source JavaScript/TypeScript gate must distinguish executable code from declarations that TypeScript removes before JavaScript execution. A type-only source file can therefore be legitimately absent from Istanbul's `coverage-final.json`; treating that absence alone as uncovered runtime code creates a false merge blocker even when every production statement and branch is covered. + +The gate now checks an omitted changed file before failing. It permits the omission only when every changed line is conservatively classified as a comment, delimiter, multiline `import type` statement, or line within a balanced `interface` declaration. TypeScript documents that `import type` is fully erased and that type annotations and other type-system constructs are removed when JavaScript is emitted. Unsupported declaration syntax, malformed or unbalanced structures, ordinary imports, values, functions, classes, object literals, and any other runtime-looking line remain fail-closed and still require matching Istanbul evidence. + +This is not a filename exemption. A file named `types.ts` receives no special trust, and a mixed declaration/runtime file continues to fail when any changed executable-looking line lacks instrumentation. The classifier also strips quoted string literals only for interface brace counting; it does not execute a TypeScript parser, infer semantics, or convert a failed coverage result into success. + +## Verification contract + +The exact-head gate requires Python 3.10 compilation, Python 3.14 tests, 100% production statement and branch coverage, 100% production docstrings, and direct execution of the shared sanitizer CLI contract. Regression cases cover mixed URL, arbitrary Authorization schemes, scheme-less Authorization values, token secrets, delimiter injection, oversized errors, missing `GITHUB_OUTPUT`, and both materializer call paths. Temporary write-capable repair workflows are removed from the final tree. + +The type-only regression reproduces the Inkspan review failure with an empty Istanbul final map, a multiline type-only import, an exported interface, interface-local documentation, and a newly added interface property. It must pass only because no changed executable unit exists. Existing tests preserve the opposite boundary: a changed runtime source absent from instrumentation fails with the file name, and a runtime-looking line with no mapped Istanbul unit fails closed. + +### Test-first evidence + +- Inkspan exact-head review failure: central OpenCode review-dispatch run `31092356765` reported `src/types.ts` absent from `coverage-final.json` even though the changed file contained only type imports, public documentation, and interface properties. +- RED regression commit: `fcd16958aaed94336a222cefdf78c68d7f39a099`; trusted full-quality run `31096345623` failed on the new omitted-type-only fixture. +- Production repair commit: `3d4b82ebb6c50a0da54a19700f11b2724fd04c81`; the first full-quality run `31096849845` proved all 918 tests passed and identified one uncovered classifier branch rather than weakening the 100% branch gate. +- Branch-completion regression commit: `b39dfe533128e96be41e7db60d5eefb3f6cf311f`; the fixture adds interface-local documentation to exercise the conservative block-comment path. + +Exact-head run identifiers after documentation integration belong in the pull-request release evidence; predecessor-head success is never sufficient for merge. + +## Standards and guidance + +GitHub environment files define delimiter-based multiline outputs and warn that a delimiter must not occur alone within arbitrary values. This implementation delimiter-proofs bounded fields before writing `GITHUB_OUTPUT`. OWASP logging guidance recommends removing, masking, sanitizing, hashing, or encrypting access tokens, passwords, database connection strings, encryption keys, session identifiers, and sensitive personal data rather than recording them directly. RFC 3986 deprecates secret passwords in URI userinfo because URIs are commonly displayed, stored, and logged. + +TypeScript's official documentation defines type-only imports as declarations that are removed from emitted JavaScript and describes TypeScript's type system as erased during compilation. That primary technical contract supports a declaration-aware coverage decision, while the local conservative classifier and negative tests preserve fail-closed behavior for syntax outside the explicitly verified subset. + +## Limitations + +Pattern-based redaction is a defense-in-depth boundary, not a general secret classifier. Callers must not intentionally place secrets in exception messages. GitHub log masking and least-privilege workflow permissions remain required. The diagnostic helper does not make untrusted test output safe for shell evaluation or workflow-command execution. + +The TypeScript classifier is intentionally not a complete parser. It does not exempt type aliases spanning arbitrary expressions, namespaces, enums, decorators, declaration merging, ambient modules, or newer syntax merely because those constructs may be erased in a particular toolchain. Expanding the accepted subset requires a failing fixture, authoritative compiler documentation, negative mixed-runtime tests, complete production statement and branch coverage, and exact-head review evidence. + +## References + +Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform resource identifier (URI): Generic syntax* (RFC 3986). Internet Engineering Task Force. https://doi.org/10.17487/RFC3986 + +GitHub. (2026). *Workflow commands for GitHub Actions*. GitHub Docs. Retrieved August 5, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands + +Microsoft. (n.d.). *TypeScript 3.8: Type-only imports and export*. TypeScript. Retrieved August 6, 2026, from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html + +Microsoft. (n.d.). *TypeScript for the new programmer*. TypeScript. Retrieved August 6, 2026, from https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html + +Microsoft. (n.d.). *Modules: Reference*. TypeScript. Retrieved August 6, 2026, from https://www.typescriptlang.org/docs/handbook/modules/reference.html + +OWASP Foundation. (n.d.). *Logging cheat sheet*. OWASP Cheat Sheet Series. Retrieved August 5, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html diff --git a/docs/doctoring/coverage-native-fuzz-lock-boundary.md b/docs/doctoring/coverage-native-fuzz-lock-boundary.md new file mode 100644 index 000000000..f1a1b6c2b --- /dev/null +++ b/docs/doctoring/coverage-native-fuzz-lock-boundary.md @@ -0,0 +1,56 @@ +# Generic coverage versus native fuzz-engine lock boundary + +## Decision + +The central OpenCode coverage image materializes immutable, hash-pinned dependencies needed to import selected production modules and run their ordinary tests. It does not install native coverage-guided fuzz engines that are executed only by dedicated repository fuzz workflows. + +`requirements-atheris.txt` is therefore classified as a native fuzz-engine lock and excluded from generic coverage materialization. The classification is exact-name based and path-independent. Hash-pinned property and regression locks such as `requirements-property.txt` and `requirements-fuzz-regression.txt` remain eligible. + +## Technical rationale + +Atheris is a coverage-guided native Python fuzzer built on libFuzzer. Its runtime role is to instrument and repeatedly execute fuzz targets, not to provide application imports required by an ordinary coverage.py test run. Installing an interpreter- and platform-specific native fuzz runtime in every generic coverage image adds an unrelated native artifact compatibility gate before application coverage begins. + +Coverage.py measures execution of Python programs and can report statement and branch coverage for the selected test process without Atheris. The central reviewer therefore preserves two independent verification layers: + +1. repository Fuzz workflows install and execute the native fuzz engine against real fuzz targets; +2. central OpenCode coverage evidence installs ordinary import/test dependencies and measures the selected production surface. + +Separating these layers avoids converting a native fuzz toolchain mismatch into a source-coverage review failure while retaining both gates. + +## Trust boundary + +The materializer still reads every candidate only from the exact validated pull-request base commit. Pull-request-mutated dependency files never enter the networked image-build stage. Every included lock must remain hash-pinned, and malformed Git metadata, unsafe paths, non-blob entries, unpinned requirements, and unsafe output destinations remain fail-closed. + +The exclusion reduces trusted inputs. It does not introduce an unhashed fallback, download a replacement package, or suppress an application/test import failure. Dedicated Fuzz required workflows continue to install `requirements-atheris.txt` directly. + +## Verification evidence + +A real temporary Git repository fixture contains: + +- `fuzz/requirements-atheris.txt`; +- `fuzz/requirements-property.txt`; +- `services/example_service/requirements-fuzz-regression.txt`. + +The test commits these files as the immutable base, materializes that exact revision, and proves that only the property and regression locks appear in the generated manifest. A second contract proves exact-name classification so a substring or directory name cannot broaden the exclusion. + +The changed helper and integration path are subject to the central 100% statement, branch, and docstring gates. + +## Operational limits + +The exact-name set initially contains only `requirements-atheris.txt`. Another native engine must not be added through a wildcard or informal comment. It requires separate artifact-role evidence, a regression fixture, review, and changelog entry. + +This boundary does not claim that Atheris is optional for fuzzing. It is optional only for the generic OpenCode import/coverage image. Repositories remain responsible for realistic dedicated fuzz execution and crash-regression evidence. + +## Rollback + +Rollback removes the exact-name classifier and its fixture. Before rollback, operators must confirm that every supported central coverage interpreter can install every repository's Atheris lock and that doing so provides coverage evidence not already supplied by the dedicated Fuzz workflow. Otherwise rollback recreates the false-negative review condition documented here. + +## APA 7 references + +Batchelder, N. (2026). *Coverage.py documentation*. https://coverage.readthedocs.io/ + +Google. (2026). *Atheris: A coverage-guided, native Python fuzzer* [Computer software]. GitHub. https://github.com/google/atheris + +Python Packaging Authority. (2026). *Dependency specifiers*. Python Packaging User Guide. https://packaging.python.org/en/latest/specifications/dependency-specifiers/ + +Semgrep, Inc. (2026). *Sample continuous integration configurations*. https://semgrep.dev/docs/semgrep-ci/sample-ci-configs diff --git a/docs/doctoring/opencode-llvm-coverage-toolchain.md b/docs/doctoring/opencode-llvm-coverage-toolchain.md new file mode 100644 index 000000000..2a2927d0e --- /dev/null +++ b/docs/doctoring/opencode-llvm-coverage-toolchain.md @@ -0,0 +1,66 @@ +# OpenCode LLVM coverage toolchain decision + +## Decision + +The central OpenCode coverage image installs Debian Trixie's `llvm-19` package and explicitly exports: + +```text +LLVM_COV=/usr/bin/llvm-cov-19 +LLVM_PROFDATA=/usr/bin/llvm-profdata-19 +``` + +The image build fails unless both paths are executable. This is required because the image uses Debian-packaged `rustc` rather than a rustup-managed toolchain, so `llvm-tools-preview` is not an available installation path. + +## Evidence and compatibility boundary + +`cargo-llvm-cov` documents `LLVM_COV` and `LLVM_PROFDATA` as the overrides to use when a Rust toolchain is installed outside rustup. It also requires the selected tools to be compatible with the LLVM version used by `rustc`. Its published compatibility table maps Rust 1.82–1.95 to LLVM 19–22. The central image therefore selects LLVM 19 as the lowest compatible family for its supported Rust range and keeps the two binary paths explicit rather than relying on an unversioned system default. + +Debian Trixie publishes `llvm-19` from the `llvm-toolchain-19` source package. The workflow installs the package from the pinned Debian image repositories and verifies the exact versioned executable paths during image construction. + +## Observed regression + +DiskSage pull request 133 exact head `b7f980d265713d5ffb84f744ce454589e3d410ea` passed its repository Test, Release, Security Scan, and SAST workflows. Central OpenCode run `31037491215`, job `92413313900`, then failed before Rust test execution with `failed to find llvm-tools-preview`. The failure reproduced the previously diagnosed central-toolchain defect rather than a DiskSage production-code failure. + +The earlier LLVM repair had been merged into an intermediate feature branch rather than protected `main`; later branch consolidation therefore left the required workflow source without the four toolchain lines. This current-main repair is intentionally limited to restoring those lines, permanent regression contracts, this decision record, the changelog, and the exact-head quality workflow that executes both focused and repository-wide evidence. + +## Security and reproducibility contract + +- Pull-request content cannot select another LLVM package or executable path. +- The coverage image definition remains default-branch controlled and is built from immutable workflow source. +- `LLVM_COV` and `LLVM_PROFDATA` are set together; partial configuration is rejected. +- Missing executables fail the image build before any pull-request coverage measurement starts. +- Every low-privilege coverage wrapper disables ambient system and global Git configuration before applying the single bounded `/work` safe-directory overlay. +- Both quality jobs check out `github.event.pull_request.head.sha`, refuse merge-tree or stale-head evidence, and preserve no repository credentials. +- The fast contract job installs no packages and evaluates no pull-request-selected dependency manifest. +- The full repository job installs only the repository's SHA-256 hash-locked quality requirements, then runs every test plus the configured 100% branch coverage and production docstring gates. +- The image digest, workflow commit SHA, pull-request head SHA, and coverage artifacts remain independently addressable evidence. +- CPU coverage is a correctness gate. GPU execution and parity tests remain separate domain-specific gates and are not represented by LLVM host coverage alone. + +This design does not claim formal compliance with a software supply-chain standard. It establishes a narrow, auditable compatibility boundary for deterministic Rust coverage execution. + +## Durable exact-head verification + +`.github/workflows/opencode-coverage-toolchain-quality-ci.yml` is the repository-owned acceptance path for this contract. It runs whenever the trusted coverage workflow, either quality workflow contract, this decision record, the hash-locked quality requirements, `pyproject.toml`, or the changelog changes. + +The first job checks out the exact pull-request head SHA, verifies that Git materialized that SHA rather than GitHub's generated merge revision, discovers every dependency-free `test_` function in the focused contract module, compiles the module, and fails if the test process changes the worktree. It never installs packages. + +Only after that job passes, a separate approved-environment job checks out and revalidates the same exact head, installs the repository-owned hash-locked quality toolchain, runs `pytest` across the complete `tests` directory under the configured 100% branch-coverage gate, enforces 100% production docstrings, and compiles all production CI modules and repository tests. This separation preserves a minimal early fail-closed contract while preventing focused tests from substituting for full repository acceptance. + +## Regression contract + +The central workflow contract test must continue to prove that: + +1. `llvm-19` is installed in the coverage image; +2. `LLVM_COV` names `/usr/bin/llvm-cov-19`; +3. `LLVM_PROFDATA` names `/usr/bin/llvm-profdata-19`; +4. the image build checks both paths before installing or invoking `cargo-llvm-cov`; +5. the OpenCode approval path remains fail-closed when Rust coverage cannot run; +6. all three low-privilege wrapper processes isolate system and global Git configuration before the safe-directory overlay; +7. the focused quality job is exact-head bound, credential-free, and dependency-free; and +8. the dependent full repository job is exact-head bound, uses the SHA-256 hash-locked quality toolchain, and runs the complete test, branch-coverage, docstring, and compilation gates. + +## References + +Debian Project. (2026). *Details of package llvm-19 in trixie*. https://packages.debian.org/trixie/amd64/llvm-19 + +Endo, T. (2026). *cargo-llvm-cov: Cargo subcommand to easily use LLVM source-based code coverage* [Computer software]. GitHub. https://github.com/taiki-e/cargo-llvm-cov diff --git a/docs/doctoring/pr-review-autofix-nvidia-nim-boundary.md b/docs/doctoring/pr-review-autofix-nvidia-nim-boundary.md new file mode 100644 index 000000000..99ca0013a --- /dev/null +++ b/docs/doctoring/pr-review-autofix-nvidia-nim-boundary.md @@ -0,0 +1,63 @@ +# OpenCode PR autofix NVIDIA NIM boundary + +## Status + +This doctoring record describes the provider and credential boundary for the write-capable `PR Review Autofix` control-plane worker. It does **not** change the read-only OpenCode review agent credential chain, reviewer identity, approval policy, branch protection, or merge policy. + +## Problem + +The central autofix worker previously configured OpenCode inference through GitHub Models and `STRIX_GITHUB_MODELS_TOKEN`. That coupling conflicts with the control-plane requirement that GitHub Actions repair agents use OpenCode with the organization `NVIDIA_NIM_API_KEY`, while repository-write credentials remain separately scoped to GitHub operations. + +For a write-capable worker, mixing model-provider and repository credentials also increases the number of credential paths that must be reasoned about during incident response and acquisition diligence. The safer boundary is one explicit model secret, one explicit provider, and an unchanged GitHub write-identity chain. + +## Decision + +`pr-review-autofix.yml` now applies the following contract to both ordinary review-feedback autofix and merge-conflict resolution: + +1. OpenCode enables only the `nvidia-nim` provider for model inference. +2. The selected model is `nvidia/llama-3.3-nemotron-super-49b-v1.5`; the small model is `meta/llama-3.3-70b-instruct`. +3. The workflow maps the organization Actions secret `NVIDIA_NIM_API_KEY` into the process-local `NVIDIA_API_KEY` expected by the OpenCode provider configuration. +4. The provider uses the OpenAI-compatible NVIDIA endpoint `https://integrate.api.nvidia.com/v1`. +5. The worker fails closed before invoking OpenCode when `NVIDIA_API_KEY` is absent. +6. `USE_GITHUB_TOKEN` is disabled for OpenCode model-provider discovery so GitHub Models is not a hidden inference fallback. +7. Existing `PR_REVIEW_MERGE_TOKEN` → `OPENCODE_APPROVE_TOKEN` → OpenCode app-token → `github.token` repository-write selection remains unchanged in the GitHub-facing steps. +8. OpenCode keeps shell execution denied and may edit only the exact allowlisted paths derived from current review feedback. + +## Security and privacy rationale + +NVIDIA NIM exposes OpenAI-compatible inference APIs, allowing the existing OpenCode OpenAI-compatible provider adapter to be used without introducing a provider-specific execution surface. OpenCode documents NVIDIA as a supported provider, supports `NVIDIA_API_KEY` for headless environments, and allows a custom base URL for NIM deployments. GitHub documents that Actions secrets can be injected through the `secrets` context and recommends environment variables rather than command-line arguments for sensitive values. + +The workflow therefore keeps the model credential in a step-scoped environment variable rather than embedding it in configuration, arguments, logs, repository content, review text, or generated artifacts. A missing secret is an error rather than a reason to fall back to GitHub Models, a public/free pool, or another provider. + +## Test-first evidence + +The permanent regression contract is `tests/test_pr_review_autofix_nvidia_nim_contract.py`. It requires: + +- the exact NIM primary and small model bindings; +- `enabled_providers` containing only `nvidia-nim`; +- the NVIDIA OpenAI-compatible base URL and environment-backed API key; +- the `NVIDIA_NIM_API_KEY` Actions secret mapping; +- absence of `STRIX_GITHUB_MODELS_TOKEN`, `github-models`, and `models.github.ai` from the autofix workflow; and +- preservation of the existing GitHub repository-write credential chain. + +The RED test was committed before the workflow change. The subsequent production commit changed only `.github/workflows/pr-review-autofix.yml`; its diff removed the GitHub Models provider and added the fail-closed NVIDIA NIM path for both write-capable OpenCode invocations. + +## Operational acceptance + +This boundary is not merge evidence by itself. The containing pull request remains subject to current-head CI, security, coverage, docstring, packaging, provenance, independent review, branch-protection, and repository-policy gates. Pending, queued, cancelled, predecessor-head, or synthetic-merge evidence is not success. + +The organization secret must be exposed only to repositories that are authorized to execute the central worker. Secret availability should be reviewed through GitHub organization Actions-secret access policy. The secret value must never be copied into PR comments, logs, artifacts, prompts, generated patches, or shareable review evidence. + +## Rollback + +Rollback is appropriate only if NVIDIA NIM is intentionally removed as the approved Actions repair provider. A rollback must update the permanent test, this doctoring record, and the workflow in one reviewed change and must not silently reintroduce GitHub Models or another provider as a fallback. Repository-write identity selection must remain separate from model-provider credentials. + +## References + +GitHub, Inc. (2026). *Using secrets in GitHub Actions*. GitHub Docs. https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets + +NVIDIA Corporation. (2026). *Architecture—NVIDIA NIM for Large Language Models*. NVIDIA Docs. https://docs.nvidia.com/nim/large-language-models/latest/reference/architecture.html + +NVIDIA Corporation. (2026). *Quickstart—NVIDIA NIM for Large Language Models*. NVIDIA Docs. https://docs.nvidia.com/nim/large-language-models/latest/get-started/quickstart.html + +OpenCode. (2026). *Providers*. https://opencode.ai/docs/providers diff --git a/docs/doctoring/rust-llvm-coverage-toolchain.md b/docs/doctoring/rust-llvm-coverage-toolchain.md new file mode 100644 index 000000000..418d961bc --- /dev/null +++ b/docs/doctoring/rust-llvm-coverage-toolchain.md @@ -0,0 +1,64 @@ +# Rust LLVM coverage toolchain boundary + +## Status + +This record defines the trusted central coverage-image contract for Rust repositories reviewed by the OpenCode dispatch workflow. It is normative for the current repair and does not by itself establish passing evidence. The production workflow, its permanent contract test, and every exact-head check must agree before the pull request may leave Draft state. + +## Problem statement + +`cargo-llvm-cov` orchestrates Rust source-based coverage by invoking LLVM coverage tools. A pinned `cargo-llvm-cov` executable alone is insufficient when the container does not also provide compatible `llvm-cov` and `llvm-profdata` executables. Missing tools, implicit PATH selection, or incompatible tool versions can turn a coverage gate into an infrastructure failure or, worse, allow an ambient executable to determine evidence semantics. + +The trusted image therefore owns the complete coverage toolchain identity. The image must install the Debian `llvm-19` package, bind `LLVM_COV` to `/usr/bin/llvm-cov-19`, bind `LLVM_PROFDATA` to `/usr/bin/llvm-profdata-19`, and verify both paths as executable before downloading or installing the pinned `cargo-llvm-cov` archive. Debian's package manifest lists both versioned executables in `llvm-19`; the workflow must use those explicit paths rather than an unversioned PATH lookup. + +The exact variables must be passed into the isolated runtime that executes untrusted repository tests. The runtime must verify both executables again before the first `cargo llvm-cov` invocation. A missing, replaced, non-executable, or unpropagated tool path is a fail-closed coverage setup failure and cannot be treated as not applicable, advisory, queued, or successful evidence. + +## Required implementation contract + +The trusted coverage workflow must preserve all of the following properties: + +1. Keep the existing digest-pinned Python base image and immutable action pins. +2. Install Debian `llvm-19` in the trusted image build. +3. Declare `ENV LLVM_COV=/usr/bin/llvm-cov-19` and `ENV LLVM_PROFDATA=/usr/bin/llvm-profdata-19` after package installation and before the pinned `cargo-llvm-cov` archive installation. +4. Run `test -x "$LLVM_COV"` and `test -x "$LLVM_PROFDATA"` before downloading or extracting the archive. +5. Preserve the reviewed `cargo-llvm-cov` version, archive URL, and SHA-256 verification. +6. Pass `LLVM_COV` and `LLVM_PROFDATA` explicitly through the isolated `docker run` boundary. +7. Re-run both executable checks inside the isolated runtime before the first `cargo llvm-cov` command. +8. Preserve exact-head source materialization, no-persisted-credential checkout, network isolation, least privilege, native-fuzz separation, 100% statement and branch coverage, and public-docstring gates. +9. Treat any setup mismatch as a blocking failure with bounded, redacted diagnostics. + +The environment variables are capability bindings, not secrets. They may be included in local diagnostic evidence, but the evidence must identify only the expected paths and command versions. It must not expose repository credentials, provider credentials, GitHub command-file contents, or host-specific filesystem material outside the approved coverage boundary. + +## Compatibility rationale + +The cargo-llvm-cov project documents `LLVM_COV` and `LLVM_PROFDATA` as explicit overrides and requires the selected LLVM tools to be compatible with the LLVM version used by `rustc`. Its compatibility table places Rust 1.82 through 1.95 with LLVM 19 through 22. The central image currently uses Debian's Rust toolchain together with Debian LLVM 19, so versioned LLVM 19 binaries are the narrowest reproducible system-package boundary for this image. + +`llvm-profdata` processes and merges instrumentation profiles; `llvm-cov` reads profile data and instrumented binaries to produce reports or exports. Both are therefore part of the evidence-producing chain. Validating only the wrapper executable does not validate the underlying coverage evidence toolchain. + +## Failure semantics + +The following conditions block approval and merge: + +- Debian `llvm-19` cannot be installed from the image's configured package sources; +- either explicit executable path is absent, is a symlink to an unapproved location, or is not executable; +- either environment variable is absent or changed at the isolated-runtime boundary; +- the second runtime validation occurs after a `cargo llvm-cov` invocation; +- the archive digest, version, or immutable source identity changes without a separate reviewed update; +- a workflow converts the failure into advisory, skipped, not-applicable, or prior-head evidence. + +A re-run without a code or infrastructure change cannot cure a deterministic missing-tool contract. Fresh exact-head CI is required after the production workflow is repaired. + +## Verification plan + +The permanent contract test must assert the package, explicit variables, two ordered executable validations, isolated-runtime propagation, and pre-invocation ordering. The GREEN head must then run the focused contract test, the complete repository test suite, production statement and branch coverage, public-docstring checks, workflow syntax and security-boundary tests, CodeQL and other security gates, packaging and provenance checks, and fresh independent review. No result from the RED head transfers to the GREEN head. + +## References + +Debian Project. (2026). *File list of package llvm-19 in trixie for amd64*. Debian Packages. https://packages.debian.org/trixie/amd64/llvm-19/filelist + +Debian Project. (2026). *Package llvm-19 in trixie*. Debian Packages. https://packages.debian.org/trixie/llvm-19 + +LLVM Project. (2026). *llvm-cov—Emit coverage information*. LLVM documentation. https://llvm.org/docs/CommandGuide/llvm-cov.html + +LLVM Project. (2026). *llvm-profdata—Profile data tool*. LLVM documentation. https://llvm.org/docs/CommandGuide/llvm-profdata.html + +Taiki Endo. (2026). *cargo-llvm-cov: Cargo subcommand to use LLVM source-based code coverage* [Computer software]. GitHub. https://github.com/taiki-e/cargo-llvm-cov diff --git a/docs/doctoring/strix-source-directory-boundary.md b/docs/doctoring/strix-source-directory-boundary.md new file mode 100644 index 000000000..738f05a4e --- /dev/null +++ b/docs/doctoring/strix-source-directory-boundary.md @@ -0,0 +1,48 @@ +# Strix source-directory boundary + +## Decision + +`STRIX_SOURCE_DIRS` is a scanner input boundary, not an arbitrary filesystem path list. The central Strix gate now accepts only `.` or direct child directory names whose characters are drawn from a known-good Unicode-aware allowlist. The normalized value is deduplicated, bounded to 32 entries and 8,192 input bytes, and frozen as a read-only shell variable before any path join occurs. + +Nested paths are intentionally not accepted. The gate already resolves and validates `STRIX_TARGET_PATH`; callers that need a nested scan root must select that root through the target-path contract and use `STRIX_SOURCE_DIRS=.`. This keeps one canonical trust boundary instead of composing two independently mutable path fragments. + +## Threat model + +Before this change, each whitespace-delimited `STRIX_SOURCE_DIRS` token was appended to the canonical target root. A caller-controlled absolute path could discard the intended root, while `..` components or nested symlink chains could resolve outside it. The subsequent recursive search could then read unrelated runner files and allow their content to influence a published Strix report. + +The protected boundary rejects: + +- absolute paths; +- `/` and `\\` separators; +- parent traversal and nested path components; +- shell glob and metacharacter input; +- option-like names beginning with `-`; +- control characters, tabs, and line breaks; +- overlong components, overlong lists, and excessive entry counts. + +Safe direct names remain internationalized: Unicode letters, combining marks, and numbers are accepted. The final candidate must still be a real non-symlink directory under the already-canonical scan target before recursive search begins. + +## Verification + +`tests/test_strix_model_utils_source_dirs.py` provides executable regressions for: + +- deterministic deduplication and order preservation; +- Korean direct-directory names; +- read-only post-validation state; +- relative and absolute traversal; +- nested paths and both path separators; +- glob, punctuation, option-like, control-character, size, and cardinality limits. + +The test was first executed against the prior helper and failed for traversal, absolute, nested, glob, punctuation, and duplicate inputs. It passes after the source-boundary contract is installed. The helper is also parsed with `bash -n`, and the Python regression module is compiled before publication. + +## Security properties and limits + +The change follows an accept-known-good strategy instead of attempting to remove dangerous substrings. It also avoids returning the rejected value in error messages. This prevents the common failure mode where filtering one traversal representation leaves another representation or where diagnostics disclose useful filesystem details. + +This control does not make arbitrary scanner output trustworthy. Strix findings remain untrusted data, provider failures remain fail-closed, PR-head materialization remains bounded to validated Git objects, and privileged workflow publication continues to require exact-head checks and repository protection. + +## References + +MITRE. (2026, April 30). *CWE-22: Improper limitation of a pathname to a restricted directory ('path traversal')* (Version 4.20). Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/22.html + +OWASP Foundation. (n.d.). *Path traversal*. Retrieved August 5, 2026, from https://owasp.org/www-community/attacks/Path_Traversal diff --git a/docs/superpowers/plans/2026-08-05-coverage-native-fuzz-lock-boundary.md b/docs/superpowers/plans/2026-08-05-coverage-native-fuzz-lock-boundary.md new file mode 100644 index 000000000..7f083022f --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-coverage-native-fuzz-lock-boundary.md @@ -0,0 +1,129 @@ +# Coverage Native-Fuzz Lock Boundary Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Prevent native fuzz-engine toolchain locks from entering generic OpenCode coverage images while retaining hash-pinned property and test dependencies. + +**Architecture:** Add one exact-name lock-role classifier to the trusted-base Python dependency materializer and evaluate it before blob selection. Protect the boundary with real temporary-Git fixtures, 100% production statement/branch coverage, docstrings, and source-backed doctoring. + +**Tech Stack:** Python 3.10+, `pathlib`, Git CLI read-only commands, pytest, pytest-cov. + +## Global Constraints + +- Continue reading dependency metadata only from the validated base commit. +- Do not change OpenCode, Noema, Strix, NVIDIA NIM, or reviewer credential names/scopes. +- Do not weaken `--require-hashes`, output bounds, symlink rejection, or malformed-tree failure. +- Every changed production helper must have a docstring and 100% statement/branch coverage. +- Document current authoritative sources in APA 7 format. +- Update `CHANGELOG.md`. + +--- + +### Task 1: Add failing real-repository coverage-role evidence + +**Files:** +- Create: `tests/test_coverage_native_fuzz_lock_boundary.py` + +**Interfaces:** +- Consumes: `materializer.materialize(repo, base_sha, output)` +- Produces: a fixture proving `requirements-atheris.txt` is excluded while property/test locks remain + +- [ ] **Step 1: Create a temporary Git base with three hash locks** + +Add: + +- `fuzz/requirements-atheris.txt`; +- `fuzz/requirements-property.txt`; +- `services/example/requirements-fuzz-regression.txt`. + +- [ ] **Step 2: Assert only the latter two enter the manifest** + +The exact Atheris name must be absent. The nonexact similarly named lock proves the classifier is not substring-based. + +- [ ] **Step 3: Run the focused test and verify RED** + +Run: `python -m pytest tests/test_coverage_native_fuzz_lock_boundary.py -q` + +Expected: FAIL because all three files are currently selected. + +- [ ] **Step 4: Commit the failing test** + +```bash +git add tests/test_coverage_native_fuzz_lock_boundary.py +git commit -m "test(coverage): exclude native fuzz engine locks" +``` + +### Task 2: Implement the exact-name lock-role boundary + +**Files:** +- Modify: `scripts/ci/materialize_base_python_requirements.py` + +**Interfaces:** +- Produces: `_is_native_fuzz_engine_lock_name(name: str) -> bool` +- Updates: `_is_candidate_lock_name(name: str) -> bool` + +- [ ] **Step 1: Add an immutable exact-name set** + +The initial set contains only `requirements-atheris.txt`. + +- [ ] **Step 2: Add the pure classifier with explanatory docstring** + +Return true only for exact members of the immutable set. + +- [ ] **Step 3: Exclude the native toolchain before ordinary candidate matching** + +Keep every existing candidate and content check unchanged for other files, including the shared bounded and credential-redacting failure-summary publisher. + +- [ ] **Step 4: Run focused tests and verify GREEN** + +Run: `python -m pytest tests/test_coverage_native_fuzz_lock_boundary.py -q` + +Expected: PASS. + +- [ ] **Step 5: Run coverage and docstring gates** + +Run the repository's full Python test, branch-coverage, compile, formatting, static-security, and interrogate commands. + +- [ ] **Step 6: Commit** + +```bash +git add scripts/ci/materialize_base_python_requirements.py tests/test_coverage_native_fuzz_lock_boundary.py .github/workflows/opencode-coverage-diagnostics-ci.yml +git commit -m "fix(coverage): skip native fuzz engine locks" +``` + +### Task 3: Record doctoring and release evidence + +**Files:** +- Create: `docs/doctoring/coverage-native-fuzz-lock-boundary.md` +- Create or modify: `CHANGELOG.md` + +**Interfaces:** +- Produces: operational rationale, standards traceability, and Unreleased evidence + +- [ ] **Step 1: Document the role boundary** + +Record why Atheris belongs to dedicated fuzz execution rather than generic import coverage, the immutable-base trust boundary, limitations, and rollback. + +- [ ] **Step 2: Add APA 7 references** + +Cite official Atheris, Python packaging, coverage.py, and Semgrep/GitHub Actions material relevant to the decision. + +- [ ] **Step 3: Update the changelog** + +Add the generic coverage materializer correction under `Unreleased / Fixed`. + +- [ ] **Step 4: Run full exact-slice verification and commit** + +```bash +git add docs/doctoring/coverage-native-fuzz-lock-boundary.md CHANGELOG.md +git commit -m "docs(coverage): record native fuzz lock boundary" +``` + +### Task 4: Validate, review, and integrate + +- [ ] **Step 1: Open a focused PR closing #762** +- [ ] **Step 2: Resolve every automated and human review finding** +- [ ] **Step 3: Re-run all exact-head central checks and independent review** +- [ ] **Step 4: Merge prerequisite #759 without administrative bypass** +- [ ] **Step 5: Retarget this PR to integrated `main`, rerun exact-head gates, and merge without bypass** +- [ ] **Step 6: Re-dispatch coverage review for contextual-orchestrator #96, merge it when green, then continue its dependency-ordered PR queue** diff --git a/docs/superpowers/specs/2026-08-05-coverage-native-fuzz-lock-boundary-design.md b/docs/superpowers/specs/2026-08-05-coverage-native-fuzz-lock-boundary-design.md new file mode 100644 index 000000000..80228153d --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-coverage-native-fuzz-lock-boundary-design.md @@ -0,0 +1,47 @@ +# Coverage Native-Fuzz Lock Boundary Design + +## Status + +Approved for autonomous implementation under issue #762. + +## Problem + +The central trusted-base dependency materializer currently treats every hash-pinned `requirements*.txt` file as part of the generic offline coverage image. That is too broad: native coverage-guided fuzz engines are execution toolchains for dedicated fuzz jobs, not import dependencies for ordinary statement/branch coverage. Selecting `requirements-atheris.txt` can make a coverage review fail on an interpreter-specific native wheel before any selected application or test code runs. + +## Decision + +Classify exact native fuzz-engine lock names as coverage-incompatible. The first supported name is `requirements-atheris.txt`. + +The classification is path-independent so standalone repositories and nested MSA modules receive the same treatment. Hash-pinned property/unit-test locks such as `requirements-property.txt` remain eligible. Exact-base Git reads, immutable-source selection, hash requirements, bounded output, and deterministic ordering remain unchanged. + +## Components + +### Lock-role classifier + +A pure helper receives one file name and returns whether it represents a native fuzz runtime. The helper uses an immutable exact-name set rather than substring matching, which avoids excluding unrelated dependencies such as `requirements-fuzz-regression.txt`. + +### Materializer integration + +`base_hash_locks` checks the role before reading or exporting the blob. Excluded native fuzz locks never enter the Docker build context or trusted coverage image. Dedicated repository fuzz workflows continue installing those locks directly. + +### Verification + +A real temporary Git repository proves that: + +- `fuzz/requirements-atheris.txt` is excluded; +- `fuzz/requirements-property.txt` remains materialized; +- a similarly named non-exact lock remains eligible; +- existing exact-base, hash, symlink, malformed-tree, uv, and CLI contracts remain green; +- changed production helpers retain 100% statement/branch coverage and docstrings. + +## Security boundary + +The change reduces trusted coverage-image inputs; it does not accept PR-controlled dependency metadata or weaken `--require-hashes`. Exclusion is limited to a toolchain that the generic coverage job never executes. Adding another excluded name requires a separate reviewed change, evidence, and test. + +## Non-goals + +- changing the dedicated Fuzz required workflow; +- changing OpenCode/Noema/Strix models or credentials; +- changing NVIDIA NIM keys; +- suppressing coverage failures in selected application/test dependencies; +- interpreting arbitrary requirement contents or comments as trusted role metadata. diff --git a/requirements-opencode-python310-ci-hashes.txt b/requirements-opencode-python310-ci-hashes.txt new file mode 100644 index 000000000..41c39db68 --- /dev/null +++ b/requirements-opencode-python310-ci-hashes.txt @@ -0,0 +1,5 @@ +# Python 3.10 compatibility dependency for the OpenCode coverage diagnostics job. +# Canonical package: tomli 2.4.1 from the Python Package Index. +# The pure-Python wheel is selected explicitly by --only-binary=:all:. +tomli==2.4.1 \ + --hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe diff --git a/scripts/ci/coverage_failure_summary.py b/scripts/ci/coverage_failure_summary.py new file mode 100644 index 000000000..5bbf45835 --- /dev/null +++ b/scripts/ci/coverage_failure_summary.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""Publish bounded, credential-redacted coverage setup failure evidence.""" + +from __future__ import annotations + +import html +import os +from pathlib import Path + +from scripts.ci.sanitize_github_output_summary import sanitize_text + +_COVERAGE_DELIMITER = "CWL_COVERAGE_SUMMARY_EOF" + + +def _safe_field(value: str, maximum_length: int) -> str: + """Normalize, redact, bound, escape, and delimiter-proof one output field.""" + + normalized = " ".join(value.split()) + redacted = sanitize_text(normalized)[:maximum_length] + escaped = html.escape(redacted, quote=True) + return escaped.replace( + _COVERAGE_DELIMITER, + "CWL_COVERAGE_SUMMARY_END", + ) + + +def publish_coverage_failure_summary( + stage: str, + error: BaseException, + remediation: str, +) -> None: + """Append one safe exact-stage failure envelope to ``GITHUB_OUTPUT``.""" + + github_output = os.environ.get("GITHUB_OUTPUT") + if not github_output: + return + + safe_stage = _safe_field(stage, 256) + safe_reason = _safe_field( + f"{error.__class__.__name__}: {error}", + 4096, + ) + safe_remediation = _safe_field(remediation, 1024) + summary = ( + "## Coverage Decision\n" + "- Result: FAIL\n" + f"- Failed stage: {safe_stage}\n" + "- Exact failure:\n" + f"
{safe_reason}
\n" + f"- Next action: {safe_remediation}\n" + ) + with Path(github_output).open("a", encoding="utf-8") as output: + output.write( + f"coverage_summary<<{_COVERAGE_DELIMITER}\n" + f"{summary}{_COVERAGE_DELIMITER}\n" + ) diff --git a/scripts/ci/javascript_coverage_gate.py b/scripts/ci/javascript_coverage_gate.py index b8c39e920..3d8e28611 100644 --- a/scripts/ci/javascript_coverage_gate.py +++ b/scripts/ci/javascript_coverage_gate.py @@ -24,6 +24,34 @@ } TEST_NAME_RE = re.compile(r"\.(?:spec|test)\.[cm]?[jt]sx?$") HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") +TYPE_IDENTIFIER_PATTERN = r"[$A-Z_a-z][$\w]*" +TYPE_MODULE_LITERAL_PATTERN = ( + r"(?:'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\")" +) +INTERFACE_RE = re.compile( + rf"^(?:export\s+)?(?:declare\s+)?interface\s+" + rf"{TYPE_IDENTIFIER_PATTERN}\s*\{{" +) +TYPE_ALIAS_START_RE = re.compile( + rf"^(?:export\s+)?(?:declare\s+)?type\s+{TYPE_IDENTIFIER_PATTERN}" + rf"(?:\s*<[^<>]*>)?\s*=\s*" +) +STRING_LITERAL_RE = re.compile( + r"'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\"|`(?:\\.|[^`\\])*`" +) +TYPE_RUNTIME_CALL_RE = re.compile(rf"\b{TYPE_IDENTIFIER_PATTERN}\s*\(") +TYPE_IMPORT_SINGLE_RE = re.compile( + rf"^import\s+type\s+(?:{TYPE_IDENTIFIER_PATTERN}|\{{[^{{}}]*\}})" + rf"\s+from\s+{TYPE_MODULE_LITERAL_PATTERN}\s*;?$" +) +TYPE_IMPORT_START_RE = re.compile(r"^import\s+type\s+\{\s*$") +TYPE_IMPORT_MEMBER_RE = re.compile( + rf"^(?:type\s+)?{TYPE_IDENTIFIER_PATTERN}" + rf"(?:\s+as\s+{TYPE_IDENTIFIER_PATTERN})?,?$" +) +TYPE_IMPORT_END_RE = re.compile( + rf"^\}}\s+from\s+{TYPE_MODULE_LITERAL_PATTERN}\s*;?$" +) def git_command(repo_root: Path, *args: str) -> list[str]: @@ -244,7 +272,11 @@ def changed_metric_counts( counts = branches.get(branch_id) or [] locations = branch_data.get("locations") or [] for index, count in enumerate(counts): - location = locations[index] if index < len(locations) else branch_data.get("loc") + location = ( + locations[index] + if index < len(locations) + else branch_data.get("loc") + ) line_range = location_range(location) if line_range is None: continue @@ -289,29 +321,315 @@ def normalize_coverage_path( return suffix_matches[0] if len(suffix_matches) == 1 else None -def likely_runtime_lines(repo_root: Path, path: str, changed_lines: set[int]) -> list[int]: - """Return changed lines that look executable when Istanbul maps no units.""" +def _type_code_without_comments(line: str) -> tuple[str | None, bool]: + """Return comment-free TypeScript code and an open-comment indicator. + + Complete quoted literals are masked before comment recognition so comment + markers inside module specifiers or string-literal types remain ordinary + syntax. Unmatched quotes and stray block-comment closers return ``None`` so + the caller classifies the line as runtime-looking rather than repairing it. + """ + masked = STRING_LITERAL_RE.sub( + lambda match: " " * len(match.group(0)), + line, + ) + if any(quote in masked for quote in ("'", '"', "`")): + return None, False + + code = list(line) + cursor = 0 + while True: + line_comment = masked.find("//", cursor) + block_start = masked.find("/*", cursor) + if line_comment >= 0 and ( + block_start < 0 or line_comment < block_start + ): + return "".join(code[:line_comment]).strip(), False + if block_start < 0: + break + block_end = masked.find("*/", block_start + 2) + if block_end < 0: + for index in range(block_start, len(code)): + code[index] = " " + return "".join(code).strip(), True + for index in range(block_start, block_end + 2): + code[index] = " " + masked = ( + masked[:block_start] + + (" " * (block_end + 2 - block_start)) + + masked[block_end + 2 :] + ) + cursor = block_start + + if "*/" in masked: + return None, False + return "".join(code).strip(), False + + +def _advance_interface_state( + structural: str, depth: int +) -> tuple[int, str | None]: + """Advance interface brace depth and return code after its closing brace. + + ``None`` means the declaration remains open. A string tail means the outer + declaration closed on this line; only an empty tail or standalone semicolon + can remain syntax-erased. + """ + for index, character in enumerate(structural): + if character == "{": + depth += 1 + elif character == "}": + depth -= 1 + if depth <= 0: + return depth, structural[index + 1 :].strip() + return depth, None + + +def _advance_type_alias_state( + structural: str, + depths: tuple[int, int, int], +) -> tuple[tuple[int, int, int], str | None, bool]: + """Advance alias delimiters and locate a top-level terminating semicolon. + + The returned boolean is false for an unmatched closing delimiter. ``None`` + as the tail means no top-level semicolon has completed the declaration yet. + Braces, brackets, and parentheses are tracked so member semicolons inside + object, tuple, or function types cannot terminate the outer alias early. + """ + round_depth, square_depth, brace_depth = depths + for index, character in enumerate(structural): + if character == "(": + round_depth += 1 + elif character == ")": + if round_depth == 0: + return (round_depth, square_depth, brace_depth), None, False + round_depth -= 1 + elif character == "[": + square_depth += 1 + elif character == "]": + if square_depth == 0: + return (round_depth, square_depth, brace_depth), None, False + square_depth -= 1 + elif character == "{": + brace_depth += 1 + elif character == "}": + if brace_depth == 0: + return (round_depth, square_depth, brace_depth), None, False + brace_depth -= 1 + elif ( + character == ";" + and round_depth == 0 + and square_depth == 0 + and brace_depth == 0 + ): + return ( + (round_depth, square_depth, brace_depth), + structural[index + 1 :].strip(), + True, + ) + return (round_depth, square_depth, brace_depth), None, True + + +def _type_alias_can_continue( + structural: str, + depths: tuple[int, int, int], + *, + initial: bool, +) -> bool: + """Return whether an unterminated line is unambiguously type continuation. + + Open delimiters are sufficient. At top level, an empty initializer line is + accepted only at the declaration start, while later continuation lines must + begin with a union, intersection, conditional-true, or conditional-false + marker. This deliberately rejects semicolonless aliases followed by a value + call, because treating that call as the alias terminator would fail open. + """ + if any(depths): + return True + text = structural.strip() + if not text: + return initial + return text.startswith(("|", "&", "?", ":")) + + +def likely_runtime_lines( + repo_root: Path, path: str, changed_lines: set[int] +) -> list[int]: + """Return changed lines that look executable when Istanbul maps no units. + + Only a narrow grammar of complete ``import type`` declarations, balanced + simple interfaces, and semicolon-terminated type aliases is accepted as + syntax-erased. Runtime tails, malformed literals or comments, unsupported + declaration forms, semicolonless aliases, and unterminated lexical or + declaration state remain runtime-looking so omitted instrumentation fails + closed. + """ source_lines = (repo_root / path).read_text( encoding="utf-8", errors="replace" ).splitlines() runtime_lines: list[int] = [] in_block_comment = False + block_comment_changed_lines: list[int] = [] + in_type_import = False + type_import_changed_lines: list[int] = [] + in_interface = False + interface_changed_lines: list[int] = [] + interface_depth = 0 + in_type_alias = False + type_alias_changed_lines: list[int] = [] + type_alias_depths = (0, 0, 0) + for line_number, raw_line in enumerate(source_lines, start=1): stripped = raw_line.strip() - if stripped.startswith("/*"): - in_block_comment = True + syntax_invalid = False + comment_only = False + + if in_block_comment: + block_end = stripped.find("*/") + if block_end < 0: + code = "" + comment_only = True + else: + in_block_comment = False + block_comment_changed_lines.clear() + tail = stripped[block_end + 2 :].strip() + if not tail or tail.startswith("//"): + code = "" + comment_only = True + else: + code, in_block_comment = _type_code_without_comments(tail) + syntax_invalid = code is None + if code is None: + code = tail + comment_only = not code and not syntax_invalid + else: + code, in_block_comment = _type_code_without_comments(stripped) + syntax_invalid = code is None + if code is None: + code = stripped + comment_only = not code and bool(stripped) and not syntax_invalid + + if in_block_comment and line_number in changed_lines: + block_comment_changed_lines.append(line_number) + + type_only = False + if not comment_only and not syntax_invalid: + if in_type_import: + if TYPE_IMPORT_END_RE.fullmatch(code): + type_only = True + in_type_import = False + type_import_changed_lines.clear() + elif TYPE_IMPORT_MEMBER_RE.fullmatch(code): + type_only = True + else: + in_type_import = False + elif in_interface: + structural = STRING_LITERAL_RE.sub("", code).strip() + interface_depth, tail = _advance_interface_state( + structural, + interface_depth, + ) + if tail is None: + type_only = True + else: + in_interface = False + interface_changed_lines.clear() + type_only = interface_depth == 0 and tail in {"", ";"} + elif in_type_alias: + structural = STRING_LITERAL_RE.sub("", code).strip() + type_alias_depths, tail, valid = _advance_type_alias_state( + structural, + type_alias_depths, + ) + if ( + valid + and tail is None + and _type_alias_can_continue( + structural, + type_alias_depths, + initial=False, + ) + ): + type_only = True + elif ( + valid + and tail == "" + and not TYPE_RUNTIME_CALL_RE.search(structural) + ): + type_only = True + in_type_alias = False + type_alias_changed_lines.clear() + else: + in_type_alias = False + type_alias_changed_lines.clear() + elif TYPE_IMPORT_SINGLE_RE.fullmatch(code): + type_only = True + elif TYPE_IMPORT_START_RE.fullmatch(code): + type_only = True + in_type_import = True + elif INTERFACE_RE.match(code): + structural = STRING_LITERAL_RE.sub("", code).strip() + interface_depth, tail = _advance_interface_state(structural, 0) + if tail is None: + in_interface = interface_depth > 0 + type_only = in_interface + else: + type_only = interface_depth == 0 and tail in {"", ";"} + else: + alias_match = TYPE_ALIAS_START_RE.match(code) + if alias_match: + structural = STRING_LITERAL_RE.sub( + "", + code[alias_match.end() :], + ).strip() + type_alias_depths, tail, valid = _advance_type_alias_state( + structural, + (0, 0, 0), + ) + if ( + valid + and tail == "" + and not TYPE_RUNTIME_CALL_RE.search(structural) + ): + type_only = True + elif ( + valid + and tail is None + and _type_alias_can_continue( + structural, + type_alias_depths, + initial=True, + ) + ): + type_only = True + in_type_alias = True + + if in_type_import and line_number in changed_lines: + type_import_changed_lines.append(line_number) + if in_interface and line_number in changed_lines: + interface_changed_lines.append(line_number) + if in_type_alias and line_number in changed_lines: + type_alias_changed_lines.append(line_number) + non_runtime = ( not stripped - or in_block_comment + or comment_only or stripped.startswith("//") or stripped in {"{", "}", "};", ");", "]", "],"} - or stripped.startswith(("interface ", "type ", "export type ", "import type ")) + or type_only ) if line_number in changed_lines and not non_runtime: runtime_lines.append(line_number) - if "*/" in stripped: - in_block_comment = False - return runtime_lines + + if in_type_import: + runtime_lines.extend(type_import_changed_lines) + if in_interface: + runtime_lines.extend(interface_changed_lines) + if in_type_alias: + runtime_lines.extend(type_alias_changed_lines) + if in_block_comment: + runtime_lines.extend(block_comment_changed_lines) + return sorted(set(runtime_lines)) def load_coverage_files( @@ -371,17 +689,26 @@ def main(argv: Sequence[str] | None = None) -> int: print(f"- {path.relative_to(repo_root)} (derived)") for metric in METRICS: print(f" {metric}: {metrics[metric]}%") - print("- Decision: advisory only; pre-existing global debt is visible but does not mask changed-code evidence.") + print( + "- Decision: advisory only; pre-existing global debt is visible but " + "does not mask changed-code evidence." + ) if not changed: print("\n## Changed-source coverage") - print("- No changed JavaScript/TypeScript runtime source files; coverage is not applicable.") + print( + "- No changed JavaScript/TypeScript runtime source files; " + "coverage is not applicable." + ) print("- Result: PASS") return 0 if not finals: print("\n## Changed-source coverage") print("- Result: FAIL") - print("- Reason: coverage-final.json is required for changed-line evidence but was not produced.") + print( + "- Reason: coverage-final.json is required for changed-line " + "evidence but was not produced." + ) return 1 changed_paths = set(changed) @@ -396,8 +723,16 @@ def main(argv: Sequence[str] | None = None) -> int: print("\n## Changed-source coverage") for path, changed_lines in sorted(changed.items()): if not records[path]: - print(f"- {path}: missing instrumentation") - failures.append(f"{path} is absent from coverage-final.json") + runtime_lines = likely_runtime_lines(repo_root, path, changed_lines) + if runtime_lines: + print(f"- {path}: missing instrumentation") + failures.append(f"{path} is absent from coverage-final.json") + else: + print(f"- {path}: no executable changed units") + print( + " changed lines are comments, delimiters, or type-only " + "declarations; no executable units apply" + ) continue counts = changed_metric_counts(records[path], changed_lines) metric_text = ", ".join( @@ -410,10 +745,14 @@ def main(argv: Sequence[str] | None = None) -> int: runtime_lines = likely_runtime_lines(repo_root, path, changed_lines) if runtime_lines: failures.append( - f"{path} changed runtime-looking lines {runtime_lines} but Istanbul mapped no execution units" + f"{path} changed runtime-looking lines {runtime_lines} " + "but Istanbul mapped no execution units" ) else: - print(" changed lines are comments, delimiters, or type-only declarations; no executable units apply") + print( + " changed lines are comments, delimiters, or type-only " + "declarations; no executable units apply" + ) continue for metric, (covered, total) in counts.items(): if total and covered != total: @@ -429,7 +768,10 @@ def main(argv: Sequence[str] | None = None) -> int: return 1 print("\n- Result: PASS") - print("- Reason: every instrumented execution unit intersecting changed runtime lines is covered.") + print( + "- Reason: every instrumented execution unit intersecting changed " + "runtime lines is covered." + ) return 0 diff --git a/scripts/ci/materialize_base_javascript_packages.py b/scripts/ci/materialize_base_javascript_packages.py index 407c17aa1..611db20e8 100644 --- a/scripts/ci/materialize_base_javascript_packages.py +++ b/scripts/ci/materialize_base_javascript_packages.py @@ -20,6 +20,11 @@ from typing import Any +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2])) +from scripts.ci.coverage_failure_summary import ( + publish_coverage_failure_summary as _publish_coverage_failure_summary, +) + SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") PNPM_SPEC_RE = re.compile(r"^pnpm@[0-9]+\.[0-9]+\.[0-9]+(?:[+-][A-Za-z0-9._+-]+)?$") PNPM_BASE_INPUT_NAMES = ("package.json", "pnpm-workspace.yaml", ".pnpmfile.cjs") @@ -445,6 +450,13 @@ def main(argv: list[str] | None = None) -> int: f"::error::Could not materialize base JavaScript package locks: {exc}", file=sys.stderr, ) + _publish_coverage_failure_summary( + "Base JavaScript package lock materialization", + exc, + "Repair or regenerate the reported lock entry so every non-link " + "package selected for the networked cache is registry- and " + "SHA-512-bounded, then rerun the current-head coverage-evidence job.", + ) return 1 if manifest: diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 98cdad459..81f415c0b 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -29,6 +29,11 @@ import tomli as tomllib +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2])) +from scripts.ci.coverage_failure_summary import ( + publish_coverage_failure_summary as _publish_coverage_failure_summary, +) + SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") UV_EXACT_REQUIREMENT_RE = re.compile( r"[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?" @@ -37,6 +42,7 @@ ) UV_SHA256_HASH_RE = re.compile(r"--hash=sha256:[0-9a-fA-F]{64}") UV_EXPORT_TIMEOUT_SECONDS = 120 +NATIVE_FUZZ_ENGINE_LOCK_NAMES = frozenset({"requirements-atheris.txt"}) TRUSTED_UV_VERSION = "0.12.1" TRUSTED_UV_ARCHIVE_URL = ( "https://releases.astral.sh/github/uv/releases/download/0.12.1/" @@ -79,11 +85,19 @@ def _install_trusted_uv_url_opener() -> None: urllib.request.install_opener(opener) +def _is_native_fuzz_engine_lock_name(name: str) -> bool: + """Return whether a lock installs a native engine used only by fuzz jobs.""" + return name in NATIVE_FUZZ_ENGINE_LOCK_NAMES + + def _is_candidate_lock_name(name: str) -> bool: - """Return whether a file name is a possible pip requirements lock.""" - return name == "requirements.lock" or ( - fnmatch.fnmatch(name, "requirements*.txt") - and not fnmatch.fnmatch(name, "requirements-*-ci-hashes.txt") + """Return whether a file name is a possible coverage dependency lock.""" + return not _is_native_fuzz_engine_lock_name(name) and ( + name == "requirements.lock" + or ( + fnmatch.fnmatch(name, "requirements*.txt") + and not fnmatch.fnmatch(name, "requirements-*-ci-hashes.txt") + ) ) @@ -516,6 +530,12 @@ def main(argv: list[str] | None = None) -> int: print( f"::error::Could not materialize base Python locks: {exc}", file=sys.stderr ) + _publish_coverage_failure_summary( + "Base Python lock materialization", + exc, + "Repair the reported trusted lock or Git metadata boundary, then " + "rerun the current-head coverage-evidence job.", + ) return 1 if manifest: diff --git a/scripts/ci/sanitize_github_output_summary.py b/scripts/ci/sanitize_github_output_summary.py index 1a7036f75..9fa3aa263 100644 --- a/scripts/ci/sanitize_github_output_summary.py +++ b/scripts/ci/sanitize_github_output_summary.py @@ -15,18 +15,19 @@ r"API[_-]?KEY|PRIVATE[_-]?KEY|ACCESS[_-]?KEY|ENCRYPTION[_-]?KEY" r")[A-Z0-9_.-]*\b)(?P\s*[:=]\s*)" ) -URL_CREDENTIAL_RE = re.compile(r"(?i)\b([a-z][a-z0-9+.-]*://)([^/\s:@]+):([^@\s/]+)@") -AUTH_HEADER_RE = re.compile(r"(?i)\b(Authorization\s*[:=]\s*)(Bearer|Basic)\s+[^\s,;]+") +URL_CREDENTIAL_RE = re.compile(r"(?i)\b([a-z][a-z0-9+.-]*://)([^/\s@]+)@") +AUTH_HEADER_RE = re.compile(r"(?i)\b(Authorization\s*[:=]\s*)[^\r\n]*") def sanitize_line(line: str) -> str: - """Redact one log line while preserving the key and evidence context.""" + """Redact one log line while preserving non-credential evidence context.""" - match = SECRET_KEY_RE.search(line) + sanitized = URL_CREDENTIAL_RE.sub(r"\1@", line) + sanitized = AUTH_HEADER_RE.sub(r"\1", sanitized) + match = SECRET_KEY_RE.search(sanitized) if match: - return f"{line[: match.end()]}" - line = URL_CREDENTIAL_RE.sub(r"\1@", line) - return AUTH_HEADER_RE.sub(r"\1\2 ", line) + return f"{sanitized[: match.end()]}" + return sanitized def sanitize_text(text: str) -> str: @@ -52,5 +53,10 @@ def main() -> int: return 0 -if __name__ == "__main__": - raise SystemExit(main()) +def _entrypoint(module_name: str) -> None: + """Run the file-oriented CLI only when executed as a script.""" + if module_name == "__main__": + raise SystemExit(main()) + + +_entrypoint(__name__) diff --git a/scripts/ci/strix_model_utils.sh b/scripts/ci/strix_model_utils.sh index 9f20eae67..d85fe9fdd 100755 --- a/scripts/ci/strix_model_utils.sh +++ b/scripts/ci/strix_model_utils.sh @@ -12,6 +12,85 @@ trim_whitespace() { printf '%s\n' "$value" } +sanitize_strix_source_dirs() { + local raw_source_dirs + raw_source_dirs="$(trim_whitespace "${1-}")" + if [ -z "$raw_source_dirs" ]; then + echo "ERROR: STRIX_SOURCE_DIRS must contain at least one safe direct directory name." >&2 + return 2 + fi + + python3 -I -S - "$raw_source_dirs" <<'PY' +from __future__ import annotations + +import sys +import unicodedata + +raw_source_dirs = sys.argv[1] +if len(raw_source_dirs.encode("utf-8")) > 8192 or any( + character in "\x00\r\n\t" for character in raw_source_dirs +): + print( + "ERROR: STRIX_SOURCE_DIRS must be a bounded space-separated directory list.", + file=sys.stderr, + ) + raise SystemExit(2) +entries = raw_source_dirs.split(" ") +entries = [entry for entry in entries if entry] +if not entries or len(entries) > 32: + print( + "ERROR: STRIX_SOURCE_DIRS must contain between 1 and 32 safe direct directory names.", + file=sys.stderr, + ) + raise SystemExit(2) + +allowed_ascii = frozenset("_.@+-") +normalized: list[str] = [] +seen: set[str] = set() +for entry in entries: + if entry == ".": + pass + elif ( + entry == ".." + or len(entry.encode("utf-8")) > 255 + or entry.startswith("-") + or "/" in entry + or "\\" in entry + or not all( + (character.isascii() and (character.isalnum() or character in allowed_ascii)) + or ( + not character.isascii() + and unicodedata.category(character)[0] in {"L", "M", "N"} + ) + for character in entry + ) + ): + print( + "ERROR: STRIX_SOURCE_DIRS accepts only '.' or safe direct directory names.", + file=sys.stderr, + ) + raise SystemExit(2) + if entry not in seen: + seen.add(entry) + normalized.append(entry) + +print(" ".join(normalized)) +PY +} + +# STRIX_SOURCE_DIRS is later split by the gate before joining each token to the +# already-canonical scan root. Freeze a lexical direct-child allowlist at source +# time so absolute paths, parent traversal, nested symlink chains, shell glob expansion, +# and option-like path ambiguity can never reach that join. +STRIX_SOURCE_DIRS_SANITIZED="$( + sanitize_strix_source_dirs "${STRIX_SOURCE_DIRS-.}" +)" || { + status=$? + return "$status" 2>/dev/null || exit "$status" +} +readonly STRIX_SOURCE_DIRS="$STRIX_SOURCE_DIRS_SANITIZED" +unset STRIX_SOURCE_DIRS_SANITIZED + sanitize_provider_name() { local provider provider="$(trim_whitespace "${1-}")" diff --git a/tests/conftest.py b/tests/conftest.py index 52922dbc8..66956f6cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from collections.abc import Iterator import pytest @@ -10,13 +11,17 @@ @pytest.fixture(autouse=True) -def clear_trusted_uv_process_caches() -> Iterator[None]: - """Isolate process-global trusted uv caches even when a test fails early.""" - materializer._install_trusted_uv.cache_clear() - materializer._install_trusted_uv_url_opener.cache_clear() +def clear_trusted_uv_process_caches(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: + """Isolate process caches and ambient Git configuration for every test.""" + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + install_trusted_uv = materializer._install_trusted_uv + install_url_opener = materializer._install_trusted_uv_url_opener + install_trusted_uv.cache_clear() + install_url_opener.cache_clear() yield - materializer._install_trusted_uv.cache_clear() - materializer._install_trusted_uv_url_opener.cache_clear() + install_trusted_uv.cache_clear() + install_url_opener.cache_clear() class FakeHttpResponse: diff --git a/tests/test_control_plane_branch_writer_absence.py b/tests/test_control_plane_branch_writer_absence.py new file mode 100644 index 000000000..2000b9570 --- /dev/null +++ b/tests/test_control_plane_branch_writer_absence.py @@ -0,0 +1,38 @@ +"""Contracts that keep transient pull-request branch writers out of the control plane.""" + +from __future__ import annotations + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +PROHIBITED_FILE_PATTERNS = ( + ".github/workflows/one-shot-*.yml", + ".github/workflows/one-shot-*.yaml", + ".github/workflows/repair-pr*.yml", + ".github/workflows/repair-pr*.yaml", + "scripts/ci/apply_pr*.py", +) +PROHIBITED_DIRECTORY_PATTERNS = ( + ".github/pr*-patch", + ".github/pr*-repair", +) + + +def test_transient_pull_request_branch_writers_are_absent() -> None: + """Reject branch-local repair workflows, apply helpers, and encoded patches.""" + offending_files = sorted( + path.relative_to(REPOSITORY_ROOT).as_posix() + for pattern in PROHIBITED_FILE_PATTERNS + for path in REPOSITORY_ROOT.glob(pattern) + if path.is_file() + ) + offending_directories = sorted( + path.relative_to(REPOSITORY_ROOT).as_posix() + for pattern in PROHIBITED_DIRECTORY_PATTERNS + for path in REPOSITORY_ROOT.glob(pattern) + if path.is_dir() + ) + + assert offending_files == [] + assert offending_directories == [] diff --git a/tests/test_coverage_materializer_failure_diagnostics.py b/tests/test_coverage_materializer_failure_diagnostics.py new file mode 100644 index 000000000..d0c10d425 --- /dev/null +++ b/tests/test_coverage_materializer_failure_diagnostics.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import json +from pathlib import Path +from types import ModuleType +from typing import Callable + +import pytest + +from scripts.ci import materialize_base_javascript_packages as javascript_materializer +from scripts.ci import materialize_base_python_requirements as python_materializer + + +def _failing_materializer(error: BaseException) -> Callable[..., None]: + """Return a materializer stub that raises the supplied failure.""" + + def fail(*_args: object, **_kwargs: object) -> None: + raise error + + return fail + + +def _run_main(module: ModuleType, tmp_path: Path) -> int: + """Invoke one materializer CLI with a valid-shaped isolated argument set.""" + return module.main( + [ + "--repo-root", + str(tmp_path), + "--base-sha", + "a" * 40, + "--output-dir", + str(tmp_path / "output"), + ] + ) + + +def test_javascript_failure_publishes_exact_coverage_reason( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The deterministic review receives the exact early npm-lock failure.""" + output_file = tmp_path / "github-output" + exact_reason = ( + "current-head npm lock package-lock.json package " + "apps/desktop/node_modules/@types/react-dom must pin a registry " + "tarball and SHA-512 integrity" + ) + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setattr( + javascript_materializer, + "materialize", + _failing_materializer(ValueError(exact_reason)), + ) + + assert _run_main(javascript_materializer, tmp_path) == 1 + + published = output_file.read_text(encoding="utf-8") + assert "coverage_summary< None: + """Early Python-lock failures remain concrete without output-file injection.""" + output_file = tmp_path / "github-output" + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setattr( + python_materializer, + "materialize", + _failing_materializer( + OSError( + "fixture \nCWL_COVERAGE_SUMMARY_EOF " + ("x" * 5000) + ) + ), + ) + + assert _run_main(python_materializer, tmp_path) == 1 + + published = output_file.read_text(encoding="utf-8") + assert "- Failed stage: Base Python lock materialization" in published + assert "OSError: fixture <unsafe> CWL_COVERAGE_SUMMARY_END" in published + assert "" not in published + assert published.count("CWL_COVERAGE_SUMMARY_EOF\n") == 2 + assert len(published) < 5000 + + +@pytest.mark.parametrize( + "module", + [javascript_materializer, python_materializer], + ids=["javascript", "python"], +) +def test_materializer_failure_summary_redacts_mixed_credentials( + module: ModuleType, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Every materializer removes mixed credentials before GitHub publication.""" + output_file = tmp_path / "github-output" + secret_values = ("url-secret", "bearer-secret", "token-secret") + reason = ( + "failure https://alice:url-secret@example.invalid/a.tgz " + "Authorization: Bearer bearer-secret TOKEN=token-secret trailing context" + ) + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + monkeypatch.setattr( + module, + "materialize", + _failing_materializer(RuntimeError(reason)), + ) + + assert _run_main(module, tmp_path) == 1 + + published = output_file.read_text(encoding="utf-8") + assert "https://<redacted>@example.invalid/a.tgz" in published + assert "Authorization: <redacted>" in published + assert "TOKEN=" not in published + for secret_value in secret_values: + assert secret_value not in published + + +@pytest.mark.parametrize( + "module", + [javascript_materializer, python_materializer], + ids=["javascript", "python"], +) +def test_failure_diagnostics_are_optional_outside_github_actions( + module: ModuleType, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Local CLI failures keep their status when no Actions output file exists.""" + monkeypatch.delenv("GITHUB_OUTPUT", raising=False) + monkeypatch.setattr( + module, + "materialize", + _failing_materializer(RuntimeError("local fixture failure")), + ) + + assert _run_main(module, tmp_path) == 1 + assert not (tmp_path / "github-output").exists() + + +def test_javascript_tree_filter_continues_after_non_regular_entries( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Symlinks and gitlinks cannot hide later regular lock inputs.""" + tree_output = ( + b"120000 blob " + (b"1" * 40) + b"\tsymlinked-lock\0" + b"160000 commit " + (b"2" * 40) + b"\tvendored-module\0" + b"100644 blob " + (b"3" * 40) + b"\tpackage.json\0" + ) + monkeypatch.setattr( + javascript_materializer, + "_git", + lambda *_args: tree_output, + ) + + assert javascript_materializer._regular_base_paths(tmp_path, "a" * 40) == { + "package.json" + } + + +def test_npm_project_without_packages_map_keeps_root_inputs( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Legacy npm locks without a packages map still preserve trusted root inputs.""" + regular_paths = {"package.json", "package-lock.json"} + lock_content = json.dumps({"name": "legacy", "lockfileVersion": 1}).encode() + monkeypatch.setattr( + javascript_materializer, + "_regular_base_paths", + lambda *_args: regular_paths, + ) + + def fake_git(_repo_root: Path, _command: str, object_spec: str) -> bytes: + if object_spec.endswith(":package.json"): + return b'{"name":"legacy"}' + if object_spec.endswith(":package-lock.json"): + return lock_content + raise AssertionError(f"unexpected git object: {object_spec}") + + monkeypatch.setattr(javascript_materializer, "_git", fake_git) + + projects = javascript_materializer.base_npm_projects(tmp_path, "a" * 40) + + assert projects == [ + ( + "package-lock.json", + "npm", + { + "package.json": b'{"name":"legacy"}', + "package-lock.json": lock_content, + }, + ) + ] + + +def test_npm_workspace_scan_iterates_missing_and_regular_manifests( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Missing workspace manifests are skipped without hiding later regular ones.""" + regular_paths = { + "package.json", + "package-lock.json", + "packages/alpha/package.json", + "packages/beta/package.json", + } + lock_content = json.dumps( + { + "name": "workspace-root", + "lockfileVersion": 3, + "packages": { + "": {"name": "workspace-root"}, + "packages/aaa-missing": {"name": "missing"}, + "packages/alpha": {"name": "alpha"}, + "packages/beta": {"name": "beta"}, + }, + } + ).encode() + blobs = { + "package.json": b'{"name":"workspace-root"}', + "package-lock.json": lock_content, + "packages/alpha/package.json": b'{"name":"alpha"}', + "packages/beta/package.json": b'{"name":"beta"}', + } + monkeypatch.setattr( + javascript_materializer, + "_regular_base_paths", + lambda *_args: regular_paths, + ) + + def fake_git(_repo_root: Path, _command: str, object_spec: str) -> bytes: + return blobs[object_spec.split(":", 1)[1]] + + monkeypatch.setattr(javascript_materializer, "_git", fake_git) + + projects = javascript_materializer.base_npm_projects(tmp_path, "a" * 40) + + assert len(projects) == 1 + assert "packages/aaa-missing/package.json" not in projects[0][2] + assert projects[0][2]["packages/alpha/package.json"] == b'{"name":"alpha"}' + assert projects[0][2]["packages/beta/package.json"] == b'{"name":"beta"}' diff --git a/tests/test_coverage_native_fuzz_lock_boundary.py b/tests/test_coverage_native_fuzz_lock_boundary.py new file mode 100644 index 000000000..36f1b3d77 --- /dev/null +++ b/tests/test_coverage_native_fuzz_lock_boundary.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import subprocess +from pathlib import Path + +from scripts.ci import materialize_base_python_requirements as materializer + + +def _git(repo: Path, *args: str) -> str: + """Run one deterministic Git command inside a temporary fixture repository.""" + + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _hashed_requirement(package_name: str, digest_character: str) -> str: + """Create one syntactically hash-pinned requirement fixture line.""" + + return ( + f"{package_name}==1.0.0 --hash=sha256:" + f"{digest_character * 64}\n" + ) + + +def test_generic_coverage_excludes_only_the_exact_native_atheris_lock( + tmp_path: Path, +) -> None: + """Coverage retains test dependencies but never installs the Atheris toolchain.""" + + repo = tmp_path / "repository" + repo.mkdir() + _git(repo, "init") + _git(repo, "config", "user.name", "Coverage Boundary Test") + _git(repo, "config", "user.email", "coverage-boundary@example.invalid") + + fuzz_directory = repo / "fuzz" + fuzz_directory.mkdir() + (fuzz_directory / "requirements-atheris.txt").write_text( + _hashed_requirement("atheris", "a"), + encoding="utf-8", + ) + (fuzz_directory / "requirements-property.txt").write_text( + _hashed_requirement("hypothesis", "b"), + encoding="utf-8", + ) + + service_directory = repo / "services" / "example_service" + service_directory.mkdir(parents=True) + (service_directory / "requirements-fuzz-regression.txt").write_text( + _hashed_requirement("pytest", "c"), + encoding="utf-8", + ) + + _git(repo, "add", ".") + _git(repo, "commit", "-m", "base dependency roles") + base_sha = _git(repo, "rev-parse", "HEAD") + + output_directory = tmp_path / "materialized" + manifest = materializer.materialize(repo, base_sha, output_directory) + + assert [entry["source"] for entry in manifest] == [ + "fuzz/requirements-property.txt", + "services/example_service/requirements-fuzz-regression.txt", + ] + assert "requirements-atheris.txt" not in ( + output_directory / "manifest.json" + ).read_text(encoding="utf-8") + + +def test_native_fuzz_engine_classifier_uses_exact_file_names() -> None: + """Role classification cannot expand through substrings or directory names.""" + + assert materializer._is_native_fuzz_engine_lock_name( + "requirements-atheris.txt" + ) + assert not materializer._is_native_fuzz_engine_lock_name( + "requirements-atheris-regression.txt" + ) + assert not materializer._is_native_fuzz_engine_lock_name( + "requirements-property.txt" + ) diff --git a/tests/test_javascript_coverage_gate_type_only.py b/tests/test_javascript_coverage_gate_type_only.py new file mode 100644 index 000000000..30b43c191 --- /dev/null +++ b/tests/test_javascript_coverage_gate_type_only.py @@ -0,0 +1,355 @@ +"""Regression tests for type-only TypeScript changed-source coverage.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +from scripts.ci import javascript_coverage_gate as gate + + +def git(repo: Path, *args: str) -> str: + """Run Git in a fixture repository and return stripped stdout.""" + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ).stdout.strip() + + +def commit(repo: Path, message: str) -> str: + """Commit the fixture tree and return the resulting exact SHA.""" + git(repo, "add", ".") + git(repo, "commit", "-m", message) + return git(repo, "rev-parse", "HEAD") + + +def test_missing_type_only_source_does_not_require_istanbul_instrumentation( + tmp_path: Path, capsys +) -> None: + """Permit an omitted file only when every changed line is type-only.""" + repo = tmp_path / "repo" + source = repo / "src" / "types.ts" + source.parent.mkdir(parents=True) + git(repo, "init", "-b", "main") + git(repo, "config", "user.name", "Coverage Test") + git(repo, "config", "user.email", "coverage@example.invalid") + source.write_text( + "import type {\n" + " JSONContent,\n" + "} from './document.js';\n" + "\n" + "/** Detached editor state. */\n" + "export interface EditorSnapshot {\n" + " readonly document: JSONContent;\n" + " readonly value: string;\n" + "}\n" + "\n" + "/** Supported serialization modes. */\n" + "export type EditorMode =\n" + " | 'markdown'\n" + " | 'html';\n", + encoding="utf-8", + ) + base_sha = commit(repo, "base type surface") + source.write_text( + "import type {\n" + " JSONContent,\n" + "} from './document.js';\n" + "\n" + "/** Detached editor state. */\n" + "export interface EditorSnapshot {\n" + " readonly document: JSONContent;\n" + " readonly value: string;\n" + " /** Destination-free reading-order projection. */\n" + " readonly plainText: string;\n" + "}\n" + "\n" + "/** Supported serialization modes. */\n" + "export type EditorMode =\n" + " | 'markdown'\n" + " | 'html';\n", + encoding="utf-8", + ) + head_sha = commit(repo, "extend type surface") + + coverage_dir = repo / "coverage" + coverage_dir.mkdir() + (coverage_dir / "coverage-final.json").write_text( + json.dumps({"unrelated.ts": {"s": {}, "f": {}, "b": {}}}), + encoding="utf-8", + ) + (coverage_dir / "coverage-summary.json").write_text( + json.dumps( + { + "total": { + metric: {"pct": 100.0} + for metric in gate.METRICS + } + } + ), + encoding="utf-8", + ) + summary_list = repo / "coverage-files.txt" + summary_list.write_text( + "coverage/coverage-summary.json\ncoverage/coverage-final.json\n", + encoding="utf-8", + ) + + assert ( + gate.main( + [ + "--repo-root", + str(repo), + "--base-sha", + base_sha, + "--head-sha", + head_sha, + "--summary-list", + str(summary_list), + ] + ) + == 0 + ) + report = capsys.readouterr().out + assert "type-only declarations" in report + assert "Result: PASS" in report + + +def test_type_only_classifier_accepts_balanced_type_aliases(tmp_path: Path) -> None: + """Recognize complete semicolon-terminated aliases without runtime tails.""" + source = tmp_path / "src" / "type_aliases.ts" + source.parent.mkdir(parents=True) + source.write_text( + "export type InlineAlias = string;\n" + "export type UnionAlias =\n" + " | 'left'\n" + " | 'right';\n" + "export type ObjectAlias = {\n" + " readonly value: string;\n" + " readonly nested: {\n" + " readonly count: number;\n" + " };\n" + "};\n", + encoding="utf-8", + ) + + assert gate.likely_runtime_lines( + tmp_path, + "src/type_aliases.ts", + set(range(1, 11)), + ) == [] + + +def test_type_only_classifier_rejects_mixed_runtime_tails(tmp_path: Path) -> None: + """Do not let declaration prefixes or comment braces hide runtime code.""" + source = tmp_path / "src" / "mixed_types.ts" + source.parent.mkdir(parents=True) + source.write_text( + "export interface InlineShape {} const inlineRuntime = 1;\n" + "import type { InlineShape } from './shape.js'; runInline();\n" + "interface MultilineShape {\n" + " readonly value: string;\n" + "} runAfterInterface();\n" + "import type {\n" + " MultilineShape,\n" + "} from './shape.js'; runAfterImport();\n" + "interface CommentedShape {\n" + " readonly value: string; /* brace { */\n" + "}\n" + "runAfterComment();\n" + "import type { SingleLineShape } from './shape.js'\n" + "runAfterSemicolonlessImport();\n" + "import type {\n" + " MultilineShape,\n" + "} from './shape.js'\n" + "runAfterMultilineSemicolonlessImport();\n" + "export type InlineAlias = string; runAfterTypeAlias();\n" + "export type MultilineAlias =\n" + " | 'left'\n" + " | 'right'; runAfterMultilineType();\n" + "export type SemicolonlessAlias = string\n" + "runAfterSemicolonlessType();\n", + encoding="utf-8", + ) + + assert gate.likely_runtime_lines( + tmp_path, + "src/mixed_types.ts", + set(range(1, 25)), + ) == [1, 2, 5, 8, 12, 14, 18, 19, 22, 23, 24] + + +def test_type_only_classifier_fails_closed_on_lexical_edges(tmp_path: Path) -> None: + """Reject malformed literals, stray closers, and unfinished declarations.""" + source = tmp_path / "src" / "lexical_edges.ts" + source.parent.mkdir(parents=True) + source.write_text( + "interface UnterminatedString {\n" + " readonly safe: '{';\n" + " readonly bad: 'unterminated {\n" + "}\n" + "runAfterUnterminatedString();\n" + "interface StrayComment {\n" + " readonly bad: string; */\n" + "}\n" + "runAfterStrayComment();\n" + "interface OpenComment {\n" + " readonly value: string; /* brace {\n" + " still comment\n" + " */\n" + "}\n" + "runAfterOpenComment();\n" + "interface CloseTail {\n" + " /* comment\n" + " */ } runAfterCommentClose();\n" + "export type UnterminatedAlias =\n" + " | 'left'\n", + encoding="utf-8", + ) + + assert gate.likely_runtime_lines( + tmp_path, + "src/lexical_edges.ts", + set(range(1, 21)), + ) == [3, 5, 7, 9, 15, 18, 19, 20] + + +def test_changed_runtime_lines_ignores_deletion_only_hunks(tmp_path: Path) -> None: + """Do not invent changed executable lines for a deletion-only hunk.""" + repo = tmp_path / "repo" + source = repo / "src" / "runtime.ts" + source.parent.mkdir(parents=True) + git(repo, "init", "-b", "main") + git(repo, "config", "user.name", "Coverage Test") + git(repo, "config", "user.email", "coverage@example.invalid") + source.write_text( + "export const retained = 1;\nexport const removed = 2;\n", + encoding="utf-8", + ) + base_sha = commit(repo, "base runtime") + source.write_text("export const retained = 1;\n", encoding="utf-8") + head_sha = commit(repo, "remove runtime line") + + assert gate.changed_runtime_lines(repo, base_sha, head_sha) == {} + + +def test_summary_and_path_helpers_cover_fallthrough_cases(tmp_path: Path) -> None: + """Exercise invalid line metadata and nonmatching normalized paths.""" + metrics = gate.summarize_final( + { + "invalid.ts": { + "s": {"0": 1}, + "f": {}, + "b": {}, + "statementMap": {"0": {"start": {"line": "invalid"}}}, + } + } + ) + assert metrics == { + "statements": 100.0, + "branches": 100.0, + "functions": 100.0, + "lines": 100.0, + } + + changed_paths = {"src/runtime.ts"} + assert ( + gate.normalize_coverage_path( + str(tmp_path / "other.ts"), tmp_path, changed_paths + ) + is None + ) + assert ( + gate.normalize_coverage_path("./other.ts", tmp_path, changed_paths) + is None + ) + + +def test_delimiter_state_helpers_reject_unmatched_closers() -> None: + """Cover balanced nesting and every fail-closed unmatched closer.""" + assert gate._advance_interface_state("{{} nested", 0) == (1, None) + assert gate._advance_type_alias_state("()[]{}", (0, 0, 0)) == ( + (0, 0, 0), + None, + True, + ) + assert gate._advance_type_alias_state(")", (0, 0, 0)) == ( + (0, 0, 0), + None, + False, + ) + assert gate._advance_type_alias_state("]", (0, 0, 0)) == ( + (0, 0, 0), + None, + False, + ) + assert gate._advance_type_alias_state("}", (0, 0, 0)) == ( + (0, 0, 0), + None, + False, + ) + + +def test_classifier_fails_closed_on_unfinished_state(tmp_path: Path) -> None: + """Retain changed evidence for every unfinished lexical or type state.""" + cases = { + "unfinished_import.ts": "import type {\n Missing,\n", + "unfinished_interface.ts": "interface Missing {\n value: string;\n", + "unfinished_alias.ts": "export type Missing =\n | 'left'\n", + "unfinished_comment.ts": "/* open comment\nstill open\n", + } + for name, content in cases.items(): + source = tmp_path / "src" / name + source.parent.mkdir(parents=True, exist_ok=True) + source.write_text(content, encoding="utf-8") + assert gate.likely_runtime_lines( + tmp_path, + f"src/{name}", + {1, 2}, + ) == [1, 2] + + +def test_classifier_rejects_malformed_tail_after_comment_close( + tmp_path: Path, +) -> None: + """Do not repair malformed code after a multiline comment closes.""" + source = tmp_path / "src" / "malformed_comment_tail.ts" + source.parent.mkdir(parents=True) + source.write_text( + "interface Broken {\n" + " /* comment\n" + " */ 'unterminated\n" + "}\n", + encoding="utf-8", + ) + + assert gate.likely_runtime_lines( + tmp_path, + "src/malformed_comment_tail.ts", + {1, 2, 3, 4}, + ) == [3] + + +def test_load_coverage_files_accepts_absolute_and_ignores_unknown_names( + tmp_path: Path, +) -> None: + """Load named evidence from mixed paths and skip unrelated JSON files.""" + summary = tmp_path / "coverage-summary.json" + final = tmp_path / "coverage-final.json" + ignored = tmp_path / "ignored.json" + summary.write_text("{}\n", encoding="utf-8") + final.write_text("{}\n", encoding="utf-8") + ignored.write_text("{}\n", encoding="utf-8") + summary_list = tmp_path / "coverage-files.txt" + summary_list.write_text( + f"\n{summary}\ncoverage-final.json\nignored.json\n", + encoding="utf-8", + ) + + summaries, finals = gate.load_coverage_files(tmp_path, summary_list) + assert summaries == [(summary, {})] + assert finals == [(final, {})] diff --git a/tests/test_opencode_llvm_coverage_current_main.py b/tests/test_opencode_llvm_coverage_current_main.py new file mode 100644 index 000000000..82ec07912 --- /dev/null +++ b/tests/test_opencode_llvm_coverage_current_main.py @@ -0,0 +1,109 @@ +from pathlib import Path + + +_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") +_QUALITY_WORKFLOW = Path( + ".github/workflows/opencode-coverage-toolchain-quality-ci.yml" +) + + +def test_opencode_coverage_image_provisions_compatible_llvm_tools_before_cargo_llvm_cov(): + """Require a compatible system LLVM pair before installing cargo-llvm-cov.""" + workflow = _WORKFLOW.read_text(encoding="utf-8") + + llvm_install = workflow.index(" llvm-19 " + chr(92)) + llvm_cov_env = workflow.index("ENV LLVM_COV=/usr/bin/llvm-cov-19") + llvm_profdata_env = workflow.index("ENV LLVM_PROFDATA=/usr/bin/llvm-profdata-19") + llvm_check = workflow.index( + 'RUN test -x "$LLVM_COV" && test -x "$LLVM_PROFDATA"' + ) + cargo_llvm_cov_install = workflow.index( + "https://github.com/taiki-e/cargo-llvm-cov/releases/download/" + ) + + assert llvm_install < llvm_cov_env < llvm_check < cargo_llvm_cov_install + assert llvm_install < llvm_profdata_env < llvm_check + + +def test_low_privilege_coverage_wrappers_isolate_ambient_git_configuration(): + """Require system and global Git isolation before the safe-directory overlay.""" + workflow = _WORKFLOW.read_text(encoding="utf-8") + boundaries = ( + ("run_and_capture", "run_r_package_testthat"), + ("run_r_package_testthat", "run_and_capture_advisory"), + ("run_and_capture_advisory", "trusted_git"), + ) + + for wrapper_name, next_name in boundaries: + start = workflow.index(f" {wrapper_name}() {{") + end = workflow.index(f" {next_name}() {{", start) + wrapper = workflow[start:end] + + no_system = wrapper.index("GIT_CONFIG_NOSYSTEM=1") + no_global = wrapper.index("GIT_CONFIG_GLOBAL=/dev/null") + safe_directory_count = wrapper.index("GIT_CONFIG_COUNT=1") + + assert no_system < no_global < safe_directory_count + assert wrapper.count("GIT_CONFIG_NOSYSTEM=1") == 1 + assert wrapper.count("GIT_CONFIG_GLOBAL=/dev/null") == 1 + + +def test_opencode_toolchain_quality_workflow_is_exact_head_bound_and_offline(): + """Require the fast contract job to stay exact-head and dependency-free.""" + workflow = _QUALITY_WORKFLOW.read_text(encoding="utf-8") + start = workflow.index(" exact-head-contract:") + end = workflow.index(" full-repository-quality:", start) + fast_job = workflow[start:end] + + assert "ref: ${{ github.event.pull_request.head.sha }}" in fast_job + assert "persist-credentials: false" in fast_job + assert ( + 'test "$(git rev-parse HEAD)" = ' + '"${{ github.event.pull_request.head.sha }}"' in fast_job + ) + assert "importlib.util.spec_from_file_location" in fast_job + assert "python3 -m compileall -q" in fast_job + assert "pip install" not in fast_job + assert "uv sync" not in fast_job + + +def test_opencode_toolchain_quality_tracks_every_llvm_contract_surface(): + """Require every LLVM contract and doctoring file to trigger exact-head quality.""" + workflow = _QUALITY_WORKFLOW.read_text(encoding="utf-8") + required_path_entries = ( + ' - "tests/test_opencode_llvm_coverage_current_main.py"', + ' - "tests/test_opencode_rust_coverage_toolchain_contract.py"', + ' - "docs/doctoring/opencode-llvm-coverage-toolchain.md"', + ' - "docs/doctoring/rust-llvm-coverage-toolchain.md"', + ) + + for path_entry in required_path_entries: + assert path_entry in workflow + + start = workflow.index(" exact-head-contract:") + end = workflow.index(" full-repository-quality:", start) + fast_job = workflow[start:end] + assert 'Path("tests/test_opencode_llvm_coverage_current_main.py")' in fast_job + assert 'Path("tests/test_opencode_rust_coverage_toolchain_contract.py")' in fast_job + + +def test_opencode_toolchain_quality_runs_full_hash_locked_repository_suite(): + """Require a separate exact-head full-suite job with 100% quality gates.""" + workflow = _QUALITY_WORKFLOW.read_text(encoding="utf-8") + start = workflow.index(" full-repository-quality:") + full_job = workflow[start:] + + assert "needs: exact-head-contract" in full_job + assert "ref: ${{ github.event.pull_request.head.sha }}" in full_job + assert "persist-credentials: false" in full_job + assert ( + 'test "$(git rev-parse HEAD)" = ' + '"${{ github.event.pull_request.head.sha }}"' in full_job + ) + assert "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" in full_job + assert 'python-version: "3.14"' in full_job + assert "--require-hashes -r requirements-opencode-review-ci-hashes.txt" in full_job + assert "python -m coverage run -m pytest tests -q" in full_job + assert "python -m coverage report" in full_job + assert "python -m interrogate --fail-under 100 scripts/ci" in full_job + assert "python -m compileall -q scripts/ci tests" in full_job diff --git a/tests/test_opencode_rust_coverage_toolchain_contract.py b/tests/test_opencode_rust_coverage_toolchain_contract.py new file mode 100644 index 000000000..2e2b2b084 --- /dev/null +++ b/tests/test_opencode_rust_coverage_toolchain_contract.py @@ -0,0 +1,83 @@ +"""Permanent contract for the trusted Rust LLVM coverage toolchain.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_WORKFLOW_PATH = _REPOSITORY_ROOT / ".github/workflows/opencode-review-dispatch.yml" +_LLVM_COV_PATH = "/usr/bin/llvm-cov-19" +_LLVM_PROFDATA_PATH = "/usr/bin/llvm-profdata-19" + + +def _workflow_text() -> str: + """Return the authoritative OpenCode review-dispatch workflow text.""" + + return _WORKFLOW_PATH.read_text(encoding="utf-8") + + +def _all_positions(text: str, fragment: str) -> list[int]: + """Return every start position of ``fragment`` in ``text``.""" + + return [match.start() for match in re.finditer(re.escape(fragment), text)] + + +def test_trusted_rust_coverage_image_provisions_verified_llvm_19_tools() -> None: + """Require explicit compatible LLVM tools before cargo-llvm-cov installation.""" + + workflow = _workflow_text() + + llvm_package = workflow.index("llvm-19") + llvm_cov_environment = workflow.index( + f"ENV LLVM_COV={_LLVM_COV_PATH}" + ) + llvm_profdata_environment = workflow.index( + f"ENV LLVM_PROFDATA={_LLVM_PROFDATA_PATH}" + ) + llvm_cov_checks = _all_positions(workflow, 'test -x "$LLVM_COV"') + llvm_profdata_checks = _all_positions(workflow, 'test -x "$LLVM_PROFDATA"') + cargo_llvm_cov_archive = workflow.index( + "cargo-llvm-cov-x86_64-unknown-linux-musl.tar.gz" + ) + + assert len(llvm_cov_checks) >= 2 + assert len(llvm_profdata_checks) >= 2 + assert ( + llvm_package + < llvm_cov_environment + < llvm_profdata_environment + < llvm_cov_checks[0] + < llvm_profdata_checks[0] + < cargo_llvm_cov_archive + ) + + +def test_isolated_runtime_receives_reviewed_llvm_constants() -> None: + """Require exact LLVM 19 path propagation through the Docker boundary.""" + + workflow = _workflow_text() + docker_run = workflow.index("docker run --rm") + llvm_cov_binding = workflow.index( + f"--env LLVM_COV={_LLVM_COV_PATH}", docker_run + ) + llvm_profdata_binding = workflow.index( + f"--env LLVM_PROFDATA={_LLVM_PROFDATA_PATH}", docker_run + ) + coverage_image = workflow.index('"$coverage_tool_image"', docker_run) + + assert docker_run < llvm_cov_binding < llvm_profdata_binding < coverage_image + + +def test_isolated_runtime_revalidates_llvm_tools_before_coverage() -> None: + """Require a second fail-closed executable check before Rust coverage.""" + + workflow = _workflow_text() + docker_run = workflow.index("docker run --rm") + cargo_coverage_invocation = workflow.index("cargo llvm-cov", docker_run) + llvm_cov_checks = _all_positions(workflow, 'test -x "$LLVM_COV"') + llvm_profdata_checks = _all_positions(workflow, 'test -x "$LLVM_PROFDATA"') + + assert docker_run < llvm_cov_checks[-1] < cargo_coverage_invocation + assert docker_run < llvm_profdata_checks[-1] < cargo_coverage_invocation diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py new file mode 100644 index 000000000..b63977a53 --- /dev/null +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -0,0 +1,38 @@ +"""Contracts for the privileged OpenCode pull-request autofix worker.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +AUTOFIX_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "pr-review-autofix.yml" + + +def test_pr_review_autofix_uses_only_nvidia_nim_for_llm_inference() -> None: + """Require the write-capable autofix agent to use the approved NVIDIA NIM secret.""" + workflow = AUTOFIX_WORKFLOW.read_text(encoding="utf-8") + + assert '"model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5"' in workflow + assert '"small_model": "nvidia-nim/meta/llama-3.3-70b-instruct"' in workflow + assert '"enabled_providers": ["nvidia-nim"]' in workflow + assert '"nvidia-nim": {' in workflow + assert '"baseURL": "https://integrate.api.nvidia.com/v1"' in workflow + assert '"apiKey": "{env:NVIDIA_API_KEY}"' in workflow + assert "NVIDIA_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}" in workflow + assert "MODEL: nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5" in workflow + + assert "STRIX_GITHUB_MODELS_TOKEN" not in workflow + assert '"github-models"' not in workflow + assert "models.github.ai" not in workflow + + +def test_pr_review_autofix_preserves_existing_github_write_identity_chain() -> None: + """Keep repository-write credentials separate from the NVIDIA model credential.""" + workflow = AUTOFIX_WORKFLOW.read_text(encoding="utf-8") + + existing_write_chain = ( + "secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || " + "steps.target_app_token.outputs.token || github.token" + ) + assert workflow.count(existing_write_chain) >= 2 + assert "GITHUB_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.target_app_token.outputs.token || github.token }}" in workflow + assert "NVIDIA_NIM_API_KEY" not in existing_write_chain diff --git a/tests/test_sanitize_github_output_summary.py b/tests/test_sanitize_github_output_summary.py index 72e2c19ee..d3f72e5a3 100644 --- a/tests/test_sanitize_github_output_summary.py +++ b/tests/test_sanitize_github_output_summary.py @@ -1,9 +1,8 @@ -import runpy import sys import pytest -from scripts.ci.sanitize_github_output_summary import sanitize_text +from scripts.ci.sanitize_github_output_summary import _entrypoint, sanitize_text def test_sanitizes_secret_like_coverage_summary_values_without_losing_result(): @@ -23,7 +22,7 @@ def test_sanitizes_secret_like_coverage_summary_values_without_losing_result(): assert "DATABASE_URL: " in sanitized assert "AUTH_SESSION_HMAC_SECRET: " in sanitized assert "ENCRYPTION_KEY=" in sanitized - assert "Authorization: Bearer " in sanitized + assert "Authorization: " in sanitized assert "secret@db" not in sanitized assert "super-secret" not in sanitized assert "token-value" not in sanitized @@ -36,6 +35,61 @@ def test_sanitizes_url_credentials_without_secret_key_prefix(): assert sanitized == "postgresql://@db:5432/app\n" +def test_sanitizes_url_userinfo_without_password(): + """A username-only URL authority cannot leak through coverage evidence.""" + sanitized = sanitize_text("https://alice@example.invalid/artifact\n") + + assert sanitized == "https://@example.invalid/artifact\n" + assert "alice" not in sanitized + + +def test_sanitizes_mixed_credentials_before_truncating_at_secret_key(): + """Mixed URL, Authorization, and key-value secrets are all removed.""" + source = ( + "failure https://alice:url-secret@example.invalid/a.tgz " + "Authorization: Bearer bearer-secret TOKEN=token-secret trailing context\n" + ) + + sanitized = sanitize_text(source) + + assert "https://@example.invalid/a.tgz" in sanitized + assert "Authorization: " in sanitized + assert "TOKEN=" not in sanitized + assert "url-secret" not in sanitized + assert "bearer-secret" not in sanitized + assert "token-secret" not in sanitized + + +def test_sanitizes_every_authorization_scheme_and_scheme_less_value(): + """No Authorization value survives because schemes are provider-extensible.""" + source = ( + "Authorization: Token token-secret\n" + "Authorization=Digest digest-secret\n" + "Authorization: AWS4-HMAC-SHA256 Credential=alice/signature-secret\n" + "Authorization: scheme-less-secret\n" + ) + + sanitized = sanitize_text(source) + + assert sanitized == ( + "Authorization: \n" + "Authorization=\n" + "Authorization: \n" + "Authorization: \n" + ) + for secret_value in ( + "Token", + "token-secret", + "Digest", + "digest-secret", + "AWS4-HMAC-SHA256", + "Credential", + "signature-secret", + "scheme-less-secret", + ): + assert secret_value not in sanitized + + def test_cli_writes_sanitized_summary(tmp_path, monkeypatch): source = tmp_path / "coverage.md" destination = tmp_path / "coverage-output.md" @@ -51,7 +105,7 @@ def test_cli_writes_sanitized_summary(tmp_path, monkeypatch): ) with pytest.raises(SystemExit) as excinfo: - runpy.run_path("scripts/ci/sanitize_github_output_summary.py", run_name="__main__") + _entrypoint("__main__") assert excinfo.value.code == 0 assert destination.read_text(encoding="utf-8") == "DATABASE_URL=\n- Result: PASS\n" diff --git a/tests/test_strix_dependency_security_floor.py b/tests/test_strix_dependency_security_floor.py new file mode 100644 index 000000000..768841a2b --- /dev/null +++ b/tests/test_strix_dependency_security_floor.py @@ -0,0 +1,23 @@ +"""Contracts for the security-reviewed Strix dependency lock.""" + +from __future__ import annotations + +from pathlib import Path + + +def test_strix_requirements_pin_reviewed_security_versions() -> None: + """The canonical input pins versions that close the August 2026 advisories.""" + requirements = Path("requirements-strix-ci.txt").read_text(encoding="utf-8") + assert "aiohttp==3.14.3\n" in requirements + assert "cryptography==50.0.0\n" in requirements + assert "aiohttp==3.14.1\n" not in requirements + assert "cryptography==49.0.0\n" not in requirements + + +def test_strix_hash_lock_matches_the_canonical_security_pins() -> None: + """The generated lock retains the reviewed direct security pins.""" + lock = Path("requirements-strix-ci-hashes.txt").read_text(encoding="utf-8") + assert "aiohttp==3.14.3 \\\n" in lock + assert "cryptography==50.0.0 \\\n" in lock + assert "aiohttp==3.14.1 \\\n" not in lock + assert "cryptography==49.0.0 \\\n" not in lock diff --git a/tests/test_strix_model_utils_source_dirs.py b/tests/test_strix_model_utils_source_dirs.py new file mode 100644 index 000000000..af4400425 --- /dev/null +++ b/tests/test_strix_model_utils_source_dirs.py @@ -0,0 +1,109 @@ +"""Regression tests for Strix source-directory input boundaries.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HELPER = ROOT / "scripts" / "ci" / "strix_model_utils.sh" +DIAGNOSTICS_WORKFLOW = ROOT / ".github" / "workflows" / "opencode-coverage-diagnostics-ci.yml" + + +def run_source(raw_value: str) -> subprocess.CompletedProcess[str]: + """Source the helper with one caller-controlled directory-list value.""" + + environment = os.environ.copy() + environment["STRIX_SOURCE_DIRS"] = raw_value + return subprocess.run( + [ + "bash", + "-c", + f'source "{HELPER}" || exit $?; printf "%s" "$STRIX_SOURCE_DIRS"', + ], + cwd=ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + + +def test_direct_source_directories_are_normalized_and_readonly() -> None: + """Keep direct safe names, Unicode names, and first-occurrence order.""" + + completed = run_source(". src 데이터 backend src 데이터") + assert completed.returncode == 0, completed.stderr + assert completed.stdout == ". src 데이터 backend" + + reassignment = subprocess.run( + [ + "bash", + "-c", + ( + f'STRIX_SOURCE_DIRS="."; source "{HELPER}"; ' + 'STRIX_SOURCE_DIRS="../etc"' + ), + ], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + assert reassignment.returncode != 0 + assert "readonly" in reassignment.stderr.lower() + + +def test_traversal_absolute_nested_glob_and_empty_values_fail_closed() -> None: + """Reject every path shape that can escape or broaden the scan root.""" + + unsafe_values = ( + "..", + "../etc", + "/etc", + "src/../etc", + "src/api", + "*", + "src[0-9]", + "src]", + "-rf", + " ", + "src\nbackend", + ) + for raw_value in unsafe_values: + completed = run_source(raw_value) + assert completed.returncode == 2, ( + raw_value, + completed.stdout, + completed.stderr, + ) + assert "STRIX_SOURCE_DIRS" in completed.stderr + + +def test_unsafe_punctuation_and_oversized_lists_fail_closed() -> None: + """Bound metacharacters, encoded size, and list cardinality.""" + + for raw_value in ("src;echo", "src$HOME", "src\\api", "src?"): + completed = run_source(raw_value) + assert completed.returncode == 2, raw_value + + oversized_entry = "a" * 256 + assert run_source(oversized_entry).returncode == 2 + + oversized_list = " ".join(f"dir{index}" for index in range(33)) + assert run_source(oversized_list).returncode == 2 + + +def test_permanent_diagnostics_runs_for_source_boundary_changes() -> None: + """Bind source-directory boundary edits to exact-head executable evidence.""" + + workflow = DIAGNOSTICS_WORKFLOW.read_text(encoding="utf-8") + executable_test_entry = " tests/test_strix_model_utils_source_dirs.py" + + assert workflow.count(' - "scripts/ci/strix_model_utils.sh"') == 2 + assert workflow.count(' - "tests/test_strix_model_utils_source_dirs.py"') == 2 + assert workflow.count(executable_test_entry) == 2 + assert executable_test_entry + " " + chr(92) in workflow diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index 23a849bd8..744787f7f 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -4,6 +4,9 @@ WORKFLOW_PATH = Path(".github/workflows/trusted-uv-materializer-quality-ci.yml") +OPENCODE_WORKFLOW_PATH = Path( + ".github/workflows/opencode-coverage-diagnostics-ci.yml" +) def _workflow_text() -> str: @@ -12,6 +15,12 @@ def _workflow_text() -> str: return WORKFLOW_PATH.read_text(encoding="utf-8") +def _opencode_workflow_text() -> str: + """Return the focused OpenCode coverage workflow as UTF-8 text.""" + + return OPENCODE_WORKFLOW_PATH.read_text(encoding="utf-8") + + def test_quality_workflow_runs_for_every_materializer_surface() -> None: """Changes to production, tests, tooling, or the gate itself trigger evidence.""" @@ -98,3 +107,21 @@ def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> ) for test_path in required_tests: assert test_path in workflow + + +def test_opencode_diagnostics_workflow_directly_validates_javascript_gate() -> None: + """The focused exact-head workflow owns the gate source and regressions.""" + + workflow = _opencode_workflow_text() + trigger_paths = ( + '"scripts/ci/javascript_coverage_gate.py"', + '"tests/test_javascript_coverage_gate.py"', + '"tests/test_javascript_coverage_gate_type_only.py"', + ) + for trigger_path in trigger_paths: + assert workflow.count(trigger_path) == 2 + + assert "tests/test_javascript_coverage_gate.py" in workflow + assert "tests/test_javascript_coverage_gate_type_only.py" in workflow + assert "--cov=scripts.ci.javascript_coverage_gate" in workflow + assert "scripts/ci/javascript_coverage_gate.py" in workflow